[IMP] Website name instead of company name in title and logo
[odoo/odoo.git] / addons / website / models / website.py
1 # -*- coding: utf-8 -*-
2 import cStringIO
3 import contextlib
4 import datetime
5 import hashlib
6 import inspect
7 import itertools
8 import logging
9 import math
10 import mimetypes
11 import unicodedata
12 import os
13 import re
14 import urlparse
15
16 from PIL import Image
17 from sys import maxint
18
19 import werkzeug
20 # optional python-slugify import (https://github.com/un33k/python-slugify)
21 try:
22     import slugify as slugify_lib
23 except ImportError:
24     slugify_lib = None
25
26 import openerp
27 from openerp.osv import orm, osv, fields
28 from openerp.tools import html_escape as escape
29 from openerp.tools import ustr as ustr
30 from openerp.tools.safe_eval import safe_eval
31 from openerp.addons.web.http import request
32 from werkzeug.exceptions import NotFound
33
34 logger = logging.getLogger(__name__)
35
36 def url_for(path_or_uri, lang=None):
37     if isinstance(path_or_uri, unicode):
38         path_or_uri = path_or_uri.encode('utf-8')
39     current_path = request.httprequest.path
40     if isinstance(current_path, unicode):
41         current_path = current_path.encode('utf-8')
42     location = path_or_uri.strip()
43     force_lang = lang is not None
44     url = urlparse.urlparse(location)
45
46     if request and not url.netloc and not url.scheme and (url.path or force_lang):
47         location = urlparse.urljoin(current_path, location)
48
49         lang = lang or request.context.get('lang')
50         langs = [lg[0] for lg in request.website.get_languages()]
51
52         if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs):
53             ps = location.split('/')
54             if ps[1] in langs:
55                 # Replace the language only if we explicitly provide a language to url_for
56                 if force_lang:
57                     ps[1] = lang
58                 # Remove the default language unless it's explicitly provided
59                 elif ps[1] == request.website.default_lang_code:
60                     ps.pop(1)
61             # Insert the context language or the provided language
62             elif lang != request.website.default_lang_code or force_lang:
63                 ps.insert(1, lang)
64             location = '/'.join(ps)
65
66     return location.decode('utf-8')
67
68 def is_multilang_url(local_url, langs=None):
69     if not langs:
70         langs = [lg[0] for lg in request.website.get_languages()]
71     spath = local_url.split('/')
72     # if a language is already in the path, remove it
73     if spath[1] in langs:
74         spath.pop(1)
75         local_url = '/'.join(spath)
76     try:
77         # Try to match an endpoint in werkzeug's routing table
78         url = local_url.split('?')
79         path = url[0]
80         query_string = url[1] if len(url) > 1 else None
81         router = request.httprequest.app.get_db_router(request.db).bind('')
82         func = router.match(path, query_args=query_string)[0]
83         return func.routing.get('website', False) and func.routing.get('multilang', True)
84     except Exception:
85         return False
86
87 def slugify(s, max_length=None):
88     """ Transform a string to a slug that can be used in a url path.
89
90     This method will first try to do the job with python-slugify if present.
91     Otherwise it will process string by stripping leading and ending spaces,
92     converting unicode chars to ascii, lowering all chars and replacing spaces
93     and underscore with hyphen "-".
94
95     :param s: str
96     :param max_length: int
97     :rtype: str
98     """
99     s = ustr(s)
100     if slugify_lib:
101         # There are 2 different libraries only python-slugify is supported
102         try:
103             return slugify_lib.slugify(s, max_length=max_length)
104         except TypeError:
105             pass
106     uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
107     slug = re.sub('[\W_]', ' ', uni).strip().lower()
108     slug = re.sub('[-\s]+', '-', slug)
109
110     return slug[:max_length]
111
112 def slug(value):
113     if isinstance(value, orm.browse_record):
114         # [(id, name)] = value.name_get()
115         id, name = value.id, value.display_name
116     else:
117         # assume name_search result tuple
118         id, name = value
119     slugname = slugify(name or '').strip().strip('-')
120     if not slugname:
121         return str(id)
122     return "%s-%d" % (slugname, id)
123
124
125 # NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags
126 _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)')
127
128 def unslug(s):
129     """Extract slug and id from a string.
130         Always return un 2-tuple (str|None, int|None)
131     """
132     m = _UNSLUG_RE.match(s)
133     if not m:
134         return None, None
135     return m.group(1), int(m.group(2))
136
137 def urlplus(url, params):
138     return werkzeug.Href(url)(params or None)
139
140 class website(osv.osv):
141     def _get_menu(self, cr, uid, ids, name, arg, context=None):
142         res = {}
143         menu_obj = self.pool.get('website.menu')
144         for id in ids:
145             menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False), ('website_id', '=', id)], order='id', context=context)
146             res[id] = menu_ids and menu_ids[0] or False
147         return res
148
149     _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
150     _description = "Website"
151     _columns = {
152         'name': fields.char('Website Name'),
153         'domain': fields.char('Website Domain'),
154         'company_id': fields.many2one('res.company', string="Company"),
155         'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
156         'default_lang_id': fields.many2one('res.lang', string="Default language"),
157         'default_lang_code': fields.related('default_lang_id', 'code', type="char", string="Default language code", store=True),
158         'social_twitter': fields.char('Twitter Account'),
159         'social_facebook': fields.char('Facebook Account'),
160         'social_github': fields.char('GitHub Account'),
161         'social_linkedin': fields.char('LinkedIn Account'),
162         'social_youtube': fields.char('Youtube Account'),
163         'social_googleplus': fields.char('Google+ Account'),
164         'google_analytics_key': fields.char('Google Analytics Key'),
165         'user_id': fields.many2one('res.users', string='Public User'),
166         'compress_html': fields.boolean('Compress HTML'),
167         'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'),
168         'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu')
169     }
170     _defaults = {
171         'user_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
172         'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID,'base.main_company'),
173         'compress_html': False,
174     }
175
176     # cf. Wizard hack in website_views.xml
177     def noop(self, *args, **kwargs):
178         pass
179
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)
183
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('.')
189
190         # completely arbitrary max_length
191         page_name = slugify(name, max_length=50)
192         page_xmlid = "%s.%s" % (template_module, page_name)
193
194         try:
195             # existing page
196             imd.get_object_reference(cr, uid, template_module, page_name)
197         except ValueError:
198             # new page
199             _, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
200             website_id = context.get('website_id')
201             key = template_module+'.'+page_name
202             page_id = view.copy(cr, uid, template_id, {'website_id': website_id, 'key': key}, context=context)
203             page = view.browse(cr, uid, page_id, context=context)
204             page.write({
205                 'arch': page.arch.replace(template, page_xmlid),
206                 'name': page_name,
207                 'page': ispage,
208             })
209         return page_xmlid
210
211     def page_for_name(self, cr, uid, ids, name, module='website', context=None):
212         # whatever
213         return '%s.%s' % (module, slugify(name, max_length=50))
214
215     def page_exists(self, cr, uid, ids, name, module='website', context=None):
216         try:
217             name = (name or "").replace("/page/website.", "").replace("/page/", "")
218             if not name:
219                 return False
220             return self.pool["ir.model.data"].get_object_reference(cr, uid, module, name)
221         except:
222             return False
223
224     @openerp.tools.ormcache(skiparg=3)
225     def _get_languages(self, cr, uid, id, context=None):
226         website = self.browse(cr, uid, id)
227         return [(lg.code, lg.name) for lg in website.language_ids]
228
229     def get_languages(self, cr, uid, ids, context=None):
230         return self._get_languages(cr, uid, ids[0], context=context)
231
232     def get_alternate_languages(self, cr, uid, ids, req=None, context=None):
233         langs = []
234         if req is None:
235             req = request.httprequest
236         default = self.get_current_website(cr, uid, context=context).default_lang_code
237         uri = req.path
238         if req.query_string:
239             uri += '?' + req.query_string
240         shorts = []
241         for code, name in self.get_languages(cr, uid, ids, context=context):
242             lg_path = ('/' + code) if code != default else ''
243             lg = code.split('_')
244             shorts.append(lg[0])
245             lang = {
246                 'hreflang': ('-'.join(lg)).lower(),
247                 'short': lg[0],
248                 'href': req.url_root[0:-1] + lg_path + uri,
249             }
250             langs.append(lang)
251         for lang in langs:
252             if shorts.count(lang['short']) == 1:
253                 lang['hreflang'] = lang['short']
254         return langs
255
256     @openerp.tools.ormcache(skiparg=4)
257     def _get_current_website_id(self, cr, uid, domain_name, context=None):
258         website_id = 1
259         if request:
260             ids = self.search(cr, uid, [('name', '=', domain_name)], context=context)
261             if ids:
262                 website_id = ids[0]
263         return website_id
264
265     def get_current_website(self, cr, uid, context=None):
266         domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0]
267         website_id = self._get_current_website_id(cr, uid, domain_name, context=context)
268         return self.browse(cr, uid, website_id, context=context)
269
270     def is_publisher(self, cr, uid, ids, context=None):
271         Access = self.pool['ir.model.access']
272         is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context=context)
273         return is_website_publisher
274
275     def is_user(self, cr, uid, ids, context=None):
276         Access = self.pool['ir.model.access']
277         return Access.check(cr, uid, 'ir.ui.menu', 'read', False, context=context)
278
279     def get_template(self, cr, uid, ids, template, context=None):
280         if not isinstance(template, (int, long)) and '.' not in template:
281             template = 'website.%s' % template
282         View = self.pool['ir.ui.view']
283         view_id = View.get_view_id(cr, uid, template, context=context)
284         if not view_id:
285             raise NotFound
286         return View.browse(cr, uid, view_id, context=context)
287
288     def _render(self, cr, uid, ids, template, values=None, context=None):
289         # TODO: remove this. (just kept for backward api compatibility for saas-3)
290         return self.pool['ir.ui.view'].render(cr, uid, template, values=values, context=context)
291
292     def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
293         # TODO: remove this. (just kept for backward api compatibility for saas-3)
294         return request.render(template, values, uid=uid)
295
296     def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
297         # Compute Pager
298         page_count = int(math.ceil(float(total) / step))
299
300         page = max(1, min(int(page if str(page).isdigit() else 1), page_count))
301         scope -= 1
302
303         pmin = max(page - int(math.floor(scope/2)), 1)
304         pmax = min(pmin + scope, page_count)
305
306         if pmax - pmin < scope:
307             pmin = pmax - scope if pmax - scope > 0 else 1
308
309         def get_url(page):
310             _url = "%s/page/%s" % (url, page) if page > 1 else url
311             if url_args:
312                 _url = "%s?%s" % (_url, werkzeug.url_encode(url_args))
313             return _url
314
315         return {
316             "page_count": page_count,
317             "offset": (page - 1) * step,
318             "page": {
319                 'url': get_url(page),
320                 'num': page
321             },
322             "page_start": {
323                 'url': get_url(pmin),
324                 'num': pmin
325             },
326             "page_previous": {
327                 'url': get_url(max(pmin, page - 1)),
328                 'num': max(pmin, page - 1)
329             },
330             "page_next": {
331                 'url': get_url(min(pmax, page + 1)),
332                 'num': min(pmax, page + 1)
333             },
334             "page_end": {
335                 'url': get_url(pmax),
336                 'num': pmax
337             },
338             "pages": [
339                 {'url': get_url(page), 'num': page}
340                 for page in xrange(pmin, pmax+1)
341             ]
342         }
343
344     def rule_is_enumerable(self, rule):
345         """ Checks that it is possible to generate sensible GET queries for
346         a given rule (if the endpoint matches its own requirements)
347
348         :type rule: werkzeug.routing.Rule
349         :rtype: bool
350         """
351         endpoint = rule.endpoint
352         methods = rule.methods or ['GET']
353         converters = rule._converters.values()
354         if not ('GET' in methods
355             and endpoint.routing['type'] == 'http'
356             and endpoint.routing['auth'] in ('none', 'public')
357             and endpoint.routing.get('website', False)
358             and all(hasattr(converter, 'generate') for converter in converters)
359             and endpoint.routing.get('website')):
360             return False
361
362         # dont't list routes without argument having no default value or converter
363         spec = inspect.getargspec(endpoint.method.original_func)
364
365         # remove self and arguments having a default value
366         defaults_count = len(spec.defaults or [])
367         args = spec.args[1:(-defaults_count or None)]
368
369         # check that all args have a converter
370         return all( (arg in rule._converters) for arg in args)
371
372     def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
373         """ Available pages in the website/CMS. This is mostly used for links
374         generation and can be overridden by modules setting up new HTML
375         controllers for dynamic pages (e.g. blog).
376
377         By default, returns template views marked as pages.
378
379         :param str query_string: a (user-provided) string, fetches pages
380                                  matching the string
381         :returns: a list of mappings with two keys: ``name`` is the displayable
382                   name of the resource (page), ``url`` is the absolute URL
383                   of the same.
384         :rtype: list({name: str, url: str})
385         """
386         router = request.httprequest.app.get_db_router(request.db)
387         # Force enumeration to be performed as public user
388         url_list = []
389         for rule in router.iter_rules():
390             if not self.rule_is_enumerable(rule):
391                 continue
392
393             converters = rule._converters or {}
394             if query_string and not converters and (query_string not in rule.build([{}], append_unknown=False)[1]):
395                 continue
396             values = [{}]
397             convitems = converters.items()
398             # converters with a domain are processed after the other ones
399             gd = lambda x: hasattr(x[1], 'domain') and (x[1].domain <> '[]')
400             convitems.sort(lambda x, y: cmp(gd(x), gd(y)))
401             for (i,(name, converter)) in enumerate(convitems):
402                 newval = []
403                 for val in values:
404                     query = i==(len(convitems)-1) and query_string
405                     for v in converter.generate(request.cr, uid, query=query, args=val, context=context):
406                         newval.append( val.copy() )
407                         v[name] = v['loc']
408                         del v['loc']
409                         newval[-1].update(v)
410                 values = newval
411
412             for value in values:
413                 domain_part, url = rule.build(value, append_unknown=False)
414                 page = {'loc': url}
415                 for key,val in value.items():
416                     if key.startswith('__'):
417                         page[key[2:]] = val
418                 if url in ('/sitemap.xml',):
419                     continue
420                 if url in url_list:
421                     continue
422                 url_list.append(url)
423
424                 yield page
425
426     def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
427         name = (needle or "").replace("/page/website.", "").replace("/page/", "")
428         res = []
429         for page in self.enumerate_pages(cr, uid, ids, query_string=name, context=context):
430             if needle in page['loc']:
431                 res.append(page)
432                 if len(res) == limit:
433                     break
434         return res
435
436     def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
437         step = step and int(step) or 10
438         scope = scope and int(scope) or 5
439         orderby = orderby or "name"
440
441         get_args = dict(request.httprequest.args or {})
442         model_obj = self.pool[model]
443         relation = model_obj._columns.get(column)._obj
444         relation_obj = self.pool[relation]
445
446         get_args.setdefault('kanban', "")
447         kanban = get_args.pop('kanban')
448         kanban_url = "?%s&kanban=" % werkzeug.url_encode(get_args)
449
450         pages = {}
451         for col in kanban.split(","):
452             if col:
453                 col = col.split("-")
454                 pages[int(col[0])] = int(col[1])
455
456         objects = []
457         for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
458             obj = {}
459
460             # browse column
461             relation_id = group[column][0]
462             obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
463
464             obj['kanban_url'] = kanban_url
465             for k, v in pages.items():
466                 if k != relation_id:
467                     obj['kanban_url'] += "%s-%s" % (k, v)
468
469             # pager
470             number = model_obj.search(cr, uid, group['__domain'], count=True)
471             obj['page_count'] = int(math.ceil(float(number) / step))
472             obj['page'] = pages.get(relation_id) or 1
473             if obj['page'] > obj['page_count']:
474                 obj['page'] = obj['page_count']
475             offset = (obj['page']-1) * step
476             obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
477             obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
478
479             # view data
480             obj['domain'] = group['__domain']
481             obj['model'] = model
482             obj['step'] = step
483             obj['orderby'] = orderby
484
485             # browse objects
486             object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
487             obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
488
489             objects.append(obj)
490
491         values = {
492             'objects': objects,
493             'range': range,
494             'template': template,
495         }
496         return request.website._render("website.kanban_contain", values)
497
498     def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
499         html = ""
500         model_obj = self.pool[model]
501         domain = safe_eval(domain)
502         step = int(step)
503         offset = (int(page)-1) * step
504         object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
505         object_ids = model_obj.browse(cr, uid, object_ids)
506         for object_id in object_ids:
507             html += request.website._render(template, {'object_id': object_id})
508         return html
509
510     def _image_placeholder(self, response):
511         # file_open may return a StringIO. StringIO can be closed but are
512         # not context managers in Python 2 though that is fixed in 3
513         with contextlib.closing(openerp.tools.misc.file_open(
514                 os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
515                 mode='rb')) as f:
516             response.data = f.read()
517             return response.make_conditional(request.httprequest)
518
519     def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_height=maxint, context=None):
520         """ Fetches the requested field and ensures it does not go above
521         (max_width, max_height), resizing it if necessary.
522
523         Resizing is bypassed if the object provides a $field_big, which will
524         be interpreted as a pre-resized version of the base field.
525
526         If the record is not found or does not have the requested field,
527         returns a placeholder image via :meth:`~._image_placeholder`.
528
529         Sets and checks conditional response parameters:
530         * :mailheader:`ETag` is always set (and checked)
531         * :mailheader:`Last-Modified is set iif the record has a concurrency
532           field (``__last_update``)
533
534         The requested field is assumed to be base64-encoded image data in
535         all cases.
536         """
537         Model = self.pool[model]
538         id = int(id)
539
540         ids = Model.search(cr, uid,
541                            [('id', '=', id)], context=context)
542         if not ids and 'website_published' in Model._all_columns:
543             ids = Model.search(cr, openerp.SUPERUSER_ID,
544                                [('id', '=', id), ('website_published', '=', True)], context=context)
545         if not ids:
546             return self._image_placeholder(response)
547
548         concurrency = '__last_update'
549         [record] = Model.read(cr, openerp.SUPERUSER_ID, [id],
550                               [concurrency, field],
551                               context=context)
552
553         if concurrency in record:
554             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
555             try:
556                 response.last_modified = datetime.datetime.strptime(
557                     record[concurrency], server_format + '.%f')
558             except ValueError:
559                 # just in case we have a timestamp without microseconds
560                 response.last_modified = datetime.datetime.strptime(
561                     record[concurrency], server_format)
562
563         # Field does not exist on model or field set to False
564         if not record.get(field):
565             # FIXME: maybe a field which does not exist should be a 404?
566             return self._image_placeholder(response)
567
568         response.set_etag(hashlib.sha1(record[field]).hexdigest())
569         response.make_conditional(request.httprequest)
570
571         # conditional request match
572         if response.status_code == 304:
573             return response
574
575         data = record[field].decode('base64')
576
577         if (not max_width) and (not max_height):
578             response.data = data
579             return response
580
581         image = Image.open(cStringIO.StringIO(data))
582         response.mimetype = Image.MIME[image.format]
583
584         w, h = image.size
585         max_w = int(max_width) if max_width else maxint
586         max_h = int(max_height) if max_height else maxint
587
588         if w < max_w and h < max_h:
589             response.data = data
590         else:
591             image.thumbnail((max_w, max_h), Image.ANTIALIAS)
592             image.save(response.stream, image.format)
593             # invalidate content-length computed by make_conditional as
594             # writing to response.stream does not do it (as of werkzeug 0.9.3)
595             del response.headers['Content-Length']
596
597         return response
598
599
600 class website_menu(osv.osv):
601     _name = "website.menu"
602     _description = "Website Menu"
603     _columns = {
604         'name': fields.char('Menu', required=True, translate=True),
605         'url': fields.char('Url'),
606         'new_window': fields.boolean('New Window'),
607         'sequence': fields.integer('Sequence'),
608         # TODO: support multiwebsite once done for ir.ui.views
609         'website_id': fields.many2one('website', 'Website'),
610         'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
611         'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
612         'parent_left': fields.integer('Parent Left', select=True),
613         'parent_right': fields.integer('Parent Right', select=True),
614     }
615
616     def __defaults_sequence(self, cr, uid, context):
617         menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
618         return menu and menu[0]["sequence"] or 0
619
620     _defaults = {
621         'url': '',
622         'sequence': __defaults_sequence,
623         'new_window': False,
624     }
625     _parent_store = True
626     _parent_order = 'sequence'
627     _order = "sequence"
628
629     # would be better to take a menu_id as argument
630     def get_tree(self, cr, uid, website_id, context=None):
631         def make_tree(node):
632             menu_node = dict(
633                 id=node.id,
634                 name=node.name,
635                 url=node.url,
636                 new_window=node.new_window,
637                 sequence=node.sequence,
638                 parent_id=node.parent_id.id,
639                 children=[],
640             )
641             for child in node.child_id:
642                 menu_node['children'].append(make_tree(child))
643             return menu_node
644         menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
645         return make_tree(menu)
646
647     def save(self, cr, uid, website_id, data, context=None):
648         def replace_id(old_id, new_id):
649             for menu in data['data']:
650                 if menu['id'] == old_id:
651                     menu['id'] = new_id
652                 if menu['parent_id'] == old_id:
653                     menu['parent_id'] = new_id
654         to_delete = data['to_delete']
655         if to_delete:
656             self.unlink(cr, uid, to_delete, context=context)
657         for menu in data['data']:
658             mid = menu['id']
659             if isinstance(mid, str):
660                 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
661                 replace_id(mid, new_id)
662         for menu in data['data']:
663             self.write(cr, uid, [menu['id']], menu, context=context)
664         return True
665
666 class ir_attachment(osv.osv):
667     _inherit = "ir.attachment"
668     def _website_url_get(self, cr, uid, ids, name, arg, context=None):
669         result = {}
670         for attach in self.browse(cr, uid, ids, context=context):
671             if attach.url:
672                 result[attach.id] = attach.url
673             else:
674                 result[attach.id] = urlplus('/website/image', {
675                     'model': 'ir.attachment',
676                     'field': 'datas',
677                     'id': attach.id
678                 })
679         return result
680     def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
681         return dict(
682             (attach['id'], self._compute_checksum(attach))
683             for attach in self.read(
684                 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
685                 context=context)
686         )
687
688     def _compute_checksum(self, attachment_dict):
689         if attachment_dict.get('res_model') == 'ir.ui.view'\
690                 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
691                 and attachment_dict.get('type', 'binary') == 'binary'\
692                 and attachment_dict.get('datas'):
693             return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
694         return None
695
696     def _datas_big(self, cr, uid, ids, name, arg, context=None):
697         result = dict.fromkeys(ids, False)
698         if context and context.get('bin_size'):
699             return result
700
701         for record in self.browse(cr, uid, ids, context=context):
702             if not record.datas: continue
703             try:
704                 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
705             except IOError: # apparently the error PIL.Image.open raises
706                 pass
707
708         return result
709
710     _columns = {
711         'datas_checksum': fields.function(_datas_checksum, size=40,
712               string="Datas checksum", type='char', store=True, select=True),
713         'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
714         'datas_big': fields.function (_datas_big, type='binary', store=True,
715                                       string="Resized file content"),
716         'mimetype': fields.char('Mime Type', readonly=True),
717     }
718
719     def _add_mimetype_if_needed(self, values):
720         if values.get('datas_fname'):
721             values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
722
723     def create(self, cr, uid, values, context=None):
724         chk = self._compute_checksum(values)
725         if chk:
726             match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
727             if match:
728                 return match[0]
729         self._add_mimetype_if_needed(values)
730         return super(ir_attachment, self).create(
731             cr, uid, values, context=context)
732
733     def write(self, cr, uid, ids, values, context=None):
734         self._add_mimetype_if_needed(values)
735         return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
736
737     def try_remove(self, cr, uid, ids, context=None):
738         """ Removes a web-based image attachment if it is used by no view
739         (template)
740
741         Returns a dict mapping attachments which would not be removed (if any)
742         mapped to the views preventing their removal
743         """
744         Views = self.pool['ir.ui.view']
745         attachments_to_remove = []
746         # views blocking removal of the attachment
747         removal_blocked_by = {}
748
749         for attachment in self.browse(cr, uid, ids, context=context):
750             # in-document URLs are html-escaped, a straight search will not
751             # find them
752             url = escape(attachment.website_url)
753             ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
754
755             if ids:
756                 removal_blocked_by[attachment.id] = Views.read(
757                     cr, uid, ids, ['name'], context=context)
758             else:
759                 attachments_to_remove.append(attachment.id)
760         if attachments_to_remove:
761             self.unlink(cr, uid, attachments_to_remove, context=context)
762         return removal_blocked_by
763
764 class res_partner(osv.osv):
765     _inherit = "res.partner"
766
767     def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
768         partner = self.browse(cr, uid, ids[0], context=context)
769         params = {
770             '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 ''),
771             'size': "%sx%s" % (height, width),
772             'zoom': zoom,
773             'sensor': 'false',
774         }
775         return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
776
777     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
778         partner = self.browse(cr, uid, ids[0], context=context)
779         params = {
780             '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 ''),
781             'z': 10
782         }
783         return urlplus('https://maps.google.com/maps' , params)
784
785 class res_company(osv.osv):
786     _inherit = "res.company"
787     def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
788         partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
789         return partner and partner.google_map_img(zoom, width, height, context=context) or None
790     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
791         partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
792         return partner and partner.google_map_link(zoom, context=context) or None
793
794 class base_language_install(osv.osv_memory):
795     _inherit = "base.language.install"
796     _columns = {
797         'website_ids': fields.many2many('website', string='Websites to translate'),
798     }
799
800     def default_get(self, cr, uid, fields, context=None):
801         if context is None:
802             context = {}
803         defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
804         website_id = context.get('params', {}).get('website_id')
805         if website_id:
806             if 'website_ids' not in defaults:
807                 defaults['website_ids'] = []
808             defaults['website_ids'].append(website_id)
809         return defaults
810
811     def lang_install(self, cr, uid, ids, context=None):
812         if context is None:
813             context = {}
814         action = super(base_language_install, self).lang_install(cr, uid, ids, context)
815         language_obj = self.browse(cr, uid, ids)[0]
816         website_ids = [website.id for website in language_obj['website_ids']]
817         lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
818         if website_ids and lang_id:
819             data = {'language_ids': [(4, lang_id[0])]}
820             self.pool['website'].write(cr, uid, website_ids, data)
821         params = context.get('params', {})
822         if 'url_return' in params:
823             return {
824                 'url': params['url_return'].replace('[lang]', language_obj['lang']),
825                 'type': 'ir.actions.act_url',
826                 'target': 'self'
827             }
828         return action
829
830 class website_seo_metadata(osv.Model):
831     _name = 'website.seo.metadata'
832     _description = 'SEO metadata'
833
834     _columns = {
835         'website_meta_title': fields.char("Website meta title", translate=True),
836         'website_meta_description': fields.text("Website meta description", translate=True),
837         'website_meta_keywords': fields.char("Website meta keywords", translate=True),
838     }
839
840 # vim:et: