[MERGE] Sync with website-al
bzr revid: tde@openerp.com-20131119160204-1yvqbg4vgpc0bkqs
This commit is contained in:
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
}
|
||||
Vendored
Vendored
|
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 |
@@ -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',
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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 */
|
||||
}
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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 ---- */
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user