1 # -*- coding: utf-8 -*-
17 from sys import maxint
20 import werkzeug.exceptions
22 import werkzeug.wrappers
23 # optional python-slugify import (https://github.com/un33k/python-slugify)
25 import slugify as slugify_lib
30 from openerp.osv import orm, osv, fields
31 from openerp.tools import html_escape as escape
32 from openerp.tools import ustr as ustr
33 from openerp.tools.safe_eval import safe_eval
34 from openerp.addons.web.http import request
36 logger = logging.getLogger(__name__)
38 def url_for(path_or_uri, lang=None):
39 if isinstance(path_or_uri, unicode):
40 path_or_uri = path_or_uri.encode('utf-8')
41 current_path = request.httprequest.path
42 if isinstance(current_path, unicode):
43 current_path = current_path.encode('utf-8')
44 location = path_or_uri.strip()
45 force_lang = lang is not None
46 url = urlparse.urlparse(location)
48 if request and not url.netloc and not url.scheme and (url.path or force_lang):
49 location = urlparse.urljoin(current_path, location)
51 lang = lang or request.context.get('lang')
52 langs = [lg[0] for lg in request.website.get_languages()]
54 if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs):
55 ps = location.split('/')
57 # Replace the language only if we explicitly provide a language to url_for
60 # Remove the default language unless it's explicitly provided
61 elif ps[1] == request.website.default_lang_code:
63 # Insert the context language or the provided language
64 elif lang != request.website.default_lang_code or force_lang:
66 location = '/'.join(ps)
68 return location.decode('utf-8')
70 def is_multilang_url(local_url, langs=None):
72 langs = [lg[0] for lg in request.website.get_languages()]
73 spath = local_url.split('/')
74 # if a language is already in the path, remove it
77 local_url = '/'.join(spath)
79 # Try to match an endpoint in werkzeug's routing table
80 url = local_url.split('?')
82 query_string = url[1] if len(url) > 1 else None
83 router = request.httprequest.app.get_db_router(request.db).bind('')
84 func = router.match(path, query_args=query_string)[0]
85 return func.routing.get('website', False) and func.routing.get('multilang', True)
89 def slugify(s, max_length=None):
90 """ Transform a string to a slug that can be used in a url path.
92 This method will first try to do the job with python-slugify if present.
93 Otherwise it will process string by stripping leading and ending spaces,
94 converting unicode chars to ascii, lowering all chars and replacing spaces
95 and underscore with hyphen "-".
98 :param max_length: int
103 # There are 2 different libraries only python-slugify is supported
105 return slugify_lib.slugify(s, max_length=max_length)
108 uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
109 slug = re.sub('[\W_]', ' ', uni).strip().lower()
110 slug = re.sub('[-\s]+', '-', slug)
112 return slug[:max_length]
115 if isinstance(value, orm.browse_record):
116 # [(id, name)] = value.name_get()
117 id, name = value.id, value[value._rec_name]
119 # assume name_search result tuple
121 slugname = slugify(name or '').strip().strip('-')
124 return "%s-%d" % (slugname, id)
127 _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[a-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)', re.I)
130 """Extract slug and id from a string.
131 Always return un 2-tuple (str|None, int|None)
133 m = _UNSLUG_RE.match(s)
136 return m.group(1), int(m.group(2))
138 def urlplus(url, params):
139 return werkzeug.Href(url)(params or None)
141 class website(osv.osv):
142 def _get_menu_website(self, cr, uid, ids, context=None):
143 # IF a menu is changed, update all websites
144 return self.search(cr, uid, [], context=context)
146 def _get_menu(self, cr, uid, ids, name, arg, context=None):
147 root_domain = [('parent_id', '=', False)]
148 menus = self.pool.get('website.menu').search(cr, uid, root_domain, order='id', context=context)
149 menu = menus and menus[0] or False
150 return dict( map(lambda x: (x, menu), ids) )
152 _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
153 _description = "Website"
155 'name': fields.char('Domain'),
156 'company_id': fields.many2one('res.company', string="Company"),
157 'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
158 'default_lang_id': fields.many2one('res.lang', string="Default language"),
159 'default_lang_code': fields.related('default_lang_id', 'code', type="char", string="Default language code", store=True),
160 'social_twitter': fields.char('Twitter Account'),
161 'social_facebook': fields.char('Facebook Account'),
162 'social_github': fields.char('GitHub Account'),
163 'social_linkedin': fields.char('LinkedIn Account'),
164 'social_youtube': fields.char('Youtube Account'),
165 'social_googleplus': fields.char('Google+ Account'),
166 'google_analytics_key': fields.char('Google Analytics Key'),
167 'user_id': fields.many2one('res.users', string='Public User'),
168 'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'),
169 'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu',
171 'website.menu': (_get_menu_website, ['sequence','parent_id','website_id'], 10)
176 'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
179 # cf. Wizard hack in website_views.xml
180 def noop(self, *args, **kwargs):
183 def write(self, cr, uid, ids, vals, context=None):
184 self._get_languages.clear_cache(self)
185 return super(website, self).write(cr, uid, ids, vals, context)
187 def new_page(self, cr, uid, name, template='website.default_page', ispage=True, context=None):
188 context = context or {}
189 imd = self.pool.get('ir.model.data')
190 view = self.pool.get('ir.ui.view')
191 template_module, template_name = template.split('.')
193 # completely arbitrary max_length
194 page_name = slugify(name, max_length=50)
195 page_xmlid = "%s.%s" % (template_module, page_name)
199 imd.get_object_reference(cr, uid, template_module, page_name)
202 _, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
203 page_id = view.copy(cr, uid, template_id, context=context)
204 page = view.browse(cr, uid, page_id, context=context)
206 'arch': page.arch.replace(template, page_xmlid),
210 imd.create(cr, uid, {
212 'module': template_module,
213 'model': 'ir.ui.view',
219 def page_for_name(self, cr, uid, ids, name, module='website', context=None):
221 return '%s.%s' % (module, slugify(name, max_length=50))
223 def page_exists(self, cr, uid, ids, name, module='website', context=None):
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])
237 def get_current_website(self, cr, uid, context=None):
238 # TODO: Select website, currently hard coded
239 return self.pool['website'].browse(cr, uid, 1, context=context)
241 def is_publisher(self, cr, uid, ids, context=None):
242 Access = self.pool['ir.model.access']
243 is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context=context)
244 return is_website_publisher
246 def is_user(self, cr, uid, ids, context=None):
247 return self.pool['res.users'].has_group(cr, uid, 'base.group_user')
249 def get_template(self, cr, uid, ids, template, context=None):
250 if isinstance(template, (int, long)):
253 if '.' not in template:
254 template = 'website.%s' % template
255 module, xmlid = template.split('.', 1)
256 model, view_id = request.registry["ir.model.data"].get_object_reference(cr, uid, module, xmlid)
257 return self.pool["ir.ui.view"].browse(cr, uid, view_id, context=context)
259 def _render(self, cr, uid, ids, template, values=None, context=None):
260 # TODO: remove this. (just kept for backward api compatibility for saas-3)
261 return self.pool['ir.ui.view'].render(cr, uid, template, values=values, context=context)
263 def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
264 # TODO: remove this. (just kept for backward api compatibility for saas-3)
265 return request.render(template, values, uid=uid)
267 def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
269 page_count = int(math.ceil(float(total) / step))
271 page = max(1, min(int(page), page_count))
274 pmin = max(page - int(math.floor(scope/2)), 1)
275 pmax = min(pmin + scope, page_count)
277 if pmax - pmin < scope:
278 pmin = pmax - scope if pmax - scope > 0 else 1
281 _url = "%s/page/%s" % (url, page) if page > 1 else url
283 _url = "%s?%s" % (_url, werkzeug.url_encode(url_args))
287 "page_count": page_count,
288 "offset": (page - 1) * step,
290 'url': get_url(page),
294 'url': get_url(pmin),
298 'url': get_url(max(pmin, page - 1)),
299 'num': max(pmin, page - 1)
302 'url': get_url(min(pmax, page + 1)),
303 'num': min(pmax, page + 1)
306 'url': get_url(pmax),
310 {'url': get_url(page), 'num': page}
311 for page in xrange(pmin, pmax+1)
315 def rule_is_enumerable(self, rule):
316 """ Checks that it is possible to generate sensible GET queries for
317 a given rule (if the endpoint matches its own requirements)
319 :type rule: werkzeug.routing.Rule
322 endpoint = rule.endpoint
323 methods = rule.methods or ['GET']
324 converters = rule._converters.values()
325 if not ('GET' in methods
326 and endpoint.routing['type'] == 'http'
327 and endpoint.routing['auth'] in ('none', 'public')
328 and endpoint.routing.get('website', False)
329 and all(hasattr(converter, 'generate') for converter in converters)
330 and endpoint.routing.get('website')):
333 # dont't list routes without argument having no default value or converter
334 spec = inspect.getargspec(endpoint.method.original_func)
336 # remove self and arguments having a default value
337 defaults_count = len(spec.defaults or [])
338 args = spec.args[1:(-defaults_count or None)]
340 # check that all args have a converter
341 return all( (arg in rule._converters) for arg in args)
343 def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
344 """ Available pages in the website/CMS. This is mostly used for links
345 generation and can be overridden by modules setting up new HTML
346 controllers for dynamic pages (e.g. blog).
348 By default, returns template views marked as pages.
350 :param str query_string: a (user-provided) string, fetches pages
352 :returns: a list of mappings with two keys: ``name`` is the displayable
353 name of the resource (page), ``url`` is the absolute URL
355 :rtype: list({name: str, url: str})
357 router = request.httprequest.app.get_db_router(request.db)
358 # Force enumeration to be performed as public user
359 uid = request.website.user_id.id
361 for rule in router.iter_rules():
362 if not self.rule_is_enumerable(rule):
365 converters = rule._converters or {}
367 convitems = converters.items()
368 # converters with a domain are processed after the other ones
369 gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain <> '[]')
370 convitems.sort(lambda x, y: cmp(gd(x), gd(y)))
371 for (name, converter) in convitems:
374 for v in converter.generate(request.cr, uid, query=query_string, args=val, context=context):
375 newval.append( val.copy() )
382 domain_part, url = rule.build(value, append_unknown=False)
384 for key,val in value.items():
385 if key.startswith('__'):
387 if url in ('/sitemap.xml',):
392 if query_string and not self.page_matches(cr, uid, page, query_string, context=context):
396 def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
397 return list(itertools.islice(
398 self.enumerate_pages(cr, uid, ids, query_string=needle, context=context),
401 def page_matches(self, cr, uid, page, needle, context=None):
402 """ Checks that a "page" matches a user-provide search string.
404 The default implementation attempts to perform a non-contiguous
405 substring match of the page's name.
407 :param page: {'name': str, 'url': str}
411 haystack = page['name'].lower()
413 needle = iter(needle.lower())
417 for char in haystack:
418 if char != n: continue
420 n = next(needle, end)
421 # found all characters of needle in haystack in order
427 def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
428 step = step and int(step) or 10
429 scope = scope and int(scope) or 5
430 orderby = orderby or "name"
432 get_args = dict(request.httprequest.args or {})
433 model_obj = self.pool[model]
434 relation = model_obj._columns.get(column)._obj
435 relation_obj = self.pool[relation]
437 get_args.setdefault('kanban', "")
438 kanban = get_args.pop('kanban')
439 kanban_url = "?%s&kanban=" % werkzeug.url_encode(get_args)
442 for col in kanban.split(","):
445 pages[int(col[0])] = int(col[1])
448 for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
452 relation_id = group[column][0]
453 obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
455 obj['kanban_url'] = kanban_url
456 for k, v in pages.items():
458 obj['kanban_url'] += "%s-%s" % (k, v)
461 number = model_obj.search(cr, uid, group['__domain'], count=True)
462 obj['page_count'] = int(math.ceil(float(number) / step))
463 obj['page'] = pages.get(relation_id) or 1
464 if obj['page'] > obj['page_count']:
465 obj['page'] = obj['page_count']
466 offset = (obj['page']-1) * step
467 obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
468 obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
471 obj['domain'] = group['__domain']
474 obj['orderby'] = orderby
477 object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
478 obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
485 'template': template,
487 return request.website._render("website.kanban_contain", values)
489 def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
491 model_obj = self.pool[model]
492 domain = safe_eval(domain)
494 offset = (int(page)-1) * step
495 object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
496 object_ids = model_obj.browse(cr, uid, object_ids)
497 for object_id in object_ids:
498 html += request.website._render(template, {'object_id': object_id})
501 def _image_placeholder(self, response):
502 # file_open may return a StringIO. StringIO can be closed but are
503 # not context managers in Python 2 though that is fixed in 3
504 with contextlib.closing(openerp.tools.misc.file_open(
505 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
507 response.data = f.read()
508 return response.make_conditional(request.httprequest)
510 def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_height=maxint, context=None):
511 """ Fetches the requested field and ensures it does not go above
512 (max_width, max_height), resizing it if necessary.
514 Resizing is bypassed if the object provides a $field_big, which will
515 be interpreted as a pre-resized version of the base field.
517 If the record is not found or does not have the requested field,
518 returns a placeholder image via :meth:`~._image_placeholder`.
520 Sets and checks conditional response parameters:
521 * :mailheader:`ETag` is always set (and checked)
522 * :mailheader:`Last-Modified is set iif the record has a concurrency
523 field (``__last_update``)
525 The requested field is assumed to be base64-encoded image data in
528 Model = self.pool[model]
531 ids = Model.search(cr, uid,
532 [('id', '=', id)], context=context)
533 if not ids and 'website_published' in Model._all_columns:
534 ids = Model.search(cr, openerp.SUPERUSER_ID,
535 [('id', '=', id), ('website_published', '=', True)], context=context)
537 return self._image_placeholder(response)
539 concurrency = '__last_update'
540 [record] = Model.read(cr, openerp.SUPERUSER_ID, [id],
541 [concurrency, field],
544 if concurrency in record:
545 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
547 response.last_modified = datetime.datetime.strptime(
548 record[concurrency], server_format + '.%f')
550 # just in case we have a timestamp without microseconds
551 response.last_modified = datetime.datetime.strptime(
552 record[concurrency], server_format)
554 # Field does not exist on model or field set to False
555 if not record.get(field):
556 # FIXME: maybe a field which does not exist should be a 404?
557 return self._image_placeholder(response)
559 response.set_etag(hashlib.sha1(record[field]).hexdigest())
560 response.make_conditional(request.httprequest)
562 # conditional request match
563 if response.status_code == 304:
566 data = record[field].decode('base64')
568 if (not max_width) and (not max_height):
572 image = Image.open(cStringIO.StringIO(data))
573 response.mimetype = Image.MIME[image.format]
577 max_w, max_h = int(max_width), int(max_height)
579 max_w, max_h = (maxint, maxint)
581 if w < max_w and h < max_h:
584 image.thumbnail((max_w, max_h), Image.ANTIALIAS)
585 image.save(response.stream, image.format)
586 # invalidate content-length computed by make_conditional as
587 # writing to response.stream does not do it (as of werkzeug 0.9.3)
588 del response.headers['Content-Length']
593 class website_menu(osv.osv):
594 _name = "website.menu"
595 _description = "Website Menu"
597 'name': fields.char('Menu', required=True, translate=True),
598 'url': fields.char('Url', translate=True),
599 'new_window': fields.boolean('New Window'),
600 'sequence': fields.integer('Sequence'),
601 # TODO: support multiwebsite once done for ir.ui.views
602 'website_id': fields.many2one('website', 'Website'),
603 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
604 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
605 'parent_left': fields.integer('Parent Left', select=True),
606 'parent_right': fields.integer('Parent Right', select=True),
609 def __defaults_sequence(self, cr, uid, context):
610 menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
611 return menu and menu[0]["sequence"] or 0
615 'sequence': __defaults_sequence,
619 _parent_order = 'sequence'
622 # would be better to take a menu_id as argument
623 def get_tree(self, cr, uid, website_id, context=None):
629 new_window=node.new_window,
630 sequence=node.sequence,
631 parent_id=node.parent_id.id,
634 for child in node.child_id:
635 menu_node['children'].append(make_tree(child))
637 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
638 return make_tree(menu)
640 def save(self, cr, uid, website_id, data, context=None):
641 def replace_id(old_id, new_id):
642 for menu in data['data']:
643 if menu['id'] == old_id:
645 if menu['parent_id'] == old_id:
646 menu['parent_id'] = new_id
647 to_delete = data['to_delete']
649 self.unlink(cr, uid, to_delete, context=context)
650 for menu in data['data']:
652 if isinstance(mid, str):
653 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
654 replace_id(mid, new_id)
655 for menu in data['data']:
656 self.write(cr, uid, [menu['id']], menu, context=context)
659 class ir_attachment(osv.osv):
660 _inherit = "ir.attachment"
661 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
663 for attach in self.browse(cr, uid, ids, context=context):
665 result[attach.id] = attach.url
667 result[attach.id] = urlplus('/website/image', {
668 'model': 'ir.attachment',
673 def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
675 (attach['id'], self._compute_checksum(attach))
676 for attach in self.read(
677 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
681 def _compute_checksum(self, attachment_dict):
682 if attachment_dict.get('res_model') == 'ir.ui.view'\
683 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
684 and attachment_dict.get('type', 'binary') == 'binary'\
685 and attachment_dict.get('datas'):
686 return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
689 def _datas_big(self, cr, uid, ids, name, arg, context=None):
690 result = dict.fromkeys(ids, False)
691 if context and context.get('bin_size'):
694 for record in self.browse(cr, uid, ids, context=context):
695 if not record.datas: continue
697 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
698 except IOError: # apparently the error PIL.Image.open raises
704 'datas_checksum': fields.function(_datas_checksum, size=40,
705 string="Datas checksum", type='char', store=True, select=True),
706 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
707 'datas_big': fields.function (_datas_big, type='binary', store=True,
708 string="Resized file content"),
709 'mimetype': fields.char('Mime Type', readonly=True),
712 def _add_mimetype_if_needed(self, values):
713 if values.get('datas_fname'):
714 values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
716 def create(self, cr, uid, values, context=None):
717 chk = self._compute_checksum(values)
719 match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
722 self._add_mimetype_if_needed(values)
723 return super(ir_attachment, self).create(
724 cr, uid, values, context=context)
726 def write(self, cr, uid, ids, values, context=None):
727 self._add_mimetype_if_needed(values)
728 return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
730 def try_remove(self, cr, uid, ids, context=None):
731 """ Removes a web-based image attachment if it is used by no view
734 Returns a dict mapping attachments which would not be removed (if any)
735 mapped to the views preventing their removal
737 Views = self.pool['ir.ui.view']
738 attachments_to_remove = []
739 # views blocking removal of the attachment
740 removal_blocked_by = {}
742 for attachment in self.browse(cr, uid, ids, context=context):
743 # in-document URLs are html-escaped, a straight search will not
745 url = escape(attachment.website_url)
746 ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
749 removal_blocked_by[attachment.id] = Views.read(
750 cr, uid, ids, ['name'], context=context)
752 attachments_to_remove.append(attachment.id)
753 if attachments_to_remove:
754 self.unlink(cr, uid, attachments_to_remove, context=context)
755 return removal_blocked_by
757 class res_partner(osv.osv):
758 _inherit = "res.partner"
760 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
761 partner = self.browse(cr, uid, ids[0], context=context)
763 '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 ''),
764 'size': "%sx%s" % (height, width),
768 return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
770 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
771 partner = self.browse(cr, uid, ids[0], context=context)
773 '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 ''),
776 return urlplus('https://maps.google.com/maps' , params)
778 class res_company(osv.osv):
779 _inherit = "res.company"
780 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
781 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
782 return partner and partner.google_map_img(zoom, width, height, context=context) or None
783 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
784 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
785 return partner and partner.google_map_link(zoom, context=context) or None
787 class base_language_install(osv.osv_memory):
788 _inherit = "base.language.install"
790 'website_ids': fields.many2many('website', string='Websites to translate'),
793 def default_get(self, cr, uid, fields, context=None):
796 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
797 website_id = context.get('params', {}).get('website_id')
799 if 'website_ids' not in defaults:
800 defaults['website_ids'] = []
801 defaults['website_ids'].append(website_id)
804 def lang_install(self, cr, uid, ids, context=None):
807 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
808 language_obj = self.browse(cr, uid, ids)[0]
809 website_ids = [website.id for website in language_obj['website_ids']]
810 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
811 if website_ids and lang_id:
812 data = {'language_ids': [(4, lang_id[0])]}
813 self.pool['website'].write(cr, uid, website_ids, data)
814 params = context.get('params', {})
815 if 'url_return' in params:
817 'url': params['url_return'].replace('[lang]', language_obj['lang']),
818 'type': 'ir.actions.act_url',
823 class website_seo_metadata(osv.Model):
824 _name = 'website.seo.metadata'
825 _description = 'SEO metadata'
828 'website_meta_title': fields.char("Website meta title", translate=True),
829 'website_meta_description': fields.text("Website meta description", translate=True),
830 'website_meta_keywords': fields.char("Website meta keywords", translate=True),