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
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 image_format = image.format
592 size = (max_w, max_h)
593 image = image_resize_and_sharpen(image, size)
595 if image_format == 'PNG':
596 image.convert('P').save(response.stream, optimize=True, format='PNG')
597 elif image.format == 'JPEG':
598 image.save(response.stream, quality=80, optimize=True, format='JPG')
600 image.save(response.stream, format=image_format)
601 # invalidate content-length computed by make_conditional as
602 # writing to response.stream does not do it (as of werkzeug 0.9.3)
603 del response.headers['Content-Length']
607 def image_url(self, cr, uid, record, field, size=None, context=None):
608 """Returns a local url that points to the image field of a given browse record."""
610 id = '%s_%s' % (record.id, hashlib.sha1(record.write_date).hexdigest()[0:7])
611 size = '' if size is None else '-%s' % size
612 return '/website/image/%s-%s-%s%s' % (model, id, field, size)
615 class website_menu(osv.osv):
616 _name = "website.menu"
617 _description = "Website Menu"
619 'name': fields.char('Menu', required=True, translate=True),
620 'url': fields.char('Url'),
621 'new_window': fields.boolean('New Window'),
622 'sequence': fields.integer('Sequence'),
623 # TODO: support multiwebsite once done for ir.ui.views
624 'website_id': fields.many2one('website', 'Website'),
625 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
626 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
627 'parent_left': fields.integer('Parent Left', select=True),
628 'parent_right': fields.integer('Parent Right', select=True),
631 def __defaults_sequence(self, cr, uid, context):
632 menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
633 return menu and menu[0]["sequence"] or 0
637 'sequence': __defaults_sequence,
641 _parent_order = 'sequence'
644 # would be better to take a menu_id as argument
645 def get_tree(self, cr, uid, website_id, context=None):
651 new_window=node.new_window,
652 sequence=node.sequence,
653 parent_id=node.parent_id.id,
656 for child in node.child_id:
657 menu_node['children'].append(make_tree(child))
659 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
660 return make_tree(menu)
662 def save(self, cr, uid, website_id, data, context=None):
663 def replace_id(old_id, new_id):
664 for menu in data['data']:
665 if menu['id'] == old_id:
667 if menu['parent_id'] == old_id:
668 menu['parent_id'] = new_id
669 to_delete = data['to_delete']
671 self.unlink(cr, uid, to_delete, context=context)
672 for menu in data['data']:
674 if isinstance(mid, str):
675 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
676 replace_id(mid, new_id)
677 for menu in data['data']:
678 self.write(cr, uid, [menu['id']], menu, context=context)
681 class ir_attachment(osv.osv):
682 _inherit = "ir.attachment"
683 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
685 for attach in self.browse(cr, uid, ids, context=context):
687 result[attach.id] = attach.url
689 result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
691 def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
693 (attach['id'], self._compute_checksum(attach))
694 for attach in self.read(
695 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
699 def _compute_checksum(self, attachment_dict):
700 if attachment_dict.get('res_model') == 'ir.ui.view'\
701 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
702 and attachment_dict.get('type', 'binary') == 'binary'\
703 and attachment_dict.get('datas'):
704 return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
707 def _datas_big(self, cr, uid, ids, name, arg, context=None):
708 result = dict.fromkeys(ids, False)
709 if context and context.get('bin_size'):
712 for record in self.browse(cr, uid, ids, context=context):
713 if not record.datas: continue
715 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
716 except IOError: # apparently the error PIL.Image.open raises
722 'datas_checksum': fields.function(_datas_checksum, size=40,
723 string="Datas checksum", type='char', store=True, select=True),
724 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
725 'datas_big': fields.function (_datas_big, type='binary', store=True,
726 string="Resized file content"),
727 'mimetype': fields.char('Mime Type', readonly=True),
730 def _add_mimetype_if_needed(self, values):
731 if values.get('datas_fname'):
732 values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
734 def create(self, cr, uid, values, context=None):
735 chk = self._compute_checksum(values)
737 match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
740 self._add_mimetype_if_needed(values)
741 return super(ir_attachment, self).create(
742 cr, uid, values, context=context)
744 def write(self, cr, uid, ids, values, context=None):
745 self._add_mimetype_if_needed(values)
746 return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
748 def try_remove(self, cr, uid, ids, context=None):
749 """ Removes a web-based image attachment if it is used by no view
752 Returns a dict mapping attachments which would not be removed (if any)
753 mapped to the views preventing their removal
755 Views = self.pool['ir.ui.view']
756 attachments_to_remove = []
757 # views blocking removal of the attachment
758 removal_blocked_by = {}
760 for attachment in self.browse(cr, uid, ids, context=context):
761 # in-document URLs are html-escaped, a straight search will not
763 url = escape(attachment.website_url)
764 ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
767 removal_blocked_by[attachment.id] = Views.read(
768 cr, uid, ids, ['name'], context=context)
770 attachments_to_remove.append(attachment.id)
771 if attachments_to_remove:
772 self.unlink(cr, uid, attachments_to_remove, context=context)
773 return removal_blocked_by
775 class res_partner(osv.osv):
776 _inherit = "res.partner"
778 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
779 partner = self.browse(cr, uid, ids[0], context=context)
781 '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 ''),
782 'size': "%sx%s" % (height, width),
786 return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
788 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
789 partner = self.browse(cr, uid, ids[0], context=context)
791 '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 ''),
794 return urlplus('https://maps.google.com/maps' , params)
796 class res_company(osv.osv):
797 _inherit = "res.company"
798 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
799 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
800 return partner and partner.google_map_img(zoom, width, height, context=context) or None
801 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
802 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
803 return partner and partner.google_map_link(zoom, context=context) or None
805 class base_language_install(osv.osv_memory):
806 _inherit = "base.language.install"
808 'website_ids': fields.many2many('website', string='Websites to translate'),
811 def default_get(self, cr, uid, fields, context=None):
814 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
815 website_id = context.get('params', {}).get('website_id')
817 if 'website_ids' not in defaults:
818 defaults['website_ids'] = []
819 defaults['website_ids'].append(website_id)
822 def lang_install(self, cr, uid, ids, context=None):
825 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
826 language_obj = self.browse(cr, uid, ids)[0]
827 website_ids = [website.id for website in language_obj['website_ids']]
828 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
829 if website_ids and lang_id:
830 data = {'language_ids': [(4, lang_id[0])]}
831 self.pool['website'].write(cr, uid, website_ids, data)
832 params = context.get('params', {})
833 if 'url_return' in params:
835 'url': params['url_return'].replace('[lang]', language_obj['lang']),
836 'type': 'ir.actions.act_url',
841 class website_seo_metadata(osv.Model):
842 _name = 'website.seo.metadata'
843 _description = 'SEO metadata'
846 'website_meta_title': fields.char("Website meta title", translate=True),
847 'website_meta_description': fields.text("Website meta description", translate=True),
848 'website_meta_keywords': fields.char("Website meta keywords", translate=True),