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