diff --git a/addons/base_import/__openerp__.py b/addons/base_import/__openerp__.py index e044930a242..79c5be8b25e 100644 --- a/addons/base_import/__openerp__.py +++ b/addons/base_import/__openerp__.py @@ -24,18 +24,16 @@ Re-implement openerp's file import system: 'category': 'Uncategorized', 'website': 'http://www.openerp.com', 'author': 'OpenERP SA', - 'depends': ['web'], + 'depends': ['web', 'select2'], 'installable': True, 'auto_install': True, 'data': [ 'security/ir.model.access.csv', ], 'css': [ - 'static/lib/select2/select2.css', 'static/src/css/import.css', ], 'js': [ - 'static/lib/select2/select2.js', 'static/lib/javascript-state-machine/state-machine.js', 'static/src/js/import.js', ], diff --git a/addons/select2/__init__.py b/addons/select2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/addons/select2/__openerp__.py b/addons/select2/__openerp__.py new file mode 100644 index 00000000000..1616bdf016b --- /dev/null +++ b/addons/select2/__openerp__.py @@ -0,0 +1,18 @@ +{ + 'name': 'Select 2', + 'description': """ +Module bundling the Select 2 javascript library +""", + 'category': 'Hidden/Dependency', + 'website': 'http://www.openerp.com', + 'author': 'OpenERP SA', + 'depends': [], + 'installable': True, + 'auto_install': True, + 'css': [ + 'static/lib/select2/select2.css', + ], + 'js': [ + 'static/lib/select2/select2.js', + ], +} diff --git a/addons/base_import/static/lib/select2/LICENSE b/addons/select2/static/lib/select2/LICENSE similarity index 100% rename from addons/base_import/static/lib/select2/LICENSE rename to addons/select2/static/lib/select2/LICENSE diff --git a/addons/base_import/static/lib/select2/README.md b/addons/select2/static/lib/select2/README.md similarity index 100% rename from addons/base_import/static/lib/select2/README.md rename to addons/select2/static/lib/select2/README.md diff --git a/addons/base_import/static/lib/select2/release.sh b/addons/select2/static/lib/select2/release.sh similarity index 100% rename from addons/base_import/static/lib/select2/release.sh rename to addons/select2/static/lib/select2/release.sh diff --git a/addons/base_import/static/lib/select2/select2.css b/addons/select2/static/lib/select2/select2.css similarity index 100% rename from addons/base_import/static/lib/select2/select2.css rename to addons/select2/static/lib/select2/select2.css diff --git a/addons/base_import/static/lib/select2/select2.js b/addons/select2/static/lib/select2/select2.js similarity index 100% rename from addons/base_import/static/lib/select2/select2.js rename to addons/select2/static/lib/select2/select2.js diff --git a/addons/base_import/static/lib/select2/select2.png b/addons/select2/static/lib/select2/select2.png similarity index 100% rename from addons/base_import/static/lib/select2/select2.png rename to addons/select2/static/lib/select2/select2.png diff --git a/addons/base_import/static/lib/select2/select2x2.png b/addons/select2/static/lib/select2/select2x2.png similarity index 100% rename from addons/base_import/static/lib/select2/select2x2.png rename to addons/select2/static/lib/select2/select2x2.png diff --git a/addons/base_import/static/lib/select2/spinner.gif b/addons/select2/static/lib/select2/spinner.gif similarity index 100% rename from addons/base_import/static/lib/select2/spinner.gif rename to addons/select2/static/lib/select2/spinner.gif diff --git a/addons/website/__openerp__.py b/addons/website/__openerp__.py index d421357e996..bb5d15e47ee 100644 --- a/addons/website/__openerp__.py +++ b/addons/website/__openerp__.py @@ -9,11 +9,12 @@ OpenERP Website CMS """, 'author': 'OpenERP SA', - 'depends': ['web', 'share'], + 'depends': ['web', 'share', 'select2'], 'installable': True, 'data': [ 'data/website_data.xml', 'security/ir.model.access.csv', + 'security/ir_ui_view.xml', 'views/website_templates.xml', 'views/website_views.xml', 'views/snippets.xml', diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 4a8cb1e7528..ba3a1154e54 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -114,13 +114,13 @@ class Website(openerp.addons.web.controllers.main.Home): def snippets(self): return request.website.render('website.snippets') - @website.route('/page/', type='http', auth="public", multilang=True) - def page(self, path, **kwargs): + @website.route('/page/', type='http', auth="public", multilang=True) + def page(self, page, **opt): values = { - 'path': path, + 'path': page, } - return request.website.render(path, values) + return request.website.render(page, values) @website.route('/website/customize_template_toggle', type='json', auth='user') def customize_template_set(self, view_id): @@ -266,12 +266,12 @@ class Website(openerp.addons.web.controllers.main.Home): @website.route('/sitemap', type='http', auth='public', multilang=True) def sitemap(self): - return request.website.render('website.sitemap', {'pages': request.website.list_pages()}) + return request.website.render('website.sitemap', {'pages': request.website.enumerate_pages()}) @website.route('/sitemap.xml', type='http', auth="public") def sitemap_xml(self): body = request.website.render('website.sitemap_xml', { - 'pages': request.website.list_pages() + 'pages': request.website.enumerate_pages() }) return request.make_response(body, [ diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml index d8c8a3df094..5ab70bf76be 100644 --- a/addons/website/data/website_data.xml +++ b/addons/website/data/website_data.xml @@ -40,6 +40,19 @@ + + Display Editor Bar on Website + + + + + + Manage Website and qWeb view + + + + + Website Apps ir.module.module diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 1a29a0c0fad..72fa455dbbf 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import werkzeug.routing import openerp from openerp.osv import orm from openerp.http import request @@ -13,6 +14,7 @@ class ir_http(orm.AbstractModel): return dict( super(ir_http, self)._get_converters(), model=ModelConverter, + page=PageConverter, ) def _auth_method_public(self): @@ -26,12 +28,37 @@ class ir_http(orm.AbstractModel): class ModelConverter(ir.ir_http.ModelConverter): def __init__(self, url_map, model=False): super(ModelConverter, self).__init__(url_map, model) - self.regex = r'[A-Za-z0-9-_]*?(\d+)' + self.regex = r'(?:[A-Za-z0-9-_]+?-)?(\d+)(?=$|/)' def to_url(self, value): - [(_, name)] = value.name_get() - return "%s-%d" % (slugify(name), value.id) + if isinstance(value, orm.browse_record): + [(id, name)] = value.name_get() + else: + # assume name_search result tuple + id, name = value + return "%s-%d" % (slugify(name), id) - def generate(self): - for id in request.registry[self.model].search(request.cr, request.uid, [], context=request.context): - yield request.registry[self.model].browse(request.cr, request.uid, id, context=request.context) + def generate(self, query=None): + return request.registry[self.model].name_search( + request.cr, request.uid, + name=query or '', + context=request.context) + +class PageConverter(werkzeug.routing.PathConverter): + """ Only point of this converter is to bundle pages enumeration logic + + Sads got: no way to get the view's human-readable name even if one exists + """ + def generate(self, query=None): + View = request.registry['ir.ui.view'] + views = View.search_read( + request.cr, request.uid, [['page', '=', True]], + fields=[], order='name', context=request.context) + xids = View.get_external_id( + request.cr, request.uid, [view['id'] for view in views], + context=request.context) + + for view in views: + xid = xids[view['id']] + if xid and (not query or query in xid): + yield xid diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 19beffc93cc..60e680aaefc 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -30,7 +30,6 @@ def route(routes, *route_args, **route_kwargs): new_routes = routes if isinstance(routes, list) else [routes] f.cms = True f.multilang = route_kwargs.get('multilang', False) - f.methods = route_kwargs.pop('methods', None) if f.multilang: route_kwargs.pop('multilang') for r in list(new_routes): @@ -50,8 +49,6 @@ def route(routes, *route_args, **route_kwargs): if not lang_ok: return request.not_found() request.website.preprocess_request(request) - if f.methods and request.httprequest.method not in f.methods: - return werkzeug.exceptions.MethodNotAllowed(valid_methods=f.methods) try: return f(*args, **kwargs) except Exception, err: @@ -136,11 +133,9 @@ class website(osv.osv): is_public_user = request.uid == self.get_public_user(cr, uid, context).id - try: - self.pool.get("ir.ui.view").check_access_rights(request.cr, request.uid, 'write') - editable = True - except: - editable = False + user = self.pool['res.users'].browse(cr, uid, uid, context=context) + website_publisher_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_website_publisher')[1] + is_website_publisher = website_publisher_id in [g.id for g in user.groups_id] # Select current language if hasattr(request, 'route_lang'): @@ -158,8 +153,7 @@ class website(osv.osv): 'multilang': request.multilang, 'is_public_user': is_public_user, 'is_master_lang': is_master_lang, - 'has_access_write': True, - 'editable': editable, + 'editable': is_website_publisher, 'translatable': not is_public_user and not is_master_lang and request.multilang, }) @@ -292,16 +286,18 @@ class website(osv.osv): """ endpoint = rule.endpoint methods = rule.methods or ['GET'] + converters = rule._converters.values() return ( 'GET' in methods and endpoint.exposed == 'http' and endpoint.auth in ('none', 'public') and getattr(endpoint, 'cms', False) + # preclude combinatorial explosion by only allowing a single converter + and len(converters) <= 1 # ensure all converters on the rule are able to generate values for # themselves - and all(hasattr(converter, 'generate') - for converter in rule._converters.itervalues()) + and all(hasattr(converter, 'generate') for converter in converters) ) and self.endpoint_is_enumerable(rule) def endpoint_is_enumerable(self, rule): @@ -322,9 +318,8 @@ class website(osv.osv): # If this is ever ported to py3, use signatures, it doesn't suck as much spec = inspect.getargspec(undecorated_func) - # if *args or **kwargs, just bail the fuck out, only dragons can - # live there - if spec.varargs or spec.keywords: + # if *args bail the fuck out, only dragons can live there + if spec.varargs: return False # remove all arguments with a default value from the list @@ -340,45 +335,77 @@ class website(osv.osv): (arg == 'self' or arg in rule._converters) for arg in args) - def list_pages(self, cr, uid, ids, context=None): + def enumerate_pages(self, cr, uid, ids, query_string=None, context=None): """ Available pages in the website/CMS. This is mostly used for links generation and can be overridden by modules setting up new HTML controllers for dynamic pages (e.g. blog). By default, returns template views marked as pages. + :param str query_string: a (user-provided) string, fetches pages + matching the string :returns: a list of mappings with two keys: ``name`` is the displayable name of the resource (page), ``url`` is the absolute URL of the same. :rtype: list({name: str, url: str}) """ - # FIXME: possibility to add custom converters without editing server - # would allow the creation of a pages converter generating page - # urls on its own - View = self.pool['ir.ui.view'] - views = View.search_read(cr, uid, [['page', '=', True]], - fields=['name'], order='name', context=context) - xids = View.get_external_id(cr, uid, [view['id'] for view in views], context=context) - for view in views: - if xids[view['id']]: - yield { - 'name': view['name'], - 'url': '/page/' + xids[view['id']], - } - router = request.httprequest.app.get_db_router(request.db) for rule in router.iter_rules(): if not self.rule_is_enumerable(rule): continue - generated = map(dict, itertools.product(*( - itertools.izip(itertools.repeat(name), converter.generate()) - for name, converter in rule._converters.iteritems() - ))) + converters = rule._converters + filtered = bool(converters) + if converters: + # allow single converter as decided by fp, checked by + # rule_is_enumerable + [(name, converter)] = converters.items() + # FIXME: perform generation as public user + generated = ({k: v} for k, v in itertools.izip( + itertools.repeat(name), + converter.generate(query=query_string))) + else: + # force single iteration for literal urls + generated = [{}] for values in generated: domain_part, url = rule.build(values, append_unknown=False) - yield {'name': url, 'url': url } + page = {'name': url, 'url': url} + + if not filtered and query_string and not self.page_matches(cr, uid, page, query_string, context=context): + continue + yield page + + def search_pages(self, cr, uid, ids, needle=None, limit=None, context=None): + return list(itertools.islice( + self.enumerate_pages(cr, uid, ids, query_string=needle, context=context), + limit)) + + def page_matches(self, cr, uid, page, needle, context=None): + """ Checks that a "page" matches a user-provide search string. + + The default implementation attempts to perform a non-contiguous + substring match of the page's name. + + :param page: {'name': str, 'url': str} + :param needle: str + :rtype: bool + """ + haystack = page['name'].lower() + + needle = iter(needle.lower()) + n = next(needle) + end = object() + + for char in haystack: + if char != n: continue + + n = next(needle, end) + # found all characters of needle in haystack in order + if n is end: + return True + + return False def kanban(self, cr, uid, ids, model, domain, column, template, step=None, scope=None, orderby=None, context=None): step = step and int(step) or 10 diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index 8304e9861ea..c5ccffd697e 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -16,3 +16,4 @@ access_website_qweb_field_many2one,access_website_qweb_field_many2one,model_webs access_website_qweb_field_html,access_website_qweb_field_html,model_website_qweb_field_html,,0,0,0,0 access_website_qweb_field_image,access_website_qweb_field_image,model_website_qweb_field_image,,0,0,0,0 access_website_qweb_field_monetary,access_website_qweb_field_monetary,model_website_qweb_field_monetary,,0,0,0,0 +access_website_ir_ui_view,access_website_ir_ui_view,model_ir_ui_view,base.group_website_designer,1,1,1,1 diff --git a/addons/website/security/ir_ui_view.xml b/addons/website/security/ir_ui_view.xml new file mode 100644 index 00000000000..0745028b7d9 --- /dev/null +++ b/addons/website/security/ir_ui_view.xml @@ -0,0 +1,25 @@ + + + + + website_designer: Manage Website and qWeb view + + [('type', '=', 'qweb')] + + + + + + + + website_designer: global view + + [('type', '!=', 'qweb')] + + + + + + + + \ No newline at end of file diff --git a/addons/website/static/lib/select2-bootstrap-css/README.md b/addons/website/static/lib/select2-bootstrap-css/README.md new file mode 100644 index 00000000000..c4d8fbdb675 --- /dev/null +++ b/addons/website/static/lib/select2-bootstrap-css/README.md @@ -0,0 +1,16 @@ +CSS for making Select2 fit in with Bootstrap a bit better, +**updated for Bootstrap v3.0.0**. + +http://fk.github.io/select2-bootstrap-css/ + +Tested with Bootstrap 3.0.0 and Select2 3.3.2, 3.4.1 and 3.4.2 and in latest Chrome, Safari, Firefox, Opera (Mac) and IE8-IE10. + +Known issues: + + * "Loading Remote Data" and "Infinite Scroll" not addressed yet, the loading indicator position probably needs some adjustments + * IE8/IE9/IE10, Firefox: Select2 in "Bootstrap input group with radio/checkbox addon" and .input-lg misses 1px in height (IE9/IE10, Firefox behave the same for Bootstrap 3's "input group sizing"-demo at http://getbootstrap.com/components/#input-groups-sizing) + * IE9/IE10: Select2 in "Bootstrap input group with button addon" (no height modifier, i. e. .input-sm, .input-lg) also misses 1px in height (bug _not_ inherited from Bootstrap 3) + * box-shadow for .select2-search input do not fit Bootstrap's defaults + * the Select2 dropdown could inherit look-and-feel from Bootstrap dropdowns and/or could honor Bootstrap height sizing classes + * border-radii for opened Select2 dropdowns could consistently be set to be … round ;-) + * checkboxes and radio-buttons in "Bootstrap input groups" could be vertically aligned to the top (instead of center) if combined with a multi Select2 to address variable height of the Select2 container diff --git a/addons/website/static/lib/select2-bootstrap-css/select2-bootstrap.css b/addons/website/static/lib/select2-bootstrap-css/select2-bootstrap.css new file mode 100644 index 00000000000..5497890f6fe --- /dev/null +++ b/addons/website/static/lib/select2-bootstrap-css/select2-bootstrap.css @@ -0,0 +1,420 @@ +/** + * Select2 Bootstrap CSS + * Compatible with Select2 3.3.2, 3.4.1, 3.4.2 and Twitter Bootstrap 3.0.0 + * MIT License + */ +/** + * Reset Bootstrap 3 .form-control styles which - if applied to the + * original -