1 # -*- coding: utf-8 -*-
13 import werkzeug.exceptions
14 import werkzeug.wrappers
15 # optional python-slugify import (https://github.com/un33k/python-slugify)
17 import slugify as slugify_lib
22 from openerp.osv import orm, osv, fields
23 from openerp.tools.safe_eval import safe_eval
24 from openerp.addons.web.http import request, LazyResponse
26 logger = logging.getLogger(__name__)
28 def url_for(path_or_uri, lang=None, keep_query=None):
29 location = path_or_uri.strip()
30 url = urlparse.urlparse(location)
31 if request and not url.netloc and not url.scheme:
32 location = urlparse.urljoin(request.httprequest.path, location)
33 lang = lang or request.context.get('lang')
34 langs = [lg[0] for lg in request.website.get_languages()]
35 if location[0] == '/' and len(langs) > 1 and lang != request.website.default_lang_code:
36 ps = location.split('/')
41 location = '/'.join(ps)
43 url = urlparse.urlparse(location)
45 params = werkzeug.url_decode(url.query)
46 query_params = frozenset(werkzeug.url_decode(request.httprequest.query_string).keys())
48 for param in fnmatch.filter(query_params, kq):
49 params[param] = request.params[param]
50 params = werkzeug.urls.url_encode(params)
52 location += '?%s' % params
56 def slugify(s, max_length=None):
58 return slugify_lib.slugify(s, max_length)
59 spaceless = re.sub(r'\s+', '-', s)
60 specialless = re.sub(r'[^-_A-Za-z0-9]', '', spaceless)
61 return specialless[:max_length]
64 if isinstance(value, orm.browse_record):
65 # [(id, name)] = value.name_get()
66 id, name = value.id, value[value._rec_name]
68 # assume name_search result tuple
70 return "%s-%d" % (slugify(name), id)
72 def urlplus(url, params):
76 # can't use urlencode because it encodes to (ascii, replace) in p2
77 return "%s?%s" % (url, '&'.join(
78 k + '=' + urllib.quote_plus(v.encode('utf-8') if isinstance(v, unicode) else str(v))
79 for k, v in params.iteritems()
82 def quote_plus(value):
83 return urllib.quote_plus(value.encode('utf-8') if isinstance(value, unicode) else str(value))
85 class website(osv.osv):
86 def _get_menu_website(self, cr, uid, ids, context=None):
87 # IF a menu is changed, update all websites
88 return self.search(cr, uid, [], context=context)
90 def _get_menu(self, cr, uid, ids, name, arg, context=None):
91 root_domain = [('parent_id', '=', False)]
92 menus = self.pool.get('website.menu').search(cr, uid, root_domain, order='id', context=context)
93 menu = menus and menus[0] or False
94 return dict( map(lambda x: (x, menu), ids) )
96 def _get_public_user(self, cr, uid, ids, name='public_user', arg=(), context=None):
97 ref = self.get_public_user(cr, uid, context=context)
98 return dict( map(lambda x: (x, ref), ids) )
100 _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
101 _description = "Website"
103 'name': fields.char('Domain'),
104 'company_id': fields.many2one('res.company', string="Company"),
105 'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
106 'default_lang_id': fields.many2one('res.lang', string="Default language"),
107 'default_lang_code': fields.related('default_lang_id', 'code', type="char", string="Default language code", store=True),
108 'social_twitter': fields.char('Twitter Account'),
109 'social_facebook': fields.char('Facebook Account'),
110 'social_github': fields.char('GitHub Account'),
111 'social_linkedin': fields.char('LinkedIn Account'),
112 'social_youtube': fields.char('Youtube Account'),
113 'social_googleplus': fields.char('Google+ Account'),
114 'google_analytics_key': fields.char('Google Analytics Key'),
115 'public_user': fields.function(_get_public_user, relation='res.users', type='many2one', string='Public User'),
116 'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu',
118 'website.menu': (_get_menu_website, ['sequence','parent_id','website_id'], 10)
122 # cf. Wizard hack in website_views.xml
123 def noop(self, *args, **kwargs):
126 def write(self, cr, uid, ids, vals, context=None):
127 self._get_languages.clear_cache(self)
128 return super(website, self).write(cr, uid, ids, vals, context)
130 def new_page(self, cr, uid, name, template='website.default_page', ispage=True, context=None):
131 context = context or {}
132 imd = self.pool.get('ir.model.data')
133 view = self.pool.get('ir.ui.view')
134 template_module, template_name = template.split('.')
136 # completely arbitrary max_length
137 page_name = slugify(name, max_length=50)
138 page_xmlid = "%s.%s" % (template_module, page_name)
142 imd.get_object_reference(cr, uid, template_module, page_name)
145 _, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
146 page_id = view.copy(cr, uid, template_id, context=context)
147 page = view.browse(cr, uid, page_id, context=context)
149 'arch': page.arch.replace(template, page_xmlid),
153 imd.create(cr, uid, {
155 'module': template_module,
156 'model': 'ir.ui.view',
162 def page_for_name(self, cr, uid, ids, name, module='website', context=None):
164 return '%s.%s' % (module, slugify(name, max_length=50))
166 def page_exists(self, cr, uid, ids, name, module='website', context=None):
167 page = self.page_for_name(cr, uid, ids, name, module=module, context=context)
170 self.pool["ir.model.data"].get_object_reference(cr, uid, module, name)
174 def get_public_user(self, cr, uid, context=None):
175 uid = openerp.SUPERUSER_ID
176 res = self.pool['ir.model.data'].get_object_reference(cr, uid, 'website', 'public_user')
177 return res and res[1] or False
179 @openerp.tools.ormcache(skiparg=3)
180 def _get_languages(self, cr, uid, id, context=None):
181 website = self.browse(cr, uid, id)
182 return [(lg.code, lg.name) for lg in website.language_ids]
184 def get_languages(self, cr, uid, ids, context=None):
185 return self._get_languages(cr, uid, ids[0])
187 def get_current_website(self, cr, uid, context=None):
188 # TODO: Select website, currently hard coded
189 return self.pool['website'].browse(cr, uid, 1, context=context)
191 def preprocess_request(self, cr, uid, ids, request, context=None):
192 # TODO FP: is_website_publisher and editable in context should be removed
193 # for performance reasons (1 query per image to load) but also to be cleaner
194 # I propose to replace this by a group 'base.group_website_publisher' on the
195 # view that requires it.
196 Access = request.registry['ir.model.access']
197 is_website_publisher = Access.check(cr, uid, 'ir.ui.view', 'write', False, context)
199 lang = request.context['lang']
200 is_master_lang = lang == request.website.default_lang_code
202 request.redirect = lambda url: werkzeug.utils.redirect(url_for(url))
203 request.context.update(
204 is_master_lang=is_master_lang,
205 editable=is_website_publisher,
206 translatable=not is_master_lang,
209 def get_template(self, cr, uid, ids, template, context=None):
210 if '.' not in template:
211 template = 'website.%s' % template
212 module, xmlid = template.split('.', 1)
213 model, view_id = request.registry["ir.model.data"].get_object_reference(cr, uid, module, xmlid)
214 return self.pool["ir.ui.view"].browse(cr, uid, view_id, context=context)
216 def _render(self, cr, uid, ids, template, values=None, context=None):
217 user = self.pool.get("res.users")
222 qweb_values = context.copy()
225 qweb_values.update(values)
230 website=request.website,
233 res_company=request.website.company_id,
234 user_id=user.browse(cr, uid, uid),
235 quote_plus=quote_plus,
237 qweb_values.setdefault('editable', False)
239 # in edit mode ir.ui.view will tag nodes
240 context['inherit_branding'] = qweb_values['editable']
242 view = self.get_template(cr, uid, ids, template)
244 if 'main_object' not in qweb_values:
245 qweb_values['main_object'] = view
246 return view.render(qweb_values, engine='website.qweb', context=context)
248 def render(self, cr, uid, ids, template, values=None, status_code=None, context=None):
249 def callback(template, values, context):
250 return self._render(cr, uid, ids, template, values, context)
253 return LazyResponse(callback, status_code=status_code, template=template, values=values, context=context)
255 def pager(self, cr, uid, ids, url, total, page=1, step=30, scope=5, url_args=None, context=None):
257 page_count = int(math.ceil(float(total) / step))
259 page = max(1, min(int(page), page_count))
262 pmin = max(page - int(math.floor(scope/2)), 1)
263 pmax = min(pmin + scope, page_count)
265 if pmax - pmin < scope:
266 pmin = pmax - scope if pmax - scope > 0 else 1
269 _url = "%spage/%s/" % (url, page)
271 _url = "%s?%s" % (_url, urllib.urlencode(url_args))
275 "page_count": page_count,
276 "offset": (page - 1) * step,
278 'url': get_url(page),
282 'url': get_url(pmin),
286 'url': get_url(max(pmin, page - 1)),
287 'num': max(pmin, page - 1)
290 'url': get_url(min(pmax, page + 1)),
291 'num': min(pmax, page + 1)
294 'url': get_url(pmax),
298 {'url': get_url(page), 'num': page}
299 for page in xrange(pmin, pmax+1)
303 def rule_is_enumerable(self, rule):
304 """ Checks that it is possible to generate sensible GET queries for
305 a given rule (if the endpoint matches its own requirements)
307 :type rule: werkzeug.routing.Rule
310 endpoint = rule.endpoint
311 methods = rule.methods or ['GET']
312 converters = rule._converters.values()
316 and endpoint.routing['type'] == 'http'
317 and endpoint.routing['auth'] in ('none', 'public')
318 and endpoint.routing.get('website', False)
319 # preclude combinatorial explosion by only allowing a single converter
320 and len(converters) <= 1
321 # ensure all converters on the rule are able to generate values for
323 and all(hasattr(converter, 'generate') for converter in converters)
324 ) and self.endpoint_is_enumerable(rule)
326 def endpoint_is_enumerable(self, rule):
327 """ Verifies that it's possible to generate a valid url for the rule's
330 :type rule: werkzeug.routing.Rule
333 spec = inspect.getargspec(rule.endpoint.method)
335 # if *args bail the fuck out, only dragons can live there
339 # remove all arguments with a default value from the list
340 defaults_count = len(spec.defaults or []) # spec.defaults can be None
341 # a[:-0] ~ a[:0] ~ [] -> replace defaults_count == 0 by None to get
343 args = spec.args[:(-defaults_count or None)]
345 # params with defaults were removed, leftover allowed are:
346 # * self (technically should be first-parameter-of-instance-method but whatever)
347 # * any parameter mapping to a converter
349 (arg == 'self' or arg in rule._converters)
352 def enumerate_pages(self, cr, uid, ids, query_string=None, context=None):
353 """ Available pages in the website/CMS. This is mostly used for links
354 generation and can be overridden by modules setting up new HTML
355 controllers for dynamic pages (e.g. blog).
357 By default, returns template views marked as pages.
359 :param str query_string: a (user-provided) string, fetches pages
361 :returns: a list of mappings with two keys: ``name`` is the displayable
362 name of the resource (page), ``url`` is the absolute URL
364 :rtype: list({name: str, url: str})
366 router = request.httprequest.app.get_db_router(request.db)
367 # Force enumeration to be performed as public user
368 uid = self.get_public_user(cr, uid, context=context)
369 for rule in router.iter_rules():
370 if not self.rule_is_enumerable(rule):
373 converters = rule._converters
374 filtered = bool(converters)
376 # allow single converter as decided by fp, checked by
378 [(name, converter)] = converters.items()
379 converter_values = converter.generate(
380 request.cr, uid, query=query_string, context=context)
381 generated = ({k: v} for k, v in itertools.izip(
382 itertools.repeat(name), converter_values))
384 # force single iteration for literal urls
387 for values in generated:
388 domain_part, url = rule.build(values, append_unknown=False)
389 page = {'name': url, 'url': url}
391 if not filtered and query_string and not self.page_matches(cr, uid, page, query_string, context=context):
395 def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None):
396 return list(itertools.islice(
397 self.enumerate_pages(cr, uid, ids, query_string=needle, context=context),
400 def page_matches(self, cr, uid, page, needle, context=None):
401 """ Checks that a "page" matches a user-provide search string.
403 The default implementation attempts to perform a non-contiguous
404 substring match of the page's name.
406 :param page: {'name': str, 'url': str}
410 haystack = page['name'].lower()
412 needle = iter(needle.lower())
416 for char in haystack:
417 if char != n: continue
419 n = next(needle, end)
420 # found all characters of needle in haystack in order
426 def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None):
427 step = step and int(step) or 10
428 scope = scope and int(scope) or 5
429 orderby = orderby or "name"
431 get_args = dict(request.httprequest.args or {})
432 model_obj = self.pool[model]
433 relation = model_obj._columns.get(column)._obj
434 relation_obj = self.pool[relation]
436 get_args.setdefault('kanban', "")
437 kanban = get_args.pop('kanban')
438 kanban_url = "?%s&kanban=" % urllib.urlencode(get_args)
441 for col in kanban.split(","):
444 pages[int(col[0])] = int(col[1])
447 for group in model_obj.read_group(cr, uid, domain, ["id", column], groupby=column):
451 relation_id = group[column][0]
452 obj['column_id'] = relation_obj.browse(cr, uid, relation_id)
454 obj['kanban_url'] = kanban_url
455 for k, v in pages.items():
457 obj['kanban_url'] += "%s-%s" % (k, v)
460 number = model_obj.search(cr, uid, group['__domain'], count=True)
461 obj['page_count'] = int(math.ceil(float(number) / step))
462 obj['page'] = pages.get(relation_id) or 1
463 if obj['page'] > obj['page_count']:
464 obj['page'] = obj['page_count']
465 offset = (obj['page']-1) * step
466 obj['page_start'] = max(obj['page'] - int(math.floor((scope-1)/2)), 1)
467 obj['page_end'] = min(obj['page_start'] + (scope-1), obj['page_count'])
470 obj['domain'] = group['__domain']
473 obj['orderby'] = orderby
476 object_ids = model_obj.search(cr, uid, group['__domain'], limit=step, offset=offset, order=orderby)
477 obj['object_ids'] = model_obj.browse(cr, uid, object_ids)
484 'template': template,
486 return request.website._render("website.kanban_contain", values)
488 def kanban_col(self, cr, uid, ids, model, domain, page, template, step, orderby, context=None):
490 model_obj = self.pool[model]
491 domain = safe_eval(domain)
493 offset = (int(page)-1) * step
494 object_ids = model_obj.search(cr, uid, domain, limit=step, offset=offset, order=orderby)
495 object_ids = model_obj.browse(cr, uid, object_ids)
496 for object_id in object_ids:
497 html += request.website._render(template, {'object_id': object_id})
500 class website_menu(osv.osv):
501 _name = "website.menu"
502 _description = "Website Menu"
504 'name': fields.char('Menu', size=64, required=True, translate=True),
505 'url': fields.char('Url', required=True, translate=True),
506 'new_window': fields.boolean('New Window'),
507 'sequence': fields.integer('Sequence'),
508 # TODO: support multiwebsite once done for ir.ui.views
509 'website_id': fields.many2one('website', 'Website'),
510 'parent_id': fields.many2one('website.menu', 'Parent Menu', select=True, ondelete="cascade"),
511 'child_id': fields.one2many('website.menu', 'parent_id', string='Child Menus'),
512 'parent_left': fields.integer('Parent Left', select=True),
513 'parent_right': fields.integer('Parent Right', select=True),
521 _parent_order = 'sequence'
524 # would be better to take a menu_id as argument
525 def get_tree(self, cr, uid, website_id, context=None):
531 new_window=node.new_window,
532 sequence=node.sequence,
533 parent_id=node.parent_id.id,
536 for child in node.child_id:
537 menu_node['children'].append(make_tree(child))
539 menu = self.pool.get('website').browse(cr, uid, website_id, context=context).menu_id
540 return make_tree(menu)
542 def save(self, cr, uid, website_id, data, context=None):
543 def replace_id(old_id, new_id):
544 for menu in data['data']:
545 if menu['id'] == old_id:
547 if menu['parent_id'] == old_id:
548 menu['parent_id'] = new_id
549 to_delete = data['to_delete']
551 self.unlink(cr, uid, to_delete, context=context)
552 for menu in data['data']:
554 if isinstance(mid, str):
555 new_id = self.create(cr, uid, {'name': menu['name']}, context=context)
556 replace_id(mid, new_id)
557 for menu in data['data']:
558 self.write(cr, uid, [menu['id']], menu, context=context)
561 class ir_attachment(osv.osv):
562 _inherit = "ir.attachment"
563 def _website_url_get(self, cr, uid, ids, name, arg, context=None):
565 for attach in self.browse(cr, uid, ids, context=context):
566 if attach.type == 'url':
567 result[attach.id] = attach.url
569 result[attach.id] = urlplus('/website/image', {
570 'model': 'ir.attachment',
578 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char')
581 class res_partner(osv.osv):
582 _inherit = "res.partner"
584 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
585 partner = self.browse(cr, uid, ids[0], context=context)
587 'center': '%s, %s %s, %s' % (partner.street, partner.city, partner.zip, partner.country_id and partner.country_id.name_get()[0][1] or ''),
588 'size': "%sx%s" % (height, width),
592 return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
594 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
595 partner = self.browse(cr, uid, ids[0], context=context)
597 'q': '%s, %s %s, %s' % (partner.street, partner.city, partner.zip, partner.country_id and partner.country_id.name_get()[0][1] or ''),
599 return urlplus('https://maps.google.be/maps' , params)
601 class res_company(osv.osv):
602 _inherit = "res.company"
603 def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None):
604 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
605 return partner and partner.google_map_img(zoom, width, height, context=context) or None
606 def google_map_link(self, cr, uid, ids, zoom=8, context=None):
607 partner = self.browse(cr, openerp.SUPERUSER_ID, ids[0], context=context).partner_id
608 return partner and partner.google_map_link(zoom, context=context) or None
610 class base_language_install(osv.osv_memory):
611 _inherit = "base.language.install"
613 'website_ids': fields.many2many('website', string='Websites to translate'),
616 def default_get(self, cr, uid, fields, context=None):
619 defaults = super(base_language_install, self).default_get(cr, uid, fields, context)
620 website_id = context.get('params', {}).get('website_id')
622 if 'website_ids' not in defaults:
623 defaults['website_ids'] = []
624 defaults['website_ids'].append(website_id)
627 def lang_install(self, cr, uid, ids, context=None):
630 action = super(base_language_install, self).lang_install(cr, uid, ids, context)
631 language_obj = self.browse(cr, uid, ids)[0]
632 website_ids = [website.id for website in language_obj['website_ids']]
633 lang_id = self.pool['res.lang'].search(cr, uid, [('code', '=', language_obj['lang'])])
634 if website_ids and lang_id:
635 data = {'language_ids': [(4, lang_id[0])]}
636 self.pool['website'].write(cr, uid, website_ids, data)
637 params = context.get('params', {})
638 if 'url_return' in params:
640 'url': params['url_return'].replace('[lang]', language_obj['lang']),
641 'type': 'ir.actions.act_url',
646 class SeoMetadata(osv.Model):
647 _name = 'website.seo.metadata'
648 _description = 'SEO metadata'
651 'website_meta_title': fields.char("Website meta title", size=70, translate=True),
652 'website_meta_description': fields.text("Website meta description", size=160, translate=True),
653 'website_meta_keywords': fields.char("Website meta keywords", translate=True),