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+)(?=$|/)')
127 DEFAULT_CDN_FILTERS = [
134 """Extract slug and id from a string.
135 Always return un 2-tuple (str|None, int|None)
137 m = _UNSLUG_RE.match(s)
140 return m.group(1), int(m.group(2))
142 def urlplus(url, params):
143 return werkzeug.Href(url)(params or None)
145 class website(osv.osv):
146 def _get_menu(self, cr, uid, ids, name, arg, context=None):
148 menu_obj = self.pool.get('website.menu')
150 menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False), ('website_id', '=', id)], order='id', context=context)
151 res[id] = menu_ids and menu_ids[0] or False
154 _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
155 _description = "Website"
157 'name': fields.char('Website Name'),
158 'domain': fields.char('Website Domain'),
159 'company_id': fields.many2one('res.company', string="Company"),
160 'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
161 'default_lang_id': fields.many2one('res.lang', string="Default language"),
162 'default_lang_code': fields.related('default_lang_id', 'code', type="char", string="Default language code", store=True),
163 'social_twitter': fields.char('Twitter Account'),
164 'social_facebook': fields.char('Facebook Account'),
165 'social_github': fields.char('GitHub Account'),
166 'social_linkedin': fields.char('LinkedIn Account'),
167 'social_youtube': fields.char('Youtube Account'),
168 'social_googleplus': fields.char('Google+ Account'),
169 'google_analytics_key': fields.char('Google Analytics Key'),
170 'user_id': fields.many2one('res.users', string='Public User'),
171 'compress_html': fields.boolean('Compress HTML'),
172 'cdn_activated': fields.boolean('Activate CDN for assets'),
173 'cdn_url': fields.char('CDN Base URL'),
174 'cdn_filters': fields.text('CDN Filters', help="URL matching those filters will be rewritten using the CDN Base URL"),
175 'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'),
176 'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu')
179 'user_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
180 'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID,'base.main_company'),
181 'compress_html': False,
182 'cdn_activated': False,
183 'cdn_url': '//localhost:8069/',
184 'cdn_filters': '\n'.join(DEFAULT_CDN_FILTERS),
187 # cf. Wizard hack in website_views.xml
188 def noop(self, *args, **kwargs):
191 def write(self, cr, uid, ids, vals, context=None):
192 self._get_languages.clear_cache(self)
193 return super(website, self).write(cr, uid, ids, vals, context)
195 def new_page(self, cr, uid, name, template='website.default_page', ispage=True, context=None):
196 context = context or {}
197 imd = self.pool.get('ir.model.data')
198 view = self.pool.get('ir.ui.view')
199 template_module, template_name = template.split('.')
201 # completely arbitrary max_length
202 page_name = slugify(name, max_length=50)
203 page_xmlid = "%s.%s" % (template_module, page_name)
207 imd.get_object_reference(cr, uid, template_module, page_name)
210 _, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
211 website_id = context.get('website_id')
212 key = template_module+'.'+page_name
213 page_id = view.copy(cr, uid, template_id, {'website_id': website_id, 'key': key}, context=context)
214 page = view.browse(cr, uid, page_id, context=context)
216 'arch': page.arch.replace(template, page_xmlid),
222 def page_for_name(self, cr, uid, ids, name, module='website', context=None):
224 return '%s.%s' % (module, slugify(name, max_length=50))
226 def page_exists(self, cr, uid, ids, name, module='website', context=None):
228 name = (name or "").replace("/page/website.", "").replace("/page/", "")
231 return self.pool["ir.model.data"].get_object_reference(cr, uid, module, name)
235 @openerp.tools.ormcache(skiparg=3)
236 def _get_languages(self, cr, uid, id, context=None):
237 website = self.browse(cr, uid, id)
238 return [(lg.code, lg.name) for lg in website.language_ids]
240 def get_cdn_url(self, cr, uid, uri, context=None):
241 # Currently only usable in a website_enable request context
242 if request and request.website and not request.debug:
243 cdn_url = request.website.cdn_url
244 cdn_filters = (request.website.cdn_filters or '').splitlines()
245 for flt in cdn_filters:
246 if flt and re.match(flt, uri):
247 return urlparse.urljoin(cdn_url, uri)
250 def get_languages(self, cr, uid, ids, context=None):
251 return self._get_languages(cr, uid, ids[0], context=context)
253 def get_alternate_languages(self, cr, uid, ids, req=None, context=None):
256 req = request.httprequest
257 default = self.get_current_website(cr, uid, context=context).default_lang_code
260 uri += '?' + req.query_string
262 for code, name in self.get_languages(cr, uid, ids, context=context):
263 lg_path = ('/' + code) if code != default else ''
267 'hreflang': ('-'.join(lg)).lower(),
269 'href': req.url_root[0:-1] + lg_path + uri,
273 if shorts.count(lang['short']) == 1:
274 lang['hreflang'] = lang['short']
277 @openerp.tools.ormcache(skiparg=4)
278 def _get_current_website_id(self, cr, uid, domain_name, context=None):
281 ids = self.search(cr, uid, [('domain', '=', domain_name)], context=context)
286 def get_current_website(self, cr, uid, context=None):
287 domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0]
288 website_id = self._get_current_website_id(cr, uid, domain_name, context=context)
289 return self.browse(cr, uid, website_id, context=context)
291 def is_publisher(self, cr, uid, ids, context=None):
292 Access = self.pool['ir.model.access']
293 is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context=context)
294 return is_website_publisher
296 def is_user(self, cr, uid, ids, context=None):
297 Access = self.pool['ir.model.access']
298 return Access.check(cr, uid, 'ir.ui.menu', 'read', False, context=context)
300 def get_template(self, cr, uid, ids, template, context=None):
301 if not isinstance(template, (int, long)) and '.' not in template:
302 template = 'website.%s' % template
303 View = self.pool['ir.ui.view']
304 view_id = View.get_view_id(cr, uid, template, context=context)
307 return View.browse(cr, uid, view_id, context=context)
309 def _render(self, cr, uid, ids, template, values=None, context=None):
310 # TODO: remove this. (just kept for backward api compatibility for saas-3)
311 return self.pool['ir.ui.view'].render(cr, uid, template, values=values, context=context)
313 def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
314 # TODO: remove this. (just kept for backward api compatibility for saas-3)
315 return request.render(template, values, uid=uid)
317 def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
319 page_count = int(math.ceil(float(total) / step))
321 page = max(1, min(int(page if str(page).isdigit() else 1), page_count))
324 pmin = max(page - int(math.floor(scope/2)), 1)
325 pmax = min(pmin + scope, page_count)
327 if pmax - pmin < scope:
328 pmin = pmax - scope if pmax - scope > 0 else 1
331 _url = "%s/page/%s" % (url, page) if page > 1 else url
333 _url = "%s?%s" % (_url, werkzeug.url_encode(url_args))
337 "page_count": page_count,
338 "offset": (page - 1) * step,
340 'url': get_url(page),
344 'url': get_url(pmin),
348 'url': get_url(max(pmin, page - 1)),
349 'num': max(pmin, page - 1)
352 'url': get_url(min(pmax, page + 1)),
353 'num': min(pmax, page + 1)
356 'url': get_url(pmax),
360 {'url': get_url(page), 'num': page}
361 for page in xrange(pmin, pmax+1)
365 def rule_is_enumerable(self, rule):
366 """ Checks that it is possible to generate sensible GET queries for
367 a given rule (if the endpoint matches its own requirements)
369 :type rule: werkzeug.routing.Rule
372 endpoint = rule.endpoint
373 methods = rule.methods or ['GET']
374 converters = rule._converters.values()
375 if not ('GET' in methods
376 and endpoint.routing['type'] == 'http'
377 and endpoint.routing['auth'] in ('none', 'public')
378 and endpoint.routing.get('website', False)
379 and all(hasattr(converter, 'generate') for converter in converters)
380 and endpoint.routing.get('website')):
383 # dont't list routes without argument having no default value or converter
384 spec = inspect.getargspec(endpoint.method.original_func)
386 # remove self and arguments having a default value
387 defaults_count = len(spec.defaults or [])
388 args = spec.args[1:(-defaults_count or None)]
390 # check that all args have a converter
391 return all( (arg in rule._converters) for arg in args)
393 def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
394 """ Available pages in the website/CMS. This is mostly used for links
395 generation and can be overridden by modules setting up new HTML
396 controllers for dynamic pages (e.g. blog).
398 By default, returns template views marked as pages.
400 :param str query_string: a (user-provided) string, fetches pages
402 :returns: a list of mappings with two keys: ``name`` is the displayable
403 name of the resource (page), ``url`` is the absolute URL
405 :rtype: list({name: str, url: str})
407 router = request.httprequest.app.get_db_router(request.db)
408 # Force enumeration to be performed as public user
410 for rule in router.iter_rules():
411 if not self.rule_is_enumerable(rule):
414 converters = rule._converters or {}
415 if query_string and not converters and (query_string not in rule.build([{}], append_unknown=False)[1]):
418 convitems = converters.items()
419 # converters with a domain are processed after the other ones
420 gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain <> '[]')
421 convitems.sort(lambda x, y: cmp(gd(x), gd(y)))
422 for (i,(name, converter)) in enumerate(convitems):
425 query = i==(len(convitems)-1) and query_string
426 for v in converter.generate(request.cr, uid, query=query, args=val, context=context):
427 newval.append( val.copy() )
434 domain_part, url = rule.build(value, append_unknown=False)
436 for key,val in value.items():
437 if key.startswith('__'):
439 if url in ('/sitemap.xml',):
447 def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
448 name = (needle or "").replace("/page/website.", "").replace("/page/", "")
450 for page in self.enumerate_pages(cr, uid, ids, query_string=name, context=context):
451 if needle in page['loc']:
453 if len(res) == limit:
457 def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
458 step = step and int(step) or 10
459 scope = scope and int(scope) or 5
460 orderby = orderby or "name"
462 get_args = dict(request.httprequest.args or {})
463 model_obj = self.pool[model]
464 relation = model_obj._columns.get(column)._obj
465 relation_obj = self.pool[relation]
467 get_args.setdefault('kanban', "")
468 kanban = get_args.pop('kanban')
469 kanban_url = "?%s&kanban=" % werkzeug.url_encode(get_args)
472 for col in kanban.split(","):
475 pages[int(col[0])] = int(col[1])
478 for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
482 relation_id = group[column][0]
483 obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
485 obj['kanban_url'] = kanban_url
486 for k, v in pages.items():
488 obj['kanban_url'] += "%s-%s" % (k, v)
491 number = model_obj.search(cr, uid, group['__domain'], count=True)
492 obj['page_count'] = int(math.ceil(float(number) / step))
493 obj['page'] = pages.get(relation_id) or 1
494 if obj['page'] > obj['page_count']:
495 obj['page'] = obj['page_count']
496 offset = (obj['page']-1) * step
497 obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
498 obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
501 obj['domain'] = group['__domain']
504 obj['orderby'] = orderby
507 object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
508 obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
515 'template': template,
517 return request.website._render("website.kanban_contain", values)
519 def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
521 model_obj = self.pool[model]
522 domain = safe_eval(domain)
524 offset = (int(page)-1) * step
525 object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
526 object_ids = model_obj.browse(cr, uid, object_ids)
527 for object_id in object_ids:
528 html += request.website._render(template, {'object_id': object_id})
531 def _image_placeholder(self, response):
532 # file_open may return a StringIO. StringIO can be closed but are
533 # not context managers in Python 2 though that is fixed in 3
534 with contextlib.closing(openerp.tools.misc.file_open(
535 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
537 response.data = f.read()
538 return response.make_conditional(request.httprequest)
540 def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_height=maxint, cache=None, context=None):
541 """ Fetches the requested field and ensures it does not go above
542 (max_width, max_height), resizing it if necessary.
544 Resizing is bypassed if the object provides a $field_big, which will
545 be interpreted as a pre-resized version of the base field.
547 If the record is not found or does not have the requested field,
548 returns a placeholder image via :meth:`~._image_placeholder`.
550 Sets and checks conditional response parameters:
551 * :mailheader:`ETag` is always set (and checked)
552 * :mailheader:`Last-Modified is set iif the record has a concurrency
553 field (``__last_update``)
555 The requested field is assumed to be base64-encoded image data in
558 Model = self.pool[model]
561 ids = Model.search(cr, uid,
562 [('id', '=', id)], context=context)
563 if not ids and 'website_published' in Model._fields:
564 ids = Model.search(cr, openerp.SUPERUSER_ID,
565 [('id', '=', id), ('website_published', '=', True)], context=context)
567 return self._image_placeholder(response)
569 concurrency = '__last_update'
570 [record] = Model.read(cr, openerp.SUPERUSER_ID, [id],
571 [concurrency, field],
574 if concurrency in record:
575 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
577 response.last_modified = datetime.datetime.strptime(
578 record[concurrency], server_format + '.%f')
580 # just in case we have a timestamp without microseconds
581 response.last_modified = datetime.datetime.strptime(
582 record[concurrency], server_format)
584 # Field does not exist on model or field set to False
585 if not record.get(field):
586 # FIXME: maybe a field which does not exist should be a 404?
587 return self._image_placeholder(response)
589 response.set_etag(hashlib.sha1(record[field]).hexdigest())
590 response.make_conditional(request.httprequest)
593 response.cache_control.max_age = cache
594 response.expires = int(time.time() + cache)
596 # conditional request match
597 if response.status_code == 304:
600 data = record[field].decode('base64')
601 image = Image.open(cStringIO.StringIO(data))
602 response.mimetype = Image.MIME[image.format]
604 filename = '%s_%s.%s' % (model.replace('.', '_'), id, str(image.format).lower())
605 response.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
607 if (not max_width) and (not max_height):
612 max_w = int(max_width) if max_width else maxint
613 max_h = int(max_height) if max_height else maxint
615 if w < max_w and h < max_h:
618 size = (max_w, max_h)
619 img = image_resize_and_sharpen(image, size, preserve_aspect_ratio=True)
620 image_save_for_web(img, response.stream, format=image.format)
621 # invalidate content-length computed by make_conditional as
622 # writing to response.stream does not do it (as of werkzeug 0.9.3)
623 del response.headers['Content-Length']
627 def image_url(self, cr, uid, record, field, size=None, context=None):
628 """Returns a local url that points to the image field of a given browse record."""
630 id = '%s_%s' % (record.id, hashlib.sha1(record.sudo().write_date).hexdigest()[0:7])
631 size = '' if size is None else '/%s' % size
632 return '/website/image/%s/%s/%s%s' % (model, id, field, size)
635 class website_menu(osv.osv):
636 _name = "website.menu"
637 _description = "Website Menu"
639 'name': fields.char('Menu', required=True, translate=True),
640 'url': fields.char('Url'),
641 'new_window': fields.boolean('New Window'),
642 'sequence': fields.integer('Sequence'),
643 # TODO: support multiwebsite once done for ir.ui.views
644 'website_id': fields.many2one('website', 'Website'),
645 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
646 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
647 'parent_left': fields.integer('Parent Left', select=True),
648 'parent_right': fields.integer('Parent Right', select=True),
651 def __defaults_sequence(self, cr, uid, context):
652 menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
653 return menu and menu[0]["sequence"] or 0
657 'sequence': __defaults_sequence,
661 _parent_order = 'sequence'
664 # would be better to take a menu_id as argument
665 def get_tree(self, cr, uid, website_id, context=None):
671 new_window=node.new_window,
672 sequence=node.sequence,
673 parent_id=node.parent_id.id,
676 for child in node.child_id:
677 menu_node['children'].append(make_tree(child))
679 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
680 return make_tree(menu)
682 def save(self, cr, uid, website_id, data, context=None):
683 def replace_id(old_id, new_id):
684 for menu in data['data']:
685 if menu['id'] == old_id:
687 if menu['parent_id'] == old_id:
688 menu['parent_id'] = new_id
689 to_delete = data['to_delete']
691 self.unlink(cr, uid, to_delete, context=context)
692 for menu in data['data']:
694 if isinstance(mid, str):
695 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
696 replace_id(mid, new_id)
697 for menu in data['data']:
698 self.write(cr, uid, [menu['id']], menu, context=context)
701 class ir_attachment(osv.osv):
702 _inherit = "ir.attachment"
703 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
705 for attach in self.browse(cr, uid, ids, context=context):
707 result[attach.id] = attach.url
709 result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
711 def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
712 result = dict.fromkeys(ids, False)
713 attachments = self.read(cr, uid, ids, ['res_model'], context=context)
714 view_attachment_ids = [attachment['id'] for attachment in attachments if attachment['res_model'] == 'ir.ui.view']
715 for attach in self.read(cr, uid, view_attachment_ids, ['res_model', 'res_id', 'type', 'datas'], context=context):
716 result[attach['id']] = self._compute_checksum(attach)
719 def _compute_checksum(self, attachment_dict):
720 if attachment_dict.get('res_model') == 'ir.ui.view'\
721 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
722 and attachment_dict.get('type', 'binary') == 'binary'\
723 and attachment_dict.get('datas'):
724 return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
727 def _datas_big(self, cr, uid, ids, name, arg, context=None):
728 result = dict.fromkeys(ids, False)
729 if context and context.get('bin_size'):
732 for record in self.browse(cr, uid, ids, context=context):
733 if record.res_model != 'ir.ui.view' or not record.datas: continue
735 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
736 except IOError: # apparently the error PIL.Image.open raises
742 'datas_checksum': fields.function(_datas_checksum, size=40,
743 string="Datas checksum", type='char', store=True, select=True),
744 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
745 'datas_big': fields.function (_datas_big, type='binary', store=True,
746 string="Resized file content"),
747 'mimetype': fields.char('Mime Type', readonly=True),
750 def _add_mimetype_if_needed(self, values):
751 if values.get('datas_fname'):
752 values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
754 def create(self, cr, uid, values, context=None):
755 chk = self._compute_checksum(values)
757 match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
760 self._add_mimetype_if_needed(values)
761 return super(ir_attachment, self).create(
762 cr, uid, values, context=context)
764 def write(self, cr, uid, ids, values, context=None):
765 self._add_mimetype_if_needed(values)
766 return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
768 def try_remove(self, cr, uid, ids, context=None):
769 """ Removes a web-based image attachment if it is used by no view
772 Returns a dict mapping attachments which would not be removed (if any)
773 mapped to the views preventing their removal
775 Views = self.pool['ir.ui.view']
776 attachments_to_remove = []
777 # views blocking removal of the attachment
778 removal_blocked_by = {}
780 for attachment in self.browse(cr, uid, ids, context=context):
781 # in-document URLs are html-escaped, a straight search will not
783 url = escape(attachment.website_url)
784 ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
787 removal_blocked_by[attachment.id] = Views.read(
788 cr, uid, ids, ['name'], context=context)
790 attachments_to_remove.append(attachment.id)
791 if attachments_to_remove:
792 self.unlink(cr, uid, attachments_to_remove, context=context)
793 return removal_blocked_by
795 class res_partner(osv.osv):
796 _inherit = "res.partner"
798 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
799 partner = self.browse(cr, uid, ids[0], context=context)
801 '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 ''),
802 'size': "%sx%s" % (height, width),
806 return urlplus('//maps.googleapis.com/maps/api/staticmap' , params)
808 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
809 partner = self.browse(cr, uid, ids[0], context=context)
811 '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 ''),
814 return urlplus('https://maps.google.com/maps' , params)
816 class res_company(osv.osv):
817 _inherit = "res.company"
818 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
819 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
820 return partner and partner.google_map_img(zoom, width, height, context=context) or None
821 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
822 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
823 return partner and partner.google_map_link(zoom, context=context) or None
825 class base_language_install(osv.osv_memory):
826 _inherit = "base.language.install"
828 'website_ids': fields.many2many('website', string='Websites to translate'),
831 def default_get(self, cr, uid, fields, context=None):
834 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
835 website_id = context.get('params', {}).get('website_id')
837 if 'website_ids' not in defaults:
838 defaults['website_ids'] = []
839 defaults['website_ids'].append(website_id)
842 def lang_install(self, cr, uid, ids, context=None):
845 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
846 language_obj = self.browse(cr, uid, ids)[0]
847 website_ids = [website.id for website in language_obj['website_ids']]
848 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
849 if website_ids and lang_id:
850 data = {'language_ids': [(4, lang_id[0])]}
851 self.pool['website'].write(cr, uid, website_ids, data)
852 params = context.get('params', {})
853 if 'url_return' in params:
855 'url': params['url_return'].replace('[lang]', language_obj['lang']),
856 'type': 'ir.actions.act_url',
861 class website_seo_metadata(osv.Model):
862 _name = 'website.seo.metadata'
863 _description = 'SEO metadata'
866 'website_meta_title': fields.char("Website meta title", translate=True),
867 'website_meta_description': fields.text("Website meta description", translate=True),
868 'website_meta_keywords': fields.char("Website meta keywords", translate=True),