Permalink
Browse files

Merge branch 'master' of github.com:Pylons/substanced

  • Loading branch information...
2 parents bca3ca0 + d05dac2 commit 42a11ec7bf736e330cbc5d6f40d4fc1181d06329 @mcdonc mcdonc committed Mar 28, 2013
View
@@ -195,6 +195,39 @@ the ``sdi.manage-contents`` permission on ``folder`` *and* if the
subobject has a ``__sdi_deletable__`` attribute which resolves to a
boolean ``True`` value.
+It is also possible to make button enabling and disabling depend on some
+application-specific condition. To do this, assign a callable to the
+``enabled_for`` key in the button spec. For example:
+
+.. code-block:: python
+
+ def catalog_buttons(context, request, default_buttons):
+ def is_indexable(folder, subobject, request):
+ """ only enable the button if subobject is indexable """
+ return subobject.is_indexable()
+
+ buttons = [
+ {'type':'single',
+ 'buttons':
+ [
+ {'id':'reindex',
+ 'name':'form.reindex',
+ 'class':'btn-primary btn-sdi-sel',
+ 'value':'reindex',
+ 'enabled_for': is_indexable,
+ 'text':'Reindex'}
+ ]
+ }
+ ] + default_buttons
+ return buttons
+
+In the example above, we define a button similar to our previous reindex
+button, except this time we have an ``enabled_for`` key that is assigned
+the ``is_indexable`` function. When the buttons are rendered, each element
+is passed to this function, along with the folder and request. If *any one*
+of the folder subobjects returns ``False`` for this call, the button will
+not be enabled.
+
Filtering What Can Be Added
===========================
View
@@ -0,0 +1,61 @@
+Substance D SDI Permission Names
+================================
+
+sdi.add-content
+
+ Protects views which allow users to add content to a folder.
+
+sdi.add-group
+
+ Protects views which add groups to a groups collection within a principals
+ service.
+
+sdi.add-services
+
+ Protects views which add built-in Substance D services.
+
+sdi.add-user
+
+ Protects views which add users to a users collection within a principals
+ service.
+
+sdi.change-acls
+
+ Protects arbitrary locations, allowing certain people to execute views the
+ under that location which change ACLs associated with a resource.
+
+sdi.change-password
+
+ Protects views of a user which allow for the changing of passwords.
+
+sdi.manage-catalog
+
+ Protects views which allow users to manage catalog data and indexes within a
+ catalog service.
+
+sdi.manage-contents
+
+ Protects views which allow users to add, remove, and rename items within
+ folders.
+
+sdi.manage-database
+
+ Protects the "manage database" view at the root.
+
+sdi.manage-references
+
+ Protects views which allow users to manage the references associated with a
+ resource.
+
+sdi.manage-workflow
+
+ Protects the views associated with managing the workflows of an object.
+
+sdi.undo
+
+ Protects the capability of users to execute views which undo transactions.
+
+sdi.view
+
+ Protects whether a user can view the SDI management pages associated with a
+ resource.
@@ -144,14 +144,15 @@ def _column_headers(self):
sortable = False
formatter = column.get('formatter', '')
-
+ cssClass = column.get('cssClass', '')
+
headers.append({
- "id": field,
+ "id": field,
"name": name,
"field": field,
"width": 120,
"minWidth": 120,
- "cssClass": "cell-%s" % field,
+ "cssClass": "cell-%s" % field + ((' ' + cssClass) if cssClass else ''),
"sortable": sortable,
"formatterName": formatter,
})
@@ -268,15 +269,24 @@ def _folder_contents(
In addition to the classes mentioned above, any custom css class or any
bootstrap button class can be used.
+ Finally, each button can optionally include an ``enabled_for`` key,
+ which will point to a callable that will be passed a subobject from the
+ current folder and must return True if the button should be enabled for
+ that subobect or False if not.
+
Most of the time, the best strategy for using the buttons callable will
be to return a value containing the default buttonspec sequence passed
in to the function (it will be a list).::
def custom_buttons(context, request, default_buttonspec):
+ def some_condition(folder, subobject, request):
+ return getattr(context, 'can_use_button1', False)
+
custom_buttonspec = [{'type': 'single',
'buttons': [{'id': 'button1',
'name': 'button1',
'class': 'btn-sdi-sel',
+ 'enabled_for': some_condition,
'value': 'button1',
'text': 'Button 1'},
{'id': 'button2',
@@ -427,6 +437,8 @@ def main(global_config, **settings):
custom_columns = request.registry.content.metadata(
folder, 'columns', _marker)
+ buttons = self._buttons()
+
records = []
for oid in itertools.islice(ids, start, end):
@@ -461,6 +473,17 @@ def main(global_config, **settings):
for column in columns:
field = column['field']
record[field] = column['value']
+ disable = []
+ for button_group in buttons:
+ for button in button_group['buttons']:
+ if 'enabled_for' not in button:
+ continue
+ condition = button['enabled_for']
+ if not callable(condition):
+ continue
+ if not condition(folder, resource, request):
+ disable.append(button['id'])
+ record['disable'] = disable
records.append(record)
return folder_length, records
@@ -121,25 +121,34 @@
grid.onSelectedRowsChanged.subscribe(function (evt) {
var selRows = grid.getSelectedRows();
var data = grid.getData();
+ var disabled = [];
if (selRows.length) {
var disable_delete = false;
var disable_multiple = false;
var i;
- for (i = 0, l = selRows.length; i < l; i++) {
+ for (var i = 0, l = selRows.length; i < l; i++) {
var item = data[selRows[i]];
// XXX bug: global selection will select all items that
// are not present.
+ disabled[i] = item.disable;
+ if (i == 1) {
+ disable_multiple = true;
+ }
if (!item.deletable) {
disable_delete = true;
break;
}
- if (i == 1) {
- disable_multiple = true;
- }
}
$('.btn-sdi-del').attr('disabled', disable_delete);
$('.btn-sdi-sel').attr('disabled', false);
$('.btn-sdi-one').attr('disabled', disable_multiple);
+ for (var i = 0, l = selRows.length; i < l; i++) {
+ if (disabled[i]) {
+ for (var j = 0, k = disabled[i].length; j < k; j++) {
+ $('#' + disabled[i][j]).attr('disabled', true)
+ }
+ }
+ }
} else {
$('.btn-sdi-del').attr('disabled', true);
$('.btn-sdi-sel').attr('disabled', true);
@@ -368,6 +368,27 @@ def test__column_headers_None(self):
result = inst._column_headers()
self.assertEqual(len(result), 0)
+ def test__column_headers_cssClass(self):
+ def sd_columns(folder, subobject, request, default_columns):
+ self.assertEqual(len(default_columns), 1)
+ return [
+ {'name': 'Col 1', 'field': 'col1', 'value':
+ 'col1', 'sortable': True, 'cssClass': 'customClass'},
+ {'name': 'Col 2', 'field': 'col2', 'value': 'col2',
+ 'sortable': True},
+ {'name': 'Col 3', 'field': 'col3', 'value': 'col3',
+ 'sortable': True, 'cssClass': 'customClass1 customClass2'},
+ ]
+ context = testing.DummyResource(is_ordered=lambda: False)
+ request = self._makeRequest(columns=sd_columns)
+
+ inst = self._makeOne(context, request)
+ result = inst._column_headers()
+ self.assertEqual(len(result), 3)
+ self.assertEqual(result[0]['cssClass'], 'cell-col1 customClass')
+ self.assertEqual(result[1]['cssClass'], 'cell-col2')
+ self.assertEqual(result[2]['cssClass'], 'cell-col3 customClass1 customClass2')
+
def test_show_non_filterable_columns(self):
dummy_column_headers = [{
'field': 'col1',
@@ -965,6 +986,60 @@ def sdi_buttons(contexr, request, default_buttons):
result = inst._buttons()
self.assertEqual(result, 'abc')
+ def test_buttons_enabled_for_true(self):
+ from substanced.interfaces import IFolder
+ context = DummyFolder(__provides__=IFolder)
+ request = self._makeRequest()
+ context['catalogs'] = self._makeCatalogs(oids=[1])
+ result = testing.DummyResource()
+ result.__name__ = 'fred'
+ context.__objectmap__ = DummyObjectMap(result)
+ inst = self._makeOne(context, request)
+ def sdi_buttons(contexr, request, default_buttons):
+ return [{'type': 'single',
+ 'buttons': [{'enabled_for': lambda x,y,z: True,
+ 'id': 'Button'}]}]
+ request.registry.content = DummyContent(buttons=sdi_buttons)
+ length, rows = inst._folder_contents()
+ self.assertEqual(length, 1)
+ self.assertEqual(rows[0]['disable'], [])
+
+ def test_buttons_enabled_for_false(self):
+ from substanced.interfaces import IFolder
+ context = DummyFolder(__provides__=IFolder)
+ request = self._makeRequest()
+ context['catalogs'] = self._makeCatalogs(oids=[1])
+ result = testing.DummyResource()
+ result.__name__ = 'fred'
+ context.__objectmap__ = DummyObjectMap(result)
+ inst = self._makeOne(context, request)
+ def sdi_buttons(contexr, request, default_buttons):
+ return [{'type': 'single',
+ 'buttons': [{'enabled_for': lambda x,y,z: False,
+ 'id': 'Button'}]}]
+ request.registry.content = DummyContent(buttons=sdi_buttons)
+ length, rows = inst._folder_contents()
+ self.assertEqual(length, 1)
+ self.assertEqual(rows[0]['disable'], ['Button'])
+
+ def test_buttons_enabled_for_non_callable(self):
+ from substanced.interfaces import IFolder
+ context = DummyFolder(__provides__=IFolder)
+ request = self._makeRequest()
+ context['catalogs'] = self._makeCatalogs(oids=[1])
+ result = testing.DummyResource()
+ result.__name__ = 'fred'
+ context.__objectmap__ = DummyObjectMap(result)
+ inst = self._makeOne(context, request)
+ def sdi_buttons(contexr, request, default_buttons):
+ return [{'type': 'single',
+ 'buttons': [{'enabled_for': 'not callable',
+ 'id': 'Button'}]}]
+ request.registry.content = DummyContent(buttons=sdi_buttons)
+ length, rows = inst._folder_contents()
+ self.assertEqual(length, 1)
+ self.assertEqual(rows[0]['disable'], [])
+
def _makeCatalogs(self, oids=()):
catalogs = DummyCatalogs()
catalog = DummyCatalog(oids)
@@ -1110,6 +1185,7 @@ def test__folder_contents_columns_None(self):
'name_url': '/mgmt_path',
'deletable': True,
'name': 'fred',
+ 'disable': [],
'id': 'fred'}
)
@@ -4,7 +4,9 @@
from deform.template import ZPTTemplateLoader
from translationstring import ChameleonTranslate
+from pyramid.i18n import get_localizer
from pyramid.renderers import get_renderer
+from pyramid.threadlocal import get_current_request
class WidgetRendererFactory(object):
"""
@@ -69,12 +71,15 @@ def load(self, template_name):
else:
return self.loader.load(template_name + '.pt')
+def translator(term): # pragma: no cover
+ return get_localizer(get_current_request()).translate(term)
+
def includeme(config): # pragma: no cover
# specify both deform and deform_bootstrap templates as "fallback"
# locations; assume user-supplied templates will be specified using asset
# specs instead.
deform_dir = resource_filename('deform', 'templates/')
deform_bootstrap_dir = resource_filename('deform_bootstrap', 'templates/')
search_path = (deform_bootstrap_dir, deform_dir)
- renderer = WidgetRendererFactory(search_path)
+ renderer = WidgetRendererFactory(search_path, translator=translator)
deform.Form.set_default_renderer(renderer)

0 comments on commit 42a11ec

Please sign in to comment.