a339bcb9c3efbb414efcad76af616e9b06631007
[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 logging
8 import math
9 import mimetypes
10 import unicodedata
11 import os
12 import re
13 import time
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, ustr, image_resize_and_sharpen
29 from openerp.tools.safe_eval import safe_eval
30 from openerp.addons.web.http import request
31
32 logger = logging.getLogger(__name__)
33
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)
43
44     if request and not url.netloc and not url.scheme and (url.path or force_lang):
45         location = urlparse.urljoin(current_path, location)
46
47         lang = lang or request.context.get('lang')
48         langs = [lg[0] for lg in request.website.get_languages()]
49
50         if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs):
51             ps = location.split('/')
52             if ps[1] in langs:
53                 # Replace the language only if we explicitly provide a language to url_for
54                 if force_lang:
55                     ps[1] = lang
56                 # Remove the default language unless it's explicitly provided
57                 elif ps[1] == request.website.default_lang_code:
58                     ps.pop(1)
59             # Insert the context language or the provided language
60             elif lang != request.website.default_lang_code or force_lang:
61                 ps.insert(1, lang)
62             location = '/'.join(ps)
63
64     return location.decode('utf-8')
65
66 def is_multilang_url(local_url, langs=None):
67     if not langs:
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
71     if spath[1] in langs:
72         spath.pop(1)
73         local_url = '/'.join(spath)
74     try:
75         # Try to match an endpoint in werkzeug's routing table
76         url = local_url.split('?')
77         path = url[0]
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)
82     except Exception:
83         return False
84
85 def slugify(s, max_length=None):
86     """ Transform a string to a slug that can be used in a url path.
87
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 "-".
92
93     :param s: str
94     :param max_length: int
95     :rtype: str
96     """
97     s = ustr(s)
98     if slugify_lib:
99         # There are 2 different libraries only python-slugify is supported
100         try:
101             return slugify_lib.slugify(s, max_length=max_length)
102         except TypeError:
103             pass
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)
107
108     return slug[:max_length]
109
110 def slug(value):
111     if isinstance(value, orm.browse_record):
112         # [(id, name)] = value.name_get()
113         id, name = value.id, value.display_name
114     else:
115         # assume name_search result tuple
116         id, name = value
117     slugname = slugify(name or '').strip().strip('-')
118     if not slugname:
119         return str(id)
120     return "%s-%d" % (slugname, id)
121
122
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+)(?=$|/)')
125
126 def unslug(s):
127     """Extract slug and id from a string.
128         Always return un 2-tuple (str|None, int|None)
129     """
130     m = _UNSLUG_RE.match(s)
131     if not m:
132         return None, None
133     return m.group(1), int(m.group(2))
134
135 def urlplus(url, params):
136     return werkzeug.Href(url)(params or None)
137
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)
142
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) )
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('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',
167             store= {
168                 'website.menu': (_get_menu_website, ['sequence','parent_id','website_id'], 10)
169             })
170     }
171
172     _defaults = {
173         'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
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             page_id = view.copy(cr, uid, template_id, context=context)
201             page = view.browse(cr, uid, page_id, context=context)
202             page.write({
203                 'arch': page.arch.replace(template, page_xmlid),
204                 'name': page_name,
205                 'page': ispage,
206             })
207             imd.create(cr, uid, {
208                 'name': page_name,
209                 'module': template_module,
210                 'model': 'ir.ui.view',
211                 'res_id': page_id,
212                 'noupdate': True
213             }, context=context)
214         return page_xmlid
215
216     def page_for_name(self, cr, uid, ids, name, module='website', context=None):
217         # whatever
218         return '%s.%s' % (module, slugify(name, max_length=50))
219
220     def page_exists(self, cr, uid, ids, name, module='website', context=None):
221         try:
222             name = (name or "").replace("/page/website.", "").replace("/page/", "")
223             if not name:
224                 return False
225             return self.pool["ir.model.data"].get_object_reference(cr, uid, module, name)
226         except:
227             return False
228
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]
233
234     def get_languages(self, cr, uid, ids, context=None):
235         return self._get_languages(cr, uid, ids[0], context=context)
236
237     def get_alternate_languages(self, cr, uid, ids, req=None, context=None):
238         langs = []
239         if req is None:
240             req = request.httprequest
241         default = self.get_current_website(cr, uid, context=context).default_lang_code
242         uri = req.path
243         if req.query_string:
244             uri += '?' + req.query_string
245         shorts = []
246         for code, name in self.get_languages(cr, uid, ids, context=context):
247             lg_path = ('/' + code) if code != default else ''
248             lg = code.split('_')
249             shorts.append(lg[0])
250             lang = {
251                 'hreflang': ('-'.join(lg)).lower(),
252                 'short': lg[0],
253                 'href': req.url_root[0:-1] + lg_path + uri,
254             }
255             langs.append(lang)
256         for lang in langs:
257             if shorts.count(lang['short']) == 1:
258                 lang['hreflang'] = lang['short']
259         return langs
260
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)
264
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
269
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)
273
274     def get_template(self, cr, uid, ids, template, context=None):
275         if isinstance(template, (int, long)):
276             view_id = template
277         else:
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)
283
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)
287
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)
291
292     def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
293         # Compute Pager
294         page_count = int(math.ceil(float(total) / step))
295
296         page = max(1, min(int(page if str(page).isdigit() else 1), page_count))
297         scope -= 1
298
299         pmin = max(page - int(math.floor(scope/2)), 1)
300         pmax = min(pmin + scope, page_count)
301
302         if pmax - pmin < scope:
303             pmin = pmax - scope if pmax - scope > 0 else 1
304
305         def get_url(page):
306             _url = "%s/page/%s" % (url, page) if page > 1 else url
307             if url_args:
308                 _url = "%s?%s" % (_url, werkzeug.url_encode(url_args))
309             return _url
310
311         return {
312             "page_count": page_count,
313             "offset": (page - 1) * step,
314             "page": {
315                 'url': get_url(page),
316                 'num': page
317             },
318             "page_start": {
319                 'url': get_url(pmin),
320                 'num': pmin
321             },
322             "page_previous": {
323                 'url': get_url(max(pmin, page - 1)),
324                 'num': max(pmin, page - 1)
325             },
326             "page_next": {
327                 'url': get_url(min(pmax, page + 1)),
328                 'num': min(pmax, page + 1)
329             },
330             "page_end": {
331                 'url': get_url(pmax),
332                 'num': pmax
333             },
334             "pages": [
335                 {'url': get_url(page), 'num': page}
336                 for page in xrange(pmin, pmax+1)
337             ]
338         }
339
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)
343
344         :type rule: werkzeug.routing.Rule
345         :rtype: bool
346         """
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')):
356             return False
357
358         # dont't list routes without argument having no default value or converter
359         spec = inspect.getargspec(endpoint.method.original_func)
360
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)]
364
365         # check that all args have a converter
366         return all( (arg in rule._converters) for arg in args)
367
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).
372
373         By default, returns template views marked as pages.
374
375         :param str query_string: a (user-provided) string, fetches pages
376                                  matching the string
377         :returns: a list of mappings with two keys: ``name`` is the displayable
378                   name of the resource (page), ``url`` is the absolute URL
379                   of the same.
380         :rtype: list({name: str, url: str})
381         """
382         router = request.httprequest.app.get_db_router(request.db)
383         # Force enumeration to be performed as public user
384         url_list = []
385         for rule in router.iter_rules():
386             if not self.rule_is_enumerable(rule):
387                 continue
388
389             converters = rule._converters or {}
390             if query_string and not converters and (query_string not in rule.build([{}], append_unknown=False)[1]):
391                 continue
392             values = [{}]
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):
398                 newval = []
399                 for val in values:
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() )
403                         v[name] = v['loc']
404                         del v['loc']
405                         newval[-1].update(v)
406                 values = newval
407
408             for value in values:
409                 domain_part, url = rule.build(value, append_unknown=False)
410                 page = {'loc': url}
411                 for key,val in value.items():
412                     if key.startswith('__'):
413                         page[key[2:]] = val
414                 if url in ('/sitemap.xml',):
415                     continue
416                 if url in url_list:
417                     continue
418                 url_list.append(url)
419
420                 yield page
421
422     def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
423         name = (needle or "").replace("/page/website.", "").replace("/page/", "")
424         res = []
425         for page in self.enumerate_pages(cr, uid, ids, query_string=name, context=context):
426             if needle in page['loc']:
427                 res.append(page)
428                 if len(res) == limit:
429                     break
430         return res
431
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"
436
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]
441
442         get_args.setdefault('kanban', "")
443         kanban = get_args.pop('kanban')
444         kanban_url = "?%s&kanban=" % werkzeug.url_encode(get_args)
445
446         pages = {}
447         for col in kanban.split(","):
448             if col:
449                 col = col.split("-")
450                 pages[int(col[0])] = int(col[1])
451
452         objects = []
453         for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
454             obj = {}
455
456             # browse column
457             relation_id = group[column][0]
458             obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
459
460             obj['kanban_url'] = kanban_url
461             for k, v in pages.items():
462                 if k != relation_id:
463                     obj['kanban_url'] += "%s-%s" % (k, v)
464
465             # pager
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'])
474
475             # view data
476             obj['domain'] = group['__domain']
477             obj['model'] = model
478             obj['step'] = step
479             obj['orderby'] = orderby
480
481             # browse objects
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)
484
485             objects.append(obj)
486
487         values = {
488             'objects': objects,
489             'range': range,
490             'template': template,
491         }
492         return request.website._render("website.kanban_contain", values)
493
494     def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
495         html = ""
496         model_obj = self.pool[model]
497         domain = safe_eval(domain)
498         step = int(step)
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})
504         return html
505
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'),
511                 mode='rb')) as f:
512             response.data = f.read()
513             return response.make_conditional(request.httprequest)
514
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.
518
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.
521
522         If the record is not found or does not have the requested field,
523         returns a placeholder image via :meth:`~._image_placeholder`.
524
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``)
529
530         The requested field is assumed to be base64-encoded image data in
531         all cases.
532         """
533         Model = self.pool[model]
534         id = int(id)
535
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)
541         if not ids:
542             return self._image_placeholder(response)
543
544         concurrency = '__last_update'
545         [record] = Model.read(cr, openerp.SUPERUSER_ID, [id],
546                               [concurrency, field],
547                               context=context)
548
549         if concurrency in record:
550             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
551             try:
552                 response.last_modified = datetime.datetime.strptime(
553                     record[concurrency], server_format + '.%f')
554             except ValueError:
555                 # just in case we have a timestamp without microseconds
556                 response.last_modified = datetime.datetime.strptime(
557                     record[concurrency], server_format)
558
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)
563
564         response.set_etag(hashlib.sha1(record[field]).hexdigest())
565         response.make_conditional(request.httprequest)
566
567         if cache:
568             response.cache_control.max_age = cache
569             response.expires = int(time.time() + cache)
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_format = image.format
592             size = (max_w, max_h)
593             image = image_resize_and_sharpen(image, size)
594
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')
599             else:
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']
604
605         return response
606
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."""
609         model = record._name
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)
613
614
615 class website_menu(osv.osv):
616     _name = "website.menu"
617     _description = "Website Menu"
618     _columns = {
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),
629     }
630
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
634
635     _defaults = {
636         'url': '',
637         'sequence': __defaults_sequence,
638         'new_window': False,
639     }
640     _parent_store = True
641     _parent_order = 'sequence'
642     _order = "sequence"
643
644     # would be better to take a menu_id as argument
645     def get_tree(self, cr, uid, website_id, context=None):
646         def make_tree(node):
647             menu_node = dict(
648                 id=node.id,
649                 name=node.name,
650                 url=node.url,
651                 new_window=node.new_window,
652                 sequence=node.sequence,
653                 parent_id=node.parent_id.id,
654                 children=[],
655             )
656             for child in node.child_id:
657                 menu_node['children'].append(make_tree(child))
658             return menu_node
659         menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
660         return make_tree(menu)
661
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:
666                     menu['id'] = new_id
667                 if menu['parent_id'] == old_id:
668                     menu['parent_id'] = new_id
669         to_delete = data['to_delete']
670         if to_delete:
671             self.unlink(cr, uid, to_delete, context=context)
672         for menu in data['data']:
673             mid = menu['id']
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)
679         return True
680
681 class ir_attachment(osv.osv):
682     _inherit = "ir.attachment"
683     def _website_url_get(self, cr, uid, ids, name, arg, context=None):
684         result = {}
685         for attach in self.browse(cr, uid, ids, context=context):
686             if attach.url:
687                 result[attach.id] = attach.url
688             else:
689                 result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
690         return result
691     def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
692         return dict(
693             (attach['id'], self._compute_checksum(attach))
694             for attach in self.read(
695                 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
696                 context=context)
697         )
698
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()
705         return None
706
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'):
710             return result
711
712         for record in self.browse(cr, uid, ids, context=context):
713             if not record.datas: continue
714             try:
715                 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
716             except IOError: # apparently the error PIL.Image.open raises
717                 pass
718
719         return result
720
721     _columns = {
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),
728     }
729
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'
733
734     def create(self, cr, uid, values, context=None):
735         chk = self._compute_checksum(values)
736         if chk:
737             match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
738             if match:
739                 return match[0]
740         self._add_mimetype_if_needed(values)
741         return super(ir_attachment, self).create(
742             cr, uid, values, context=context)
743
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)
747
748     def try_remove(self, cr, uid, ids, context=None):
749         """ Removes a web-based image attachment if it is used by no view
750         (template)
751
752         Returns a dict mapping attachments which would not be removed (if any)
753         mapped to the views preventing their removal
754         """
755         Views = self.pool['ir.ui.view']
756         attachments_to_remove = []
757         # views blocking removal of the attachment
758         removal_blocked_by = {}
759
760         for attachment in self.browse(cr, uid, ids, context=context):
761             # in-document URLs are html-escaped, a straight search will not
762             # find them
763             url = escape(attachment.website_url)
764             ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
765
766             if ids:
767                 removal_blocked_by[attachment.id] = Views.read(
768                     cr, uid, ids, ['name'], context=context)
769             else:
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
774
775 class res_partner(osv.osv):
776     _inherit = "res.partner"
777
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)
780         params = {
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),
783             'zoom': zoom,
784             'sensor': 'false',
785         }
786         return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
787
788     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
789         partner = self.browse(cr, uid, ids[0], context=context)
790         params = {
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 ''),
792             'z': 10
793         }
794         return urlplus('https://maps.google.com/maps' , params)
795
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
804
805 class base_language_install(osv.osv_memory):
806     _inherit = "base.language.install"
807     _columns = {
808         'website_ids': fields.many2many('website', string='Websites to translate'),
809     }
810
811     def default_get(self, cr, uid, fields, context=None):
812         if context is None:
813             context = {}
814         defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
815         website_id = context.get('params', {}).get('website_id')
816         if website_id:
817             if 'website_ids' not in defaults:
818                 defaults['website_ids'] = []
819             defaults['website_ids'].append(website_id)
820         return defaults
821
822     def lang_install(self, cr, uid, ids, context=None):
823         if context is None:
824             context = {}
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:
834             return {
835                 'url': params['url_return'].replace('[lang]', language_obj['lang']),
836                 'type': 'ir.actions.act_url',
837                 'target': 'self'
838             }
839         return action
840
841 class website_seo_metadata(osv.Model):
842     _name = 'website.seo.metadata'
843     _description = 'SEO metadata'
844
845     _columns = {
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),
849     }
850
851 # vim:et: