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._fields:
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')
576 image = Image.open(cStringIO.StringIO(data))
577 response.mimetype = Image.MIME[image.format]
579 filename = '%s_%s.%s' % (model.replace('.', '_'), id, str(image.format).lower())
580 response.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
582 if (not max_width) and (not max_height):
587 max_w = int(max_width) if max_width else maxint
588 max_h = int(max_height) if max_height else maxint
590 if w < max_w and h < max_h:
593 size = (max_w, max_h)
594 img = image_resize_and_sharpen(image, size, preserve_aspect_ratio=True)
595 image_save_for_web(img, response.stream, format=image.format)
596 # invalidate content-length computed by make_conditional as
597 # writing to response.stream does not do it (as of werkzeug 0.9.3)
598 del response.headers['Content-Length']
602 def image_url(self, cr, uid, record, field, size=None, context=None):
603 """Returns a local url that points to the image field of a given browse record."""
605 id = '%s_%s' % (record.id, hashlib.sha1(record.sudo().write_date).hexdigest()[0:7])
606 size = '' if size is None else '/%s' % size
607 return '/website/image/%s/%s/%s%s' % (model, id, field, size)
610 class website_menu(osv.osv):
611 _name = "website.menu"
612 _description = "Website Menu"
614 'name': fields.char('Menu', required=True, translate=True),
615 'url': fields.char('Url'),
616 'new_window': fields.boolean('New Window'),
617 'sequence': fields.integer('Sequence'),
618 # TODO: support multiwebsite once done for ir.ui.views
619 'website_id': fields.many2one('website', 'Website'),
620 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
621 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
622 'parent_left': fields.integer('Parent Left', select=True),
623 'parent_right': fields.integer('Parent Right', select=True),
626 def __defaults_sequence(self, cr, uid, context):
627 menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
628 return menu and menu[0]["sequence"] or 0
632 'sequence': __defaults_sequence,
636 _parent_order = 'sequence'
639 # would be better to take a menu_id as argument
640 def get_tree(self, cr, uid, website_id, context=None):
646 new_window=node.new_window,
647 sequence=node.sequence,
648 parent_id=node.parent_id.id,
651 for child in node.child_id:
652 menu_node['children'].append(make_tree(child))
654 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
655 return make_tree(menu)
657 def save(self, cr, uid, website_id, data, context=None):
658 def replace_id(old_id, new_id):
659 for menu in data['data']:
660 if menu['id'] == old_id:
662 if menu['parent_id'] == old_id:
663 menu['parent_id'] = new_id
664 to_delete = data['to_delete']
666 self.unlink(cr, uid, to_delete, context=context)
667 for menu in data['data']:
669 if isinstance(mid, str):
670 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
671 replace_id(mid, new_id)
672 for menu in data['data']:
673 self.write(cr, uid, [menu['id']], menu, context=context)
676 class ir_attachment(osv.osv):
677 _inherit = "ir.attachment"
678 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
680 for attach in self.browse(cr, uid, ids, context=context):
682 result[attach.id] = attach.url
684 result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
686 def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
687 result = dict.fromkeys(ids, False)
688 attachments = self.read(cr, uid, ids, ['res_model'], context=context)
689 view_attachment_ids = [attachment['id'] for attachment in attachments if attachment['res_model'] == 'ir.ui.view']
690 for attach in self.read(cr, uid, view_attachment_ids, ['res_model', 'res_id', 'type', 'datas'], context=context):
691 result[attach['id']] = self._compute_checksum(attach)
694 def _compute_checksum(self, attachment_dict):
695 if attachment_dict.get('res_model') == 'ir.ui.view'\
696 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
697 and attachment_dict.get('type', 'binary') == 'binary'\
698 and attachment_dict.get('datas'):
699 return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
702 def _datas_big(self, cr, uid, ids, name, arg, context=None):
703 result = dict.fromkeys(ids, False)
704 if context and context.get('bin_size'):
707 for record in self.browse(cr, uid, ids, context=context):
708 if record.res_model != 'ir.ui.view' or not record.datas: continue
710 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
711 except IOError: # apparently the error PIL.Image.open raises
717 'datas_checksum': fields.function(_datas_checksum, size=40,
718 string="Datas checksum", type='char', store=True, select=True),
719 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
720 'datas_big': fields.function (_datas_big, type='binary', store=True,
721 string="Resized file content"),
722 'mimetype': fields.char('Mime Type', readonly=True),
725 def _add_mimetype_if_needed(self, values):
726 if values.get('datas_fname'):
727 values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
729 def create(self, cr, uid, values, context=None):
730 chk = self._compute_checksum(values)
732 match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
735 self._add_mimetype_if_needed(values)
736 return super(ir_attachment, self).create(
737 cr, uid, values, context=context)
739 def write(self, cr, uid, ids, values, context=None):
740 self._add_mimetype_if_needed(values)
741 return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
743 def try_remove(self, cr, uid, ids, context=None):
744 """ Removes a web-based image attachment if it is used by no view
747 Returns a dict mapping attachments which would not be removed (if any)
748 mapped to the views preventing their removal
750 Views = self.pool['ir.ui.view']
751 attachments_to_remove = []
752 # views blocking removal of the attachment
753 removal_blocked_by = {}
755 for attachment in self.browse(cr, uid, ids, context=context):
756 # in-document URLs are html-escaped, a straight search will not
758 url = escape(attachment.website_url)
759 ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
762 removal_blocked_by[attachment.id] = Views.read(
763 cr, uid, ids, ['name'], context=context)
765 attachments_to_remove.append(attachment.id)
766 if attachments_to_remove:
767 self.unlink(cr, uid, attachments_to_remove, context=context)
768 return removal_blocked_by
770 class res_partner(osv.osv):
771 _inherit = "res.partner"
773 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
774 partner = self.browse(cr, uid, ids[0], context=context)
776 '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 ''),
777 'size': "%sx%s" % (height, width),
781 return urlplus('//maps.googleapis.com/maps/api/staticmap' , params)
783 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
784 partner = self.browse(cr, uid, ids[0], context=context)
786 '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 ''),
789 return urlplus('https://maps.google.com/maps' , params)
791 class res_company(osv.osv):
792 _inherit = "res.company"
793 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
794 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
795 return partner and partner.google_map_img(zoom, width, height, context=context) or None
796 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
797 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
798 return partner and partner.google_map_link(zoom, context=context) or None
800 class base_language_install(osv.osv_memory):
801 _inherit = "base.language.install"
803 'website_ids': fields.many2many('website', string='Websites to translate'),
806 def default_get(self, cr, uid, fields, context=None):
809 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
810 website_id = context.get('params', {}).get('website_id')
812 if 'website_ids' not in defaults:
813 defaults['website_ids'] = []
814 defaults['website_ids'].append(website_id)
817 def lang_install(self, cr, uid, ids, context=None):
820 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
821 language_obj = self.browse(cr, uid, ids)[0]
822 website_ids = [website.id for website in language_obj['website_ids']]
823 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
824 if website_ids and lang_id:
825 data = {'language_ids': [(4, lang_id[0])]}
826 self.pool['website'].write(cr, uid, website_ids, data)
827 params = context.get('params', {})
828 if 'url_return' in params:
830 'url': params['url_return'].replace('[lang]', language_obj['lang']),
831 'type': 'ir.actions.act_url',
836 class website_seo_metadata(osv.Model):
837 _name = 'website.seo.metadata'
838 _description = 'SEO metadata'
841 'website_meta_title': fields.char("Website meta title", translate=True),
842 'website_meta_description': fields.text("Website meta description", translate=True),
843 'website_meta_keywords': fields.char("Website meta keywords", translate=True),