Skip to content
Browse files

Merge branch 'master' into ree-actually-use-catalog-for-sorting

  • Loading branch information...
2 parents 22c445d + 2e1d5c5 commit 52f35e2ae11ed3c7bc9a32ee52adfa6446e833c9 @mcdonc mcdonc committed Mar 28, 2013
View
33 docs/folder_contents.rst
@@ -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
61 docs/permissions.rst
@@ -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.
View
11 substanced/folder/__init__.py
@@ -754,6 +754,7 @@ class SequentialAutoNamingFolder(Folder, _AutoNamingFolder):
_autoname_length = 7
_autoname_start = -1
+ _autoname_reset = False
def __init__(
self,
@@ -789,10 +790,14 @@ def next_name(self, subobject):
will be the value of this folder's ``autoname_start`` constructor
value.
"""
- try:
- maxkey = self.data.maxKey()
- except ValueError: # empty tree
+ if self._autoname_reset:
maxkey = self._autoname_start
+ self._autoname_reset = False
+ else:
+ try:
+ maxkey = self.data.maxKey()
+ except ValueError: # empty tree
+ maxkey = self._autoname_start
name = self._zfill(int(maxkey) + 1)
return name
View
13 substanced/folder/tests.py
@@ -938,6 +938,19 @@ def test_next_name_alternate_autoname_start(self):
inst = self._makeOne(autoname_start=0)
self.assertEqual(inst.next_name(None), '1'.zfill(7))
+ def test_next_name_empty_autoname_reset(self):
+ inst = self._makeOne()
+ inst._autoname_reset = True
+ self.assertEqual(inst.next_name(None), '0'.zfill(7))
+ self.assertFalse(inst._autoname_reset)
+
+ def test_next_name_nonempty_autoname_reset(self):
+ ob = DummyModel()
+ inst = self._makeOne({'0000005':ob})
+ inst._autoname_reset = True
+ self.assertEqual(inst.next_name(None), '0'.zfill(7))
+ self.assertFalse(inst._autoname_reset)
+
def test_add_not_intifiable(self):
ob = DummyModel()
inst = self._makeOne()
View
22 substanced/sdi/views/folder.py
@@ -269,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',
@@ -452,6 +461,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):
@@ -486,6 +497,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
View
17 substanced/sdi/views/templates/contents.pt
@@ -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);
View
55 substanced/sdi/views/tests/test_folder.py
@@ -986,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)
@@ -1131,6 +1185,7 @@ def test__folder_contents_columns_None(self):
'name_url': '/mgmt_path',
'deletable': True,
'name': 'fred',
+ 'disable': [],
'id': 'fred'}
)
View
7 substanced/widget/__init__.py
@@ -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)
View
7 tox.ini
@@ -6,10 +6,6 @@ envlist =
commands =
python setup.py dev
python setup.py test -q
-deps =
- git+git://github.com/Pylons/pyramid.git#egg=pyramid
- git+git://github.com/Pylons/colander.git#egg=colander
- git+git://github.com/Pylons/hypatia.git#egg=hypatia
[testenv:cover]
basepython =
@@ -18,8 +14,5 @@ commands =
python setup.py dev
python setup.py nosetests --with-xunit --with-xcoverage
deps =
- git+git://github.com/Pylons/pyramid.git#egg=pyramid
- git+git://github.com/Pylons/colander.git#egg=colander
- git+git://github.com/Pylons/hypatia.git#egg=hypatia
nosexcover

0 comments on commit 52f35e2

Please sign in to comment.
Something went wrong with that request. Please try again.