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
31 from werkzeug.exceptions import NotFound
33 logger = logging.getLogger(__name__)
35 def url_for(path_or_uri, lang=None):
36 if isinstance(path_or_uri, unicode):
37 path_or_uri = path_or_uri.encode('utf-8')
38 current_path = request.httprequest.path
39 if isinstance(current_path, unicode):
40 current_path = current_path.encode('utf-8')
41 location = path_or_uri.strip()
42 force_lang = lang is not None
43 url = urlparse.urlparse(location)
45 if request and not url.netloc and not url.scheme and (url.path or force_lang):
46 location = urlparse.urljoin(current_path, location)
48 lang = lang or request.context.get('lang')
49 langs = [lg[0] for lg in request.website.get_languages()]
51 if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs):
52 ps = location.split('/')
54 # Replace the language only if we explicitly provide a language to url_for
57 # Remove the default language unless it's explicitly provided
58 elif ps[1] == request.website.default_lang_code:
60 # Insert the context language or the provided language
61 elif lang != request.website.default_lang_code or force_lang:
63 location = '/'.join(ps)
65 return location.decode('utf-8')
67 def is_multilang_url(local_url, langs=None):
69 langs = [lg[0] for lg in request.website.get_languages()]
70 spath = local_url.split('/')
71 # if a language is already in the path, remove it
74 local_url = '/'.join(spath)
76 # Try to match an endpoint in werkzeug's routing table
77 url = local_url.split('?')
79 query_string = url[1] if len(url) > 1 else None
80 router = request.httprequest.app.get_db_router(request.db).bind('')
81 func = router.match(path, query_args=query_string)[0]
82 return func.routing.get('website', False) and func.routing.get('multilang', True)
86 def slugify(s, max_length=None):
87 """ Transform a string to a slug that can be used in a url path.
89 This method will first try to do the job with python-slugify if present.
90 Otherwise it will process string by stripping leading and ending spaces,
91 converting unicode chars to ascii, lowering all chars and replacing spaces
92 and underscore with hyphen "-".
95 :param max_length: int
100 # There are 2 different libraries only python-slugify is supported
102 return slugify_lib.slugify(s, max_length=max_length)
105 uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
106 slug = re.sub('[\W_]', ' ', uni).strip().lower()
107 slug = re.sub('[-\s]+', '-', slug)
109 return slug[:max_length]
112 if isinstance(value, orm.browse_record):
113 # [(id, name)] = value.name_get()
114 id, name = value.id, value.display_name
116 # assume name_search result tuple
118 slugname = slugify(name or '').strip().strip('-')
121 return "%s-%d" % (slugname, id)
124 # NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags
125 _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)')
128 """Extract slug and id from a string.
129 Always return un 2-tuple (str|None, int|None)
131 m = _UNSLUG_RE.match(s)
134 return m.group(1), int(m.group(2))
136 def urlplus(url, params):
137 return werkzeug.Href(url)(params or None)
139 class website(osv.osv):
140 def _get_menu(self, cr, uid, ids, name, arg, context=None):
142 menu_obj = self.pool.get('website.menu')
144 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False), ('website_id', '=', id)], order='id', context=context)
145 res[id] = menu_ids and menu_ids[0] or False
148 _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
149 _description = "Website"
151 'name': fields.char('Website Name'),
152 'domain': fields.char('Website 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, [('domain', '=', 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, cache=None, 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)
571 response.cache_control.max_age = cache
572 response.expires = int(time.time() + cache)
574 # conditional request match
575 if response.status_code == 304:
578 data = record[field].decode('base64')
579 image = Image.open(cStringIO.StringIO(data))
580 response.mimetype = Image.MIME[image.format]
582 filename = '%s_%s.%s' % (model.replace('.', '_'), id, str(image.format).lower())
583 response.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
585 if (not max_width) and (not max_height):
590 max_w = int(max_width) if max_width else maxint
591 max_h = int(max_height) if max_height else maxint
593 if w < max_w and h < max_h:
596 size = (max_w, max_h)
597 img = image_resize_and_sharpen(image, size)
598 image_save_for_web(img, response.stream, format=image.format)
599 # invalidate content-length computed by make_conditional as
600 # writing to response.stream does not do it (as of werkzeug 0.9.3)
601 del response.headers['Content-Length']
605 def image_url(self, cr, uid, record, field, size=None, context=None):
606 """Returns a local url that points to the image field of a given browse record."""
608 id = '%s_%s' % (record.id, hashlib.sha1(record.sudo().write_date).hexdigest()[0:7])
609 size = '' if size is None else '/%s' % size
610 return '/website/image/%s/%s/%s%s' % (model, id, field, size)
613 class website_menu(osv.osv):
614 _name = "website.menu"
615 _description = "Website Menu"
617 'name': fields.char('Menu', required=True, translate=True),
618 'url': fields.char('Url'),
619 'new_window': fields.boolean('New Window'),
620 'sequence': fields.integer('Sequence'),
621 # TODO: support multiwebsite once done for ir.ui.views
622 'website_id': fields.many2one('website', 'Website'),
623 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
624 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
625 'parent_left': fields.integer('Parent Left', select=True),
626 'parent_right': fields.integer('Parent Right', select=True),
629 def __defaults_sequence(self, cr, uid, context):
630 menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
631 return menu and menu[0]["sequence"] or 0
635 'sequence': __defaults_sequence,
639 _parent_order = 'sequence'
642 # would be better to take a menu_id as argument
643 def get_tree(self, cr, uid, website_id, context=None):
649 new_window=node.new_window,
650 sequence=node.sequence,
651 parent_id=node.parent_id.id,
654 for child in node.child_id:
655 menu_node['children'].append(make_tree(child))
657 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
658 return make_tree(menu)
660 def save(self, cr, uid, website_id, data, context=None):
661 def replace_id(old_id, new_id):
662 for menu in data['data']:
663 if menu['id'] == old_id:
665 if menu['parent_id'] == old_id:
666 menu['parent_id'] = new_id
667 to_delete = data['to_delete']
669 self.unlink(cr, uid, to_delete, context=context)
670 for menu in data['data']:
672 if isinstance(mid, str):
673 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
674 replace_id(mid, new_id)
675 for menu in data['data']:
676 self.write(cr, uid, [menu['id']], menu, context=context)
679 class ir_attachment(osv.osv):
680 _inherit = "ir.attachment"
681 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
683 for attach in self.browse(cr, uid, ids, context=context):
685 result[attach.id] = attach.url
687 result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
689 def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
691 (attach['id'], self._compute_checksum(attach))
692 for attach in self.read(
693 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
697 def _compute_checksum(self, attachment_dict):
698 if attachment_dict.get('res_model') == 'ir.ui.view'\
699 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
700 and attachment_dict.get('type', 'binary') == 'binary'\
701 and attachment_dict.get('datas'):
702 return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
705 def _datas_big(self, cr, uid, ids, name, arg, context=None):
706 result = dict.fromkeys(ids, False)
707 if context and context.get('bin_size'):
710 for record in self.browse(cr, uid, ids, context=context):
711 if not record.datas: continue
713 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
714 except IOError: # apparently the error PIL.Image.open raises
720 'datas_checksum': fields.function(_datas_checksum, size=40,
721 string="Datas checksum", type='char', store=True, select=True),
722 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
723 'datas_big': fields.function (_datas_big, type='binary', store=True,
724 string="Resized file content"),
725 'mimetype': fields.char('Mime Type', readonly=True),
728 def _add_mimetype_if_needed(self, values):
729 if values.get('datas_fname'):
730 values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
732 def create(self, cr, uid, values, context=None):
733 chk = self._compute_checksum(values)
735 match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
738 self._add_mimetype_if_needed(values)
739 return super(ir_attachment, self).create(
740 cr, uid, values, context=context)
742 def write(self, cr, uid, ids, values, context=None):
743 self._add_mimetype_if_needed(values)
744 return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
746 def try_remove(self, cr, uid, ids, context=None):
747 """ Removes a web-based image attachment if it is used by no view
750 Returns a dict mapping attachments which would not be removed (if any)
751 mapped to the views preventing their removal
753 Views = self.pool['ir.ui.view']
754 attachments_to_remove = []
755 # views blocking removal of the attachment
756 removal_blocked_by = {}
758 for attachment in self.browse(cr, uid, ids, context=context):
759 # in-document URLs are html-escaped, a straight search will not
761 url = escape(attachment.website_url)
762 ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
765 removal_blocked_by[attachment.id] = Views.read(
766 cr, uid, ids, ['name'], context=context)
768 attachments_to_remove.append(attachment.id)
769 if attachments_to_remove:
770 self.unlink(cr, uid, attachments_to_remove, context=context)
771 return removal_blocked_by
773 class res_partner(osv.osv):
774 _inherit = "res.partner"
776 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
777 partner = self.browse(cr, uid, ids[0], context=context)
779 '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 ''),
780 'size': "%sx%s" % (height, width),
784 return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
786 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
787 partner = self.browse(cr, uid, ids[0], context=context)
789 '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 ''),
792 return urlplus('https://maps.google.com/maps' , params)
794 class res_company(osv.osv):
795 _inherit = "res.company"
796 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
797 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
798 return partner and partner.google_map_img(zoom, width, height, context=context) or None
799 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
800 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
801 return partner and partner.google_map_link(zoom, context=context) or None
803 class base_language_install(osv.osv_memory):
804 _inherit = "base.language.install"
806 'website_ids': fields.many2many('website', string='Websites to translate'),
809 def default_get(self, cr, uid, fields, context=None):
812 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
813 website_id = context.get('params', {}).get('website_id')
815 if 'website_ids' not in defaults:
816 defaults['website_ids'] = []
817 defaults['website_ids'].append(website_id)
820 def lang_install(self, cr, uid, ids, context=None):
823 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
824 language_obj = self.browse(cr, uid, ids)[0]
825 website_ids = [website.id for website in language_obj['website_ids']]
826 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
827 if website_ids and lang_id:
828 data = {'language_ids': [(4, lang_id[0])]}
829 self.pool['website'].write(cr, uid, website_ids, data)
830 params = context.get('params', {})
831 if 'url_return' in params:
833 'url': params['url_return'].replace('[lang]', language_obj['lang']),
834 'type': 'ir.actions.act_url',
839 class website_seo_metadata(osv.Model):
840 _name = 'website.seo.metadata'
841 _description = 'SEO metadata'
844 'website_meta_title': fields.char("Website meta title", translate=True),
845 'website_meta_description': fields.text("Website meta description", translate=True),
846 'website_meta_keywords': fields.char("Website meta keywords", translate=True),