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