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