21a192b0468746ecf7bbc8cbc3bd1e8162b709f5
[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, image_save_for_web
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             size = (max_w, max_h)
592             img = image_resize_and_sharpen(image, size)
593             image_save_for_web(img, response.stream, format=image.format)
594             # invalidate content-length computed by make_conditional as
595             # writing to response.stream does not do it (as of werkzeug 0.9.3)
596             del response.headers['Content-Length']
597
598         return response
599
600     def image_url(self, cr, uid, record, field, size=None, context=None):
601         """Returns a local url that points to the image field of a given browse record."""
602         model = record._name
603         id = '%s_%s' % (record.id, hashlib.sha1(record.write_date).hexdigest()[0:7])
604         size = '' if size is None else '-%s' % size
605         return '/website/image/%s-%s-%s%s' % (model, id, field, size)
606
607
608 class website_menu(osv.osv):
609     _name = "website.menu"
610     _description = "Website Menu"
611     _columns = {
612         'name': fields.char('Menu', required=True, translate=True),
613         'url': fields.char('Url'),
614         'new_window': fields.boolean('New Window'),
615         'sequence': fields.integer('Sequence'),
616         # TODO: support multiwebsite once done for ir.ui.views
617         'website_id': fields.many2one('website', 'Website'),
618         'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
619         'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
620         'parent_left': fields.integer('Parent Left', select=True),
621         'parent_right': fields.integer('Parent Right', select=True),
622     }
623
624     def __defaults_sequence(self, cr, uid, context):
625         menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context)
626         return menu and menu[0]["sequence"] or 0
627
628     _defaults = {
629         'url': '',
630         'sequence': __defaults_sequence,
631         'new_window': False,
632     }
633     _parent_store = True
634     _parent_order = 'sequence'
635     _order = "sequence"
636
637     # would be better to take a menu_id as argument
638     def get_tree(self, cr, uid, website_id, context=None):
639         def make_tree(node):
640             menu_node = dict(
641                 id=node.id,
642                 name=node.name,
643                 url=node.url,
644                 new_window=node.new_window,
645                 sequence=node.sequence,
646                 parent_id=node.parent_id.id,
647                 children=[],
648             )
649             for child in node.child_id:
650                 menu_node['children'].append(make_tree(child))
651             return menu_node
652         menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
653         return make_tree(menu)
654
655     def save(self, cr, uid, website_id, data, context=None):
656         def replace_id(old_id, new_id):
657             for menu in data['data']:
658                 if menu['id'] == old_id:
659                     menu['id'] = new_id
660                 if menu['parent_id'] == old_id:
661                     menu['parent_id'] = new_id
662         to_delete = data['to_delete']
663         if to_delete:
664             self.unlink(cr, uid, to_delete, context=context)
665         for menu in data['data']:
666             mid = menu['id']
667             if isinstance(mid, str):
668                 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
669                 replace_id(mid, new_id)
670         for menu in data['data']:
671             self.write(cr, uid, [menu['id']], menu, context=context)
672         return True
673
674 class ir_attachment(osv.osv):
675     _inherit = "ir.attachment"
676     def _website_url_get(self, cr, uid, ids, name, arg, context=None):
677         result = {}
678         for attach in self.browse(cr, uid, ids, context=context):
679             if attach.url:
680                 result[attach.id] = attach.url
681             else:
682                 result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
683         return result
684     def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
685         return dict(
686             (attach['id'], self._compute_checksum(attach))
687             for attach in self.read(
688                 cr, uid, ids, ['res_model', 'res_id', 'type', 'datas'],
689                 context=context)
690         )
691
692     def _compute_checksum(self, attachment_dict):
693         if attachment_dict.get('res_model') == 'ir.ui.view'\
694                 and not attachment_dict.get('res_id') and not attachment_dict.get('url')\
695                 and attachment_dict.get('type', 'binary') == 'binary'\
696                 and attachment_dict.get('datas'):
697             return hashlib.new('sha1', attachment_dict['datas']).hexdigest()
698         return None
699
700     def _datas_big(self, cr, uid, ids, name, arg, context=None):
701         result = dict.fromkeys(ids, False)
702         if context and context.get('bin_size'):
703             return result
704
705         for record in self.browse(cr, uid, ids, context=context):
706             if not record.datas: continue
707             try:
708                 result[record.id] = openerp.tools.image_resize_image_big(record.datas)
709             except IOError: # apparently the error PIL.Image.open raises
710                 pass
711
712         return result
713
714     _columns = {
715         'datas_checksum': fields.function(_datas_checksum, size=40,
716               string="Datas checksum", type='char', store=True, select=True),
717         'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
718         'datas_big': fields.function (_datas_big, type='binary', store=True,
719                                       string="Resized file content"),
720         'mimetype': fields.char('Mime Type', readonly=True),
721     }
722
723     def _add_mimetype_if_needed(self, values):
724         if values.get('datas_fname'):
725             values['mimetype'] = mimetypes.guess_type(values.get('datas_fname'))[0] or 'application/octet-stream'
726
727     def create(self, cr, uid, values, context=None):
728         chk = self._compute_checksum(values)
729         if chk:
730             match = self.search(cr, uid, [('datas_checksum', '=', chk)], context=context)
731             if match:
732                 return match[0]
733         self._add_mimetype_if_needed(values)
734         return super(ir_attachment, self).create(
735             cr, uid, values, context=context)
736
737     def write(self, cr, uid, ids, values, context=None):
738         self._add_mimetype_if_needed(values)
739         return super(ir_attachment, self).write(cr, uid, ids, values, context=context)
740
741     def try_remove(self, cr, uid, ids, context=None):
742         """ Removes a web-based image attachment if it is used by no view
743         (template)
744
745         Returns a dict mapping attachments which would not be removed (if any)
746         mapped to the views preventing their removal
747         """
748         Views = self.pool['ir.ui.view']
749         attachments_to_remove = []
750         # views blocking removal of the attachment
751         removal_blocked_by = {}
752
753         for attachment in self.browse(cr, uid, ids, context=context):
754             # in-document URLs are html-escaped, a straight search will not
755             # find them
756             url = escape(attachment.website_url)
757             ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context)
758
759             if ids:
760                 removal_blocked_by[attachment.id] = Views.read(
761                     cr, uid, ids, ['name'], context=context)
762             else:
763                 attachments_to_remove.append(attachment.id)
764         if attachments_to_remove:
765             self.unlink(cr, uid, attachments_to_remove, context=context)
766         return removal_blocked_by
767
768 class res_partner(osv.osv):
769     _inherit = "res.partner"
770
771     def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
772         partner = self.browse(cr, uid, ids[0], context=context)
773         params = {
774             '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 ''),
775             'size': "%sx%s" % (height, width),
776             'zoom': zoom,
777             'sensor': 'false',
778         }
779         return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
780
781     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
782         partner = self.browse(cr, uid, ids[0], context=context)
783         params = {
784             '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 ''),
785             'z': 10
786         }
787         return urlplus('https://maps.google.com/maps' , params)
788
789 class res_company(osv.osv):
790     _inherit = "res.company"
791     def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
792         partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
793         return partner and partner.google_map_img(zoom, width, height, context=context) or None
794     def google_map_link(self, cr, uid, ids, zoom=8, context=None):
795         partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
796         return partner and partner.google_map_link(zoom, context=context) or None
797
798 class base_language_install(osv.osv_memory):
799     _inherit = "base.language.install"
800     _columns = {
801         'website_ids': fields.many2many('website', string='Websites to translate'),
802     }
803
804     def default_get(self, cr, uid, fields, context=None):
805         if context is None:
806             context = {}
807         defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
808         website_id = context.get('params', {}).get('website_id')
809         if website_id:
810             if 'website_ids' not in defaults:
811                 defaults['website_ids'] = []
812             defaults['website_ids'].append(website_id)
813         return defaults
814
815     def lang_install(self, cr, uid, ids, context=None):
816         if context is None:
817             context = {}
818         action = super(base_language_install, self).lang_install(cr, uid, ids, context)
819         language_obj = self.browse(cr, uid, ids)[0]
820         website_ids = [website.id for website in language_obj['website_ids']]
821         lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
822         if website_ids and lang_id:
823             data = {'language_ids': [(4, lang_id[0])]}
824             self.pool['website'].write(cr, uid, website_ids, data)
825         params = context.get('params', {})
826         if 'url_return' in params:
827             return {
828                 'url': params['url_return'].replace('[lang]', language_obj['lang']),
829                 'type': 'ir.actions.act_url',
830                 'target': 'self'
831             }
832         return action
833
834 class website_seo_metadata(osv.Model):
835     _name = 'website.seo.metadata'
836     _description = 'SEO metadata'
837
838     _columns = {
839         'website_meta_title': fields.char("Website meta title", translate=True),
840         'website_meta_description': fields.text("Website meta description", translate=True),
841         'website_meta_keywords': fields.char("Website meta keywords", translate=True),
842     }
843
844 # vim:et: