[MERGE] Sync with website-al

bzr revid: tde@openerp.com-20131119160204-1yvqbg4vgpc0bkqs
This commit is contained in:
Thibault Delavallée
2013-11-19 17:02:04 +01:00
39 changed files with 891 additions and 228 deletions
+1 -3
View File
@@ -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',
],
View File
+18
View File
@@ -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',
],
}

Before

Width:  |  Height:  |  Size: 613 B

After

Width:  |  Height:  |  Size: 613 B

Before

Width:  |  Height:  |  Size: 845 B

After

Width:  |  Height:  |  Size: 845 B

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+2 -1
View File
@@ -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',
+6 -6
View File
@@ -114,13 +114,13 @@ class Website(openerp.addons.web.controllers.main.Home):
def snippets(self):
return request.website.render('website.snippets')
@website.route('/page/<path:path>', type='http', auth="public", multilang=True)
def page(self, path, **kwargs):
@website.route('/page/<page: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, [
+13
View File
@@ -40,6 +40,19 @@
<field name="users" eval="[(4, ref('public_user'))]"/>
</record>
<record id="base.group_website_publisher" model="res.groups">
<field name="name">Display Editor Bar on Website</field>
<field name="category_id" ref="base.module_category_website"/>
</record>
<!-- Access rules to edit ir.ui.view with qWeb type -->
<record id="base.group_website_designer" model="res.groups">
<field name="name">Manage Website and qWeb view</field>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
<field name="implied_ids" eval="[(4, ref('base.group_website_publisher'))]"/>
<field name="category_id" ref="base.module_category_website"/>
</record>
<record id="action_module_website" model="ir.actions.act_window">
<field name="name">Website Apps</field>
<field name="res_model">ir.module.module</field>
+33 -6
View File
@@ -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
+62 -35
View File
@@ -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
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
16 access_website_qweb_field_html access_website_qweb_field_html model_website_qweb_field_html 0 0 0 0
17 access_website_qweb_field_image access_website_qweb_field_image model_website_qweb_field_image 0 0 0 0
18 access_website_qweb_field_monetary access_website_qweb_field_monetary model_website_qweb_field_monetary 0 0 0 0
19 access_website_ir_ui_view access_website_ir_ui_view model_ir_ui_view base.group_website_designer 1 1 1 1
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="website_designer_edit_qweb" model="ir.rule">
<field name="name">website_designer: Manage Website and qWeb view</field>
<field name="model_id" ref="base.model_ir_ui_view"/>
<field name="domain_force">[('type', '=', 'qweb')]</field>
<field name="groups" eval="[(4, ref('base.group_website_designer'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<record id="website_designer_view" model="ir.rule">
<field name="name">website_designer: global view</field>
<field name="model_id" ref="base.model_ir_ui_view"/>
<field name="domain_force">[('type', '!=', 'qweb')]</field>
<field name="groups" eval="[(4, ref('base.group_website_designer'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
</data>
</openerp>
@@ -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
@@ -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 <select>-element the Select2-plugin may be run against -
* are copied to the .select2-container.
*/
.select2-container.form-control {
background: transparent;
border: none;
margin: 0;
padding: 0;
}
/**
* Adjust Select2 inputs to fit Bootstrap 3 default .form-control appearance.
*/
.select2-container .select2-choices .select2-search-field input,
.select2-container .select2-choice,
.select2-container .select2-choices {
background: none;
padding: 0;
border-color: #cccccc;
border-radius: 4px;
color: #555555;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background-color: white;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.select2-search input {
border-color: #cccccc;
border-radius: 4px;
color: #555555;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background-color: white;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.select2-container .select2-choices .select2-search-field input {
-webkit-box-shadow: none;
box-shadow: none;
}
/**
* Adjust Select2 input heights to match the Bootstrap default.
*/
.select2-container .select2-choice {
height: 34px;
line-height: 1.42857;
}
/**
* Address Multi Select2's height which - depending on how many elements have been selected -
* may grown higher than their initial size.
*/
.select2-container.select2-container-multi.form-control {
height: auto;
}
/**
* Address Bootstrap 3 control sizing classes
* @see http://getbootstrap.com/css/#forms-control-sizes
*/
.select2-container.input-sm .select2-choice,
.input-group-sm .select2-container .select2-choice {
height: 30px;
line-height: 1.5;
border-radius: 3px;
}
.select2-container.input-lg .select2-choice,
.input-group-lg .select2-container .select2-choice {
height: 45px;
line-height: 1.33;
border-radius: 6px;
}
.select2-container-multi .select2-choices .select2-search-field input {
height: 32px;
}
.select2-container-multi.input-sm .select2-choices .select2-search-field input,
.input-group-sm .select2-container-multi .select2-choices .select2-search-field input {
height: 28px;
}
.select2-container-multi.input-lg .select2-choices .select2-search-field input,
.input-group-lg .select2-container-multi .select2-choices .select2-search-field input {
height: 43px;
}
/**
* Adjust height and line-height for .select2-search-field amd multi-select Select2 widgets.
*
* 1. Class repetition to address missing .select2-chosen in Select2 < 3.3.2.
*/
.select2-container-multi .select2-choices .select2-search-field input {
margin: 0;
}
.select2-chosen,
.select2-choice > span:first-child,
.select2-container .select2-choices .select2-search-field input {
padding: 6px 12px;
}
.input-sm .select2-chosen,
.input-group-sm .select2-chosen,
.input-sm .select2-choice > span:first-child,
.input-group-sm .select2-choice > span:first-child,
.input-sm .select2-choices .select2-search-field input,
.input-group-sm .select2-choices .select2-search-field input {
padding: 5px 10px;
}
.input-lg .select2-chosen,
.input-group-lg .select2-chosen,
.input-lg .select2-choice > span:first-child,
.input-group-lg .select2-choice > span:first-child,
.input-lg .select2-choices .select2-search-field input,
.input-group-lg .select2-choices .select2-search-field input {
padding: 10px 16px;
}
.select2-container-multi .select2-choices .select2-search-choice {
margin-top: 5px;
margin-bottom: 3px;
}
.select2-container-multi.input-sm .select2-choices .select2-search-choice,
.input-group-sm .select2-container-multi .select2-choices .select2-search-choice {
margin-top: 3px;
margin-bottom: 2px;
}
.select2-container-multi.input-lg .select2-choices .select2-search-choice,
.input-group-lg .select2-container-multi .select2-choices .select2-search-choice {
line-height: 24px;
}
/**
* Adjust the single Select2's dropdown arrow button appearance.
*
* 1. For Select2 v.3.3.2.
*/
.select2-container .select2-choice .select2-arrow,
.select2-container .select2-choice div {
border-left: 1px solid #cccccc;
background: none;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.select2-dropdown-open .select2-choice .select2-arrow,
.select2-dropdown-open .select2-choice div {
border-left-color: transparent;
background: none;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
/**
* Adjust the dropdown arrow button icon position for the single-select Select2 elements
* to make it line up vertically now that we increased the height of .select2-container.
*
* 1. Class repetition to address missing .select2-chosen in Select2 v.3.3.2.
*/
.select2-container .select2-choice .select2-arrow b,
.select2-container .select2-choice div b {
background-position: 0 3px;
}
.select2-dropdown-open .select2-choice .select2-arrow b,
.select2-dropdown-open .select2-choice div b {
background-position: -18px 3px;
}
.select2-container.input-sm .select2-choice .select2-arrow b,
.input-group-sm .select2-container .select2-choice .select2-arrow b,
.select2-container.input-sm .select2-choice div b,
.input-group-sm .select2-container .select2-choice div b {
background-position: 0 1px;
}
.select2-dropdown-open.input-sm .select2-choice .select2-arrow b,
.input-group-sm .select2-dropdown-open .select2-choice .select2-arrow b,
.select2-dropdown-open.input-sm .select2-choice div b,
.input-group-sm .select2-dropdown-open .select2-choice div b {
background-position: -18px 1px;
}
.select2-container.input-lg .select2-choice .select2-arrow b,
.input-group-lg .select2-container .select2-choice .select2-arrow b,
.select2-container.input-lg .select2-choice div b,
.input-group-lg .select2-container .select2-choice div b {
background-position: 0 9px;
}
.select2-dropdown-open.input-lg .select2-choice .select2-arrow b,
.input-group-lg .select2-dropdown-open .select2-choice .select2-arrow b,
.select2-dropdown-open.input-lg .select2-choice div b,
.input-group-lg .select2-dropdown-open .select2-choice div b {
background-position: -18px 9px;
}
/**
* Address Bootstrap's validation states and change Select2's border colors and focus states.
* Apply .has-warning, .has-danger or .has-succes to #select2-drop to match Bootstraps' colors.
*/
.has-warning .select2-choice,
.has-warning .select2-choices {
border-color: #c09853;
}
.has-warning .select2-container-active .select2-choice,
.has-warning .select2-container-multi.select2-container-active .select2-choices {
border-color: #a47e3c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
}
.has-warning.select2-drop-active {
border-color: #a47e3c;
}
.has-warning.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #a47e3c;
}
.has-error .select2-choice,
.has-error .select2-choices {
border-color: #b94a48;
}
.has-error .select2-container-active .select2-choice,
.has-error .select2-container-multi.select2-container-active .select2-choices {
border-color: #953b39;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
}
.has-error.select2-drop-active {
border-color: #953b39;
}
.has-error.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #953b39;
}
.has-success .select2-choice,
.has-success .select2-choices {
border-color: #468847;
}
.has-success .select2-container-active .select2-choice,
.has-success .select2-container-multi.select2-container-active .select2-choices {
border-color: #356635;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
}
.has-success.select2-drop-active {
border-color: #356635;
}
.has-success.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #356635;
}
/**
* Make Select2's active-styles - applied to .select2-container when the widget receives focus -
* fit Bootstrap 3's .form-element:focus appearance.
*/
.select2-container-active .select2-choice,
.select2-container-multi.select2-container-active .select2-choices {
border-color: #66afe9;
outline: none;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
.select2-drop-active {
border-color: #66afe9;
}
.select2-drop-auto-width,
.select2-drop.select2-drop-above.select2-drop-active {
border-top-color: #66afe9;
}
/**
* Select2 widgets in Bootstrap Input Groups
*
* When Select2 widgets are combined with other elements using Bootstrap 3's
* "Input Group" component, we don't want specific edges of the Select2 container
* to have a border-radius.
*
* In Bootstrap 2, input groups required a markup where these style adjustments
* could be bound to a CSS-class identifying if the additional elements are appended,
* prepended or both.
*
* Bootstrap 3 doesn't rely on these classes anymore, so we have to use our own.
* Use .select2-bootstrap-prepend and .select2-bootstrap-append on a Bootstrap 3 .input-group
* to let the contained Select2 widget know which edges should not be rounded as they are
* directly followed by another element.
*
* @see http://getbootstrap.com/components/#input-groups
*/
.input-group.select2-bootstrap-prepend [class^="select2-choice"] {
border-bottom-left-radius: 0 !important;
border-top-left-radius: 0 !important;
}
.input-group.select2-bootstrap-append [class^="select2-choice"] {
border-bottom-right-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.select2-dropdown-open [class^="select2-choice"] {
border-bottom-right-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
border-top-right-radius: 0 !important;
border-top-left-radius: 0 !important;
}
/**
* Adjust Select2's choices hover and selected styles to match Bootstrap 3's default dropdown styles.
*/
.select2-results .select2-highlighted {
color: white;
background-color: #428bca;
}
/**
* Adjust alignment of Bootstrap 3 buttons in Bootstrap 3 Input Groups to address
* Multi Select2's height which - depending on how many elements have been selected -
* may grown higher than their initial size.
*/
.select2-bootstrap-append .select2-container-multiple,
.select2-bootstrap-append .input-group-btn,
.select2-bootstrap-append .input-group-btn .btn,
.select2-bootstrap-prepend .select2-container-multiple,
.select2-bootstrap-prepend .input-group-btn,
.select2-bootstrap-prepend .input-group-btn .btn {
vertical-align: top;
}
/**
* Make Multi Select2's choices match Bootstrap 3's default button styles.
*/
.select2-container-multi .select2-choices .select2-search-choice {
color: #555555;
background: white;
border-color: #cccccc;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: none;
box-shadow: none;
}
.select2-container-multi .select2-choices .select2-search-choice-focus {
background: #ebebeb;
border-color: #adadad;
color: #333333;
-webkit-box-shadow: none;
box-shadow: none;
}
/**
* Address Multi Select2's choice close-button vertical alignment.
*/
.select2-search-choice-close {
margin-top: -7px;
top: 50%;
}
/**
* Adjust the single Select2's clear button position (used to reset the select box
* back to the placeholder value and visible once a selection is made
* activated by Select2's "allowClear" option).
*/
.select2-container .select2-choice abbr {
top: 50%;
}
/**
* Adjust "no results" and "selection limit" messages to make use
* of Bootstrap 3's default "Alert" style.
*
* @see http://getbootstrap.com/components/#alerts-default
*/
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-selection-limit {
background-color: #fcf8e3;
color: #c09853;
}
/**
* Address disabled Select2 styles.
*
* 1. For Select2 v.3.3.2.
* 2. Revert border-left:0 inherited from Select2's CSS to prevent the arrow
* from jumping when switching from disabled to enabled state and vice versa.
*/
.select2-container.select2-container-disabled .select2-choice,
.select2-container.select2-container-disabled .select2-choices {
cursor: not-allowed;
background-color: #eeeeee;
border-color: #cccccc;
}
.select2-container.select2-container-disabled .select2-choice .select2-arrow,
.select2-container.select2-container-disabled .select2-choice div,
.select2-container.select2-container-disabled .select2-choices .select2-arrow,
.select2-container.select2-container-disabled .select2-choices div {
background-color: transparent;
border-left: 1px solid transparent;
/* 2 */
}
+73 -53
View File
@@ -2,7 +2,7 @@
'use strict';
var website = openerp.website;
// $.fn.data automatically parses value, '0'|'1' -> 0|1
var _t = openerp._t;
website.add_template_file('/website/static/src/xml/website.editor.xml');
website.dom_ready.done(function () {
@@ -14,12 +14,7 @@
$(document).on('click', 'a.js_link2post', function (ev) {
ev.preventDefault();
var form = document.createElement('form');
form.action = this.pathname; // restrict to same origin
form.method = 'POST';
// TODO: support this.search as form input fields
$(this).append(form);
form.submit();
website.form(this.pathname, 'POST');
});
$(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
@@ -90,7 +85,7 @@
// display button
var $el = $(previousSelection = selected.$);
var $btn = $('<button type="button" class="btn btn-primary" contenteditable="false">Edit</button>')
var $btn = $('<button type="button" class="btn btn-primary image-edit-button" contenteditable="false">Edit</button>')
.insertAfter($el)
.click(function (e) {
e.preventDefault();
@@ -105,6 +100,11 @@
left: $el.width() / 2 + position.left - $btn.outerWidth() / 2,
});
});
editor.on('destroy', function (evt) {
if (previousSelection) {
$('.image-edit-button').remove();
}
});
//noinspection JSValidateTypes
editor.addCommand('link', {
@@ -513,16 +513,23 @@
if (!_.isArray(failed)) { failed = [failed]; }
_(failed).each(function (failure) {
var html = failure.error.exception_type === "except_osv";
if (html) {
var msg = $("<div/>").text(failure.error.message).html();
var data = msg.substring(3,msg.length-2).split(/', u'/);
failure.error.message = '<b>' + data[0] + '</b><br/>' + data[1];
}
$(root).find('.' + failure.id)
.removeClass(failure.id)
.popover({
html: html,
trigger: 'hover',
content: failure.error.message,
placement: 'auto top',
})
// Force-show popovers so users will notice them.
.popover('show');
})
});
});
});
},
@@ -694,9 +701,11 @@
self.setup_editables(root);
// disable firefox's broken table resizing thing
document.execCommand("enableObjectResizing", false, "false");
document.execCommand("enableInlineTableEditing", false, "false");
try {
// disable firefox's broken table resizing thing
document.execCommand("enableObjectResizing", false, "false");
document.execCommand("enableInlineTableEditing", false, "false");
} catch (e) {}
self.trigger('rte:ready');
def.resolve();
@@ -848,7 +857,7 @@
website.editor.LinkDialog = website.editor.Dialog.extend({
template: 'website.editor.dialog.link',
events: _.extend({}, website.editor.Dialog.prototype.events, {
'change .url-source': function (e) { this.changed($(e.target)); },
'change :input.url-source': function (e) { this.changed($(e.target)); },
'mousedown': function (e) {
var $target = $(e.target).closest('.list-group-item');
if (!$target.length || $target.hasClass('active')) {
@@ -856,7 +865,7 @@
return;
}
this.changed($target.find('.url-source'));
this.changed($target.find('.url-source').filter(':input'));
},
'click button.remove': 'remove_link',
'change input#link-text': function (e) {
@@ -865,22 +874,38 @@
}),
init: function (editor) {
this._super(editor);
// url -> name mapping for existing pages
this.pages = Object.create(null);
this.text = null;
// Store last-performed request to be able to cancel/abort it.
this.req = null;
},
start: function () {
var self = this;
return $.when(
this.fetch_pages().done(this.proxy('fill_pages')),
this._super()
).done(function () {
self.bind_data();
this.$('#link-page').select2({
minimumInputLength: 3,
placeholder: _t("New or existing page"),
query: function (q) {
// FIXME: out-of-order, abort
self.fetch_pages(q.term).then(function (results) {
var rs = _.map(results, function (r) {
return { id: r.url, text: r.name, };
});
rs.push({
create: true,
id: q.term,
text: _.str.sprintf(_t("Create page '%s'"), q.term),
});
q.callback({
more: false,
results: rs
});
});
},
});
return this._super().then(this.proxy('bind_data'));
},
save: function () {
var self = this, _super = this._super.bind(this);
var $e = this.$('.list-group-item.active .url-source');
var $e = this.$('.list-group-item.active .url-source').filter(':input');
var val = $e.val();
if (!val || !$e[0].checkValidity()) {
// FIXME: error message
@@ -892,15 +917,18 @@
var done = $.when();
if ($e.hasClass('email-address')) {
this.make_link('mailto:' + val, false, val);
} else if ($e.hasClass('existing')) {
self.make_link(val, false, this.pages[val]);
} else if ($e.hasClass('pages')) {
// Create the page, get the URL back
done = $.get(_.str.sprintf(
'/pagenew/%s?noredirect', encodeURI(val)))
.then(function (response) {
self.make_link(response, false, val);
});
} else if ($e.hasClass('page')) {
var data = $e.select2('data');
if (!data.create) {
self.make_link(data.id, false, data.text);
} else {
// Create the page, get the URL back
done = $.get(_.str.sprintf(
'/pagenew/%s?noredirect', encodeURI(data.id)))
.then(function (response) {
self.make_link(response, false, data.id);
});
}
} else {
this.make_link(val, this.$('input.window-new').prop('checked'));
}
@@ -923,13 +951,6 @@
var match, $control;
if ((match = /mailto:(.+)/.exec(href))) {
$control = this.$('input.email-address').val(match[1]);
} else if (href in this.pages) {
$control = this.$('select.existing').val(href);
} else if ((match = /\/page\/(.+)/.exec(href))) {
var actual_href = '/page/website.' + match[1];
if (actual_href in this.pages) {
$control = this.$('select.existing').val(actual_href);
}
}
if (!$control) {
$control = this.$('input.url').val(href);
@@ -941,30 +962,28 @@
this.$('input.window-new').prop('checked', new_window);
},
changed: function ($e) {
this.$('.url-source').not($e).val('');
this.$('.url-source').filter(':input').not($e).val('')
.filter(function () { return !!$(this).data('select2'); })
.select2('data', null);
$e.closest('.list-group-item')
.addClass('active')
.siblings().removeClass('active')
.addBack().removeClass('has-error');
},
fetch_pages: function () {
return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
fetch_pages: function (term) {
var self = this;
if (this.req) { this.req.abort(); }
return this.req = openerp.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website',
method: 'list_pages',
args: [null],
method: 'search_pages',
args: [null, term],
kwargs: {
limit: 9,
context: website.get_context()
},
});
},
fill_pages: function (results) {
var self = this;
var pages = this.$('select.existing')[0];
_(results).each(function (result) {
self.pages[result.url] = result.name;
pages.options[pages.options.length] =
new Option(result.name, result.url);
}).done(function () {
// request completed successfully -> unstore it
self.req = null;
});
},
});
@@ -1185,6 +1204,7 @@
if (!(element = this.element)) {
element = editor.document.createElement('img');
element.addClass('img');
element.addClass('img-responsive');
// focus event handler interactions between bootstrap (modal)
// and ckeditor (RTE) lead to blowing the stack in Safari and
// Chrome (but not FF) when this is done synchronously =>
+41
View File
@@ -13,6 +13,31 @@
}, dict);
};
website.parseQS = function (qs) {
var match,
params = {},
pl = /\+/g, // Regex for replacing addition symbol with a space
search = /([^&=]+)=?([^&]*)/g;
while ((match = search.exec(qs))) {
var name = decodeURIComponent(match[1].replace(pl, " "));
var value = decodeURIComponent(match[2].replace(pl, " "));
params[name] = value;
}
return params;
};
var parsedSearch;
website.parseSearch = function () {
if (!parsedSearch) {
parsedSearch = website.parseQS(window.location.search.substring(1));
}
return parsedSearch;
};
website.parseHash = function () {
return website.parseQS(window.location.hash.substring(1));
};
/* ----- TEMPLATE LOADING ---- */
var templates_def = $.Deferred().resolve();
website.add_template_file = function(template) {
@@ -211,6 +236,7 @@
if (field.is('input[type="text"], select')) {
field.keypress(function (e) {
if (e.which == 13) {
e.preventDefault();
dialog.find('.btn-primary').trigger('click');
}
});
@@ -218,6 +244,21 @@
return def;
};
website.form = function (url, method, params) {
var form = document.createElement('form');
form.action = url;
form.method = method;
_.each(params, function (v, k) {
var param = document.createElement('input');
param.type = 'hidden';
param.name = k;
param.value = v;
form.appendChild(param);
});
document.body.appendChild(form);
form.submit();
};
dom_ready.then(function () {
/* ----- BOOTSTRAP STUFF ---- */
+5 -6
View File
@@ -155,7 +155,6 @@
template: 'website.menu.dialog.add',
init: function (editor, data) {
this.data = data;
this.update_mode = !!this.data;
return this._super.apply(this, arguments);
},
start: function () {
@@ -165,11 +164,11 @@
self.bind_data(self.data.name, self.data.url, self.data.new_window);
}
var $link_text = self.$('#link-text').focus();
self.$('#link-existing').change(function () {
if (!$link_text.val()) {
$link_text.val($(this).find('option:selected').text());
$link_text.focus();
}
self.$('#link-page').change(function (e) {
if ($link_text.val()) { return; }
var data = $(this).select2('data');
$link_text.val(data.create ? data.id : data.text);
$link_text.focus();
});
});
globalEditor.trigger('tour:new_menu_entry_dialog_ready');
@@ -6,8 +6,9 @@
var render = website.tour.render;
website.EditorBannerTour = website.EditorTour.extend({
id: 'add_banner_tour',
id: 'banner-tutorial',
name: "Insert a banner",
startPath: '/page/website.homepage',
init: function (editor) {
var self = this;
var $body = $(document.body);
@@ -132,28 +133,14 @@
var showTutorialsIndex = this.indexOfStep('show-tutorials');
return (currentStepIndex === secondPartIndex || currentStepIndex === showTutorialsIndex) && !this.tour.ended();
},
canResume: function () {
continueTour: function () {
return this.startOfPart2() || this._super();
},
});
website.EditorBar.include({
start: function () {
var menu = $('#help-menu');
var bannerTour = new website.EditorBannerTour(this);
var $menuItem = $($.parseHTML('<li><a href="#">'+bannerTour.name+'</a></li>'));
$menuItem.click(function () {
if (url.pathname !== '/' && url.pathname !== '/page/website.homepage') {
window.location.replace('/page/website.homepage?banner-tutorial=true');
}
bannerTour.reset();
bannerTour.start();
});
menu.append($menuItem);
var url = new website.UrlParser(window.location.href);
if (url.search.indexOf('?banner-tutorial=true') === 0 || bannerTour.startOfPart2()) {
bannerTour.start();
}
this.registerTour(new website.EditorBannerTour(this));
return this._super();
},
});
@@ -6,7 +6,7 @@
var render = website.tour.render;
website.InstallAppTour = website.EditorTour.extend({
id: 'install_app',
id: 'install-app',
name: "Install a new App",
init: function (editor) {
var self = this;
@@ -49,14 +49,7 @@
website.EditorBar.include({
start: function () {
var menu = $('#help-menu');
var installAppTour = new website.InstallAppTour(this);
var $menuItem = $($.parseHTML('<li><a href="#">'+installAppTour.name+'</a></li>'));
$menuItem.click(function () {
installAppTour.reset();
installAppTour.start();
});
menu.append($menuItem);
this.registerTour(new website.InstallAppTour(this));
return this._super();
},
});
+37 -17
View File
@@ -24,22 +24,6 @@
step.title = website.tour.render('website.tour_popover_title', { title: step.title });
return step;
}));
// TODO: Disabled until properly implemented
// this.monkeyPatchTour();
},
monkeyPatchTour: function () {
var self = this;
// showStep should wait for 'element' to appear instead of moving to the next step
self.tour.showStep = function (i) {
var step = self.tour.getStep(i);
return (function proceed () {
if (step.orphan || $(step.element).length > 0) {
return Tour.prototype.showStep.call(self.tour, i);
} else {
setTimeout(proceed, 50);
}
}());
};
},
reset: function () {
this.tourStorage.removeItem(this.id+'_current_step');
@@ -53,7 +37,7 @@
}
},
canResume: function () {
return (this.currentStepIndex() === 0) && !this.tour.ended();
return this.continueTour() || ((this.currentStepIndex() === 0) && !this.tour.ended());
},
currentStepIndex: function () {
var index = this.tourStorage.getItem(this.id+'_current_step') || 0;
@@ -82,6 +66,23 @@
stop: function () {
this.tour.end();
},
redirect: function (url) {
url = url || new website.UrlParser(window.location.href);
if (this.startPath && url.pathname !== this.startPath) {
var newUrl = this.startPath + (url.search ? (url.search + "&") : "?") + this.id + "=true"
window.location.replace(newUrl);
}
},
continueTour: function () {
// Override if necessary
return this.currentStepIndex() === 0;
},
isTriggerUrl: function (url) {
// Override if necessary
url = url || new website.UrlParser(window.location.href);
var urlTrigger = this.id + "=true";
return url.search.indexOf(urlTrigger) >= 0;
},
});
website.UrlParser = openerp.Class.extend({
@@ -96,17 +97,36 @@
this.pathname = a.pathname;
this.origin = a.origin;
this.search = a.search;
this.hash = a.hash;
},
});
website.EditorBar.include({
tours: [],
start: function () {
$('.tour-backdrop').click(function (e) {
e.stopImmediatePropagation();
e.preventDefault();
});
var url = new website.UrlParser(window.location.href);
var menu = $('#help-menu');
_.each(this.tours, function (tour) {
var $menuItem = $($.parseHTML('<li><a href="#">'+tour.name+'</a></li>'));
$menuItem.click(function () {
tour.redirect(url);
tour.reset();
tour.start();
});
menu.append($menuItem);
if (tour.isTriggerUrl()) {
tour.start();
}
});
return this._super();
},
registerTour: function (tour) {
this.tours.push(tour);
},
});
}());
@@ -6,7 +6,7 @@
var render = website.tour.render;
website.EditorBuilderTour = website.EditorTour.extend({
id: 'menu_editor',
id: 'menu-editor',
name: "Add a new page",
init: function (editor) {
var self = this;
@@ -100,14 +100,7 @@
website.EditorBar.include({
start: function () {
var menu = $('#help-menu');
var builderTour = new website.EditorBuilderTour(this);
var $menuItem = $($.parseHTML('<li><a href="#">'+builderTour.name+'</a></li>'));
$menuItem.click(function () {
builderTour.reset();
builderTour.start();
});
menu.append($menuItem);
this.registerTour(new website.EditorBuilderTour(this));
return this._super();
},
});
@@ -41,23 +41,12 @@
<ul class="list-group">
<li class="list-group-item form-group active">
<h4 class="list-group-item-heading">
<label for="link-existing" class="control-label">
Existing page
<label for="link-page" class="control-label">
Page
</label>
</h4>
<select class="existing form-control url-source"
id="link-existing">
<option/>
</select>
</li>
<li class="list-group-item form-group">
<h4 class="list-group-item-heading">
<label for="link-new" class="control-label">
New page
</label>
</h4>
<input type="text" class="form-control pages url-source"
id="link-new" placeholder="Page Name"/>
<input class="page form-control url-source"
id="link-page" type="hidden"/>
</li>
<li class="list-group-item form-group clearfix">
<div class="pull-right">
+6 -3
View File
@@ -80,6 +80,9 @@
<script t-if="not translatable" type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script>
<t t-if="editable" id="editable_scripts_hook">
<script type="text/javascript" src="/select2/static/lib/select2/select2.js"></script>
<link rel="stylesheet" href="/select2/static/lib/select2/select2.css"/>
<link rel="stylesheet" href="/website/static/lib/select2-bootstrap-css/select2-bootstrap.css"/>
<script type="text/javascript" src="/website/static/lib/ckeditor/ckeditor.js"></script>
<script type="text/javascript" src="/website/static/lib/ckeditor.sharedspace/plugin.js"></script>
<script type="text/javascript" src="/website/static/lib/bootstrap-tour/bootstrap-tour.js"></script>
@@ -108,7 +111,7 @@
<t t-raw="head or ''"/>
</head>
<body t-att-class="has_access_write and 'has_access_write' or ''">
<body>
<div id="wrapwrap">
<header>
<div class="navbar navbar-default navbar-static-top">
@@ -266,7 +269,7 @@
</template>
<template id="publish_management">
<div t-if="has_access_write" t-ignore="true" class="pull-right" t-att-style="style or ''">
<div t-if="editable" t-ignore="true" class="pull-right" t-att-style="style or ''">
<div t-attf-class="btn-group js_publish_management #{object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-id="object.id" t-att-data-object="object._name" t-att-data-controller="publish_controller">
<button t-attf-class="btn btn-#{object.website_published and 'success' or 'danger'} js_publish_btn">
<span class="css_publish">Not Published</span>
@@ -287,7 +290,7 @@
</template>
<template id="publish_short">
<t t-if="has_access_write" t-ignore="true">
<t t-if="editable" t-ignore="true">
<div t-attf-class="btn-group pull-right js_publish_management #{object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-id="object.id" t-att-data-object="object._name" t-att-data-controller="publish_controller">
<button t-attf-class="btn btn-sm btn-#{object.website_published and 'success' or 'danger'} js_publish_btn">
<span class="css_publish">Not Published</span>
+2 -2
View File
@@ -242,11 +242,11 @@ class website_event(http.Controller):
return controllers.publish(id, object)
@website.route('/event/add_event/', type='http', auth="user", multilang=True, methods=['POST'])
def add_event(self, **kwargs):
def add_event(self, event_name="New Event", **kwargs):
Event = request.registry.get('event.event')
date_begin = datetime.today() + timedelta(days=(15)) # FIXME: better defaults
event_id = Event.create(request.cr, request.uid, {
'name': 'New Event',
'name': event_name,
'date_begin': date_begin.strftime('%Y-%m-%d'),
'date_end': (date_begin + timedelta(days=(1))).strftime('%Y-%m-%d'),
}, context=request.context)
@@ -3,4 +3,20 @@
var website = openerp.website;
website.add_template_file('/website_event/static/src/xml/website_event.xml');
website.EditorBar.include({
events: _.extend({}, website.EditorBar.prototype.events, {
'click a[data-action=new_event]': function (ev) {
ev.preventDefault();
website.prompt({
window_title: "New Event",
input: "Event Name",
}).then(function (event_name) {
website.form('/event/add_event', 'POST', {
event_name: event_name
});
});
}
}),
});
})();
@@ -1,7 +1,7 @@
<templates id="template" xml:space="preserve">
<t t-extend="website.editorbar">
<t t-jquery="ul.oe_content_menu" t-operation="append">
<li><a href="/event/add_event" class="js_link2post">New Event</a></li>
<li><a href="#" data-action="new_event">New Event</a></li>
</t>
</t>
</templates>
+2 -17
View File
@@ -293,7 +293,6 @@ class Ecommerce(http.Controller):
domain = [('id', 'in', ids or [0] )] + domain
step = 20
print '**', domain
product_count = len(product_obj.search(request.cr, request.uid, domain, context=request.context))
pager = request.website.pager(url="/shop/", total=product_count, page=page, step=step, scope=7, url_args=post)
@@ -308,14 +307,7 @@ class Ecommerce(http.Controller):
style_ids = style_obj.search(request.cr, request.uid, [(1, '=', 1)], context=request.context)
styles = style_obj.browse(request.cr, request.uid, style_ids, context=request.context)
try:
product_obj.check_access_rights(request.cr, request.uid, 'write')
has_access_write = True
except:
has_access_write = False
values = {
'has_access_write': has_access_write,
'Ecommerce': self,
'product_ids': product_ids,
'product_ids_for_holes': fill_hole,
@@ -347,14 +339,7 @@ class Ecommerce(http.Controller):
request.context['pricelist'] = self.get_pricelist()
try:
request.registry.get('product.template').check_access_rights(request.cr, request.uid, 'write')
has_access_write = True
except:
has_access_write = False
values = {
'has_access_write': has_access_write,
'Ecommerce': self,
'category': category,
'category_list': category_list,
@@ -369,10 +354,10 @@ class Ecommerce(http.Controller):
return request.website.render("website_sale.product", values)
@website.route(['/shop/add_product/', '/shop/category/<int:cat_id>/add_product/'], type='http', auth="user", multilang=True, methods=['POST'])
def add_product(self, cat_id=0, **post):
def add_product(self, name="New Product", cat_id=0, **post):
Product = request.registry.get('product.product')
product_id = Product.create(request.cr, request.uid, {
'name': 'New Product', 'public_categ_id': cat_id
'name': name, 'public_categ_id': cat_id
}, context=request.context)
product = Product.browse(request.cr, request.uid, product_id, context=request.context)
@@ -345,7 +345,6 @@ ul.wizard {
padding: 0;
margin-top: 20px;
list-style: none outside none;
border: 1px solid #d4d4d4;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
@@ -355,6 +354,8 @@ ul.wizard {
}
ul.wizard li {
border: 1px solid #d4d4d4;
border-right-width: 0;
position: relative;
float: left;
padding: 0 10px 0 20px;
@@ -303,7 +303,6 @@ ul.wizard
padding: 0
margin-top: 20px
list-style: none outside none
border: 1px solid #d4d4d4
-webkit-border-radius: 4px
-moz-border-radius: 4px
border-radius: 4px
@@ -312,6 +311,8 @@ ul.wizard
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.065)
ul.wizard li
border: 1px solid #d4d4d4
border-right-width: 0
position: relative
float: left
padding: 0 10px 0 20px
@@ -6,8 +6,9 @@
var render = website.tour.render;
website.EditorShopTour = website.EditorTour.extend({
id: 'shop_tour',
id: 'shop-tutorial',
name: "Create a product",
startPath: '/shop',
init: function (editor) {
var self = this;
var $body = $(document.body);
@@ -16,32 +17,90 @@
stepId: 'welcome-shop',
orphan: true,
backdrop: true,
title: "e-commerce",
content: "Welcome to the e-commerce. This tutorial will help you to create a product.",
title: "Welcome to your shop",
content: "You successfully installed the e-commerce. This guide will help you to create your product and promote your sales.",
template: render('website.tour_popover', { next: "Start Tutorial", end: "Skip It" }),
},
{
stepId: 'content-menu',
element: '#content-menu-button',
placement: 'left',
reflex: true,
title: "Create your first product",
content: "Click here to add a new product.",
template: render('website.tour_popover'),
},
{
stepId: 'edit-entry',
element: '#create-new-product',
placement: 'left',
title: "Create a new product",
content: "Select 'New Product' to create it and manage its properties to boost your sales.",
template: render('website.tour_popover'),
onShow: function () {
$(document).one('shown.bs.modal', function () {
$('.modal button.btn-primary').click(function () {
self.movetoStep('product-page');
});
self.movetoStep('enter-name');
});
},
},
{
stepId: 'enter-name',
element: '.modal input[type=text]',
placement: 'right',
title: "Choose name",
content: "Enter a name for your new product.",
template: render('website.tour_popover'),
},
{
stepId: 'product-page',
orphan: true,
backdrop: true,
title: "New product created",
content: "This page contains all the information related to the new product.",
template: render('website.tour_popover', { next: "OK" }),
},
{
stepId: 'edit-price',
element: '.product_price',
placement: 'left',
title: "Change the public price",
content: "Edit the sale price of this product by clicking on the amount. The price is the sale price used in all sale order when selling this product.",
template: render('website.tour_popover', { next: "OK" }),
},
{
stepId: 'add-block',
element: 'button[data-action=snippet]',
placement: 'bottom',
reflex: true,
title: "Describe the product for your audience",
content: "Insert blocks like text-image, or gallery to fully describe the product and make your visitors want to buy this product.",
template: render('website.tour_popover'),
},
];
return this._super();
},
productPage: function () {
var currentStepIndex = this.currentStepIndex();
var productPageIndex = this.indexOfStep('product-page');
return (currentStepIndex === productPageIndex) && !this.tour.ended();
},
continueTour: function () {
return this.productPage();
},
isTriggerUrl: function (url) {
url = url || new website.UrlParser(window.location.href);
var addProductPattern = /^\/shop\/product\/[0-9]+\/\?enable_editor=1/;
return (this.productPage() && addProductPattern.test(url.pathname+url.search)) || this._super();
},
});
website.EditorBar.include({
start: function () {
var menu = $('#help-menu');
var shopTour = new website.EditorShopTour(this);
var $menuItem = $($.parseHTML('<li><a href="#">'+shopTour.name+'</a></li>'));
var url = new website.UrlParser(window.location.href);
$menuItem.click(function () {
shopTour.reset();
shopTour.start();
if (url.pathname !== '/shop') {
window.location.replace('/shop?shop-tutorial=true');
}
});
if (url.search.indexOf('?shop-tutorial=true') === 0) {
shopTour.start();
}
menu.append($menuItem);
this.registerTour(new website.EditorShopTour(this));
return this._super();
},
});
@@ -3,4 +3,20 @@
var website = openerp.website;
website.add_template_file('/website_sale/static/src/xml/website_sale.xml');
website.EditorBar.include({
events: _.extend({}, website.EditorBar.prototype.events, {
'click a[data-action=new_product]': function (ev) {
ev.preventDefault();
website.prompt({
window_title: "New Product",
input: "Product Name",
}).then(function (name) {
website.form('/shop/add_product', 'POST', {
name: name
});
});
}
}),
});
})();
@@ -1,7 +1,7 @@
<templates id="template" xml:space="preserve">
<t t-extend="website.editorbar">
<t t-jquery="ul.oe_content_menu" t-operation="append">
<li><a href="/shop/add_product" class="js_link2post">New Product</a></li>
<li><a id="create-new-product" href="#" data-action="new_product">New Product</a></li>
</t>
</t>
</templates>
+1 -7
View File
@@ -89,12 +89,6 @@
<div class="oe_structure"/>
<div class="container oe_website_sale">
<div class="row">
<div class="col-sm-4 pagination hidden-xs" style="padding-left: 15px;">
<form t-if="editable" t-keep-query="category,search,facettes"
method="POST" t-action="/shop/add_product">
<button class="btn btn-primary">New Product</button>
</form>
</div>
<div class="col-sm-8 products_pager">
<t t-call="website.pager">
<t t-set="classname">pull-right</t>
@@ -124,7 +118,7 @@
<div class="oe_product_cart" t-att-data-publish="product.website_published and 'on' or 'off'">
<div class="css_options" t-ignore="true" t-if="has_access_write">
<div class="css_options" t-ignore="true" t-if="is_website_publisher">
<div t-attf-class="dropdown js_options" t-att-data-id="product.id">
<a class="btn btn-default" t-att-id="'dopprod-%s' % product.id" role="button" data-toggle="dropdown">Options <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu" t-att-aria-labelledby="'dopprod-%s' % product.id">