Index: modules/block/block.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.admin.inc,v retrieving revision 1.90 diff -u -p -r1.90 block.admin.inc --- modules/block/block.admin.inc 10 Oct 2010 20:11:21 -0000 1.90 +++ modules/block/block.admin.inc 23 Oct 2010 03:49:43 -0000 @@ -286,11 +286,12 @@ function block_admin_configure($form, &$ '#type' => 'fieldset', '#title' => t('Region settings'), '#collapsible' => FALSE, - '#description' => t('Specify in which themes and regions this block is displayed.'), + '#description' => t('Specify where this block is displayed.'), '#tree' => TRUE, ); $theme_default = variable_get('theme_default', 'bartik'); + $admin_theme = variable_get('admin_theme'); foreach (list_themes() as $key => $theme) { // Only display enabled themes if ($theme->status) { @@ -300,9 +301,18 @@ function block_admin_configure($form, &$ ':theme' => $key, ))->fetchField(); + // Provide labels to help distinguish the administrative and + // default themes. + $theme_title = $theme->info['name']; + if ($key == $theme_default) { + $theme_title = t('!theme (default theme)', array('!theme' => $theme_title)); + } + elseif ($admin_theme && $key == $admin_theme) { + $theme_title = t('!theme (administration theme)', array('!theme' => $theme_title)); + } $form['regions'][$key] = array( '#type' => 'select', - '#title' => $theme->info['name'], + '#title' => $theme_title, '#default_value' => !empty($region) && $region != -1 ? $region : NULL, '#empty_value' => BLOCK_REGION_NONE, '#options' => system_region_list($key, REGIONS_VISIBLE), Index: modules/block/block.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.api.php,v retrieving revision 1.13 diff -u -p -r1.13 block.api.php --- modules/block/block.api.php 13 Aug 2010 12:25:14 -0000 1.13 +++ modules/block/block.api.php 23 Oct 2010 03:49:43 -0000 @@ -57,6 +57,11 @@ * - DRUPAL_CACHE_GLOBAL: The block is the same for every user on every * page where it is visible. * - DRUPAL_NO_CACHE: The block should not get cached. + * - 'properties': (optional) Array of additional metadata to add to the + * block. Common properties include: + * - 'administrative': Boolean which categorizes this block as usable in + * an administrative context. This might include blocks which help an + * admin approve/deny comments, or view recently created user accounts. * - 'weight': (optional) Initial value for the ordering weight of this block. * Most modules do not provide an initial value, and any value provided can * be modified by a user on the block configuration screen. Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.906 diff -u -p -r1.906 comment.module --- modules/comment/comment.module 20 Oct 2010 01:31:06 -0000 1.906 +++ modules/comment/comment.module 23 Oct 2010 03:49:44 -0000 @@ -402,6 +402,7 @@ function comment_permission() { */ function comment_block_info() { $blocks['recent']['info'] = t('Recent comments'); + $blocks['recent']['properties']['administrative'] = TRUE; return $blocks; } Index: modules/dashboard/dashboard.admin.js =================================================================== RCS file: modules/dashboard/dashboard.admin.js diff -N modules/dashboard/dashboard.admin.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/dashboard/dashboard.admin.js 23 Oct 2010 03:49:44 -0000 @@ -0,0 +1,129 @@ +// $Id$ + +Drupal.behaviors.dashboardAdmin = {}; + +Drupal.behaviors.dashboardAdmin.attach = function (context, settings) { + var $ = jQuery; + var theme = settings.dashboardTheme; + var regions = settings.dashboardRegions; + var $checkbox = $('#edit-dashboard-availability').once('dashboard-admin'); + var $select = $('#edit-regions-' + theme); + if ($checkbox.length && $select.length) { + // When the user changes the administrative theme regions dropdown, update + // the dashboard checkbox, and vice versa. + var handler = new Drupal.dashboardAdminHandler($checkbox, $select, regions); + handler.initialize(); + } +}; + +/** + * Constructor for the Drupal.dashboardAdminHandler class. + * + * Keeps the administrative theme regions dropdown and the dashboard checkbox + * in sync. + * + * @param {jQuery} $checkbox + * A jQuery object representing the dashboard checkbox. + * @param {jQuery} $select + * A jQuery object representing the select dropdown for the administrative + * theme's regions. + * @param {array} regions + * An array of dashboard region names. + */ +Drupal.dashboardAdminHandler = function ($checkbox, $select, regions) { + this.$checkbox = $checkbox; + this.$select = $select; + this.initializeDashboardRegions(regions); + this.dashboardMain = 'dashboard_main'; + + this.initialCheckboxValue = $checkbox.is(':checked'); + this.initialSelectValue = $select.val(); + this.blockRegionNone = $select.find('option:first').val(); +}; + +/** + * Initializes the dashboardAdminHandler object. + * + * Determines the initial values for the dashboard region and the content + * region, and binds a change handler to the checkbox and the select dropdown. + */ +Drupal.dashboardAdminHandler.prototype.initialize = function () { + var $ = jQuery; + + // Keep track of the original value of the region dropdown, so we can restore + // it when the checkbox value changes. + if (this.isDashboardRegion(this.initialSelectValue)) { + this.dashboardRegion = this.initialSelectValue; + this.contentRegion = this.blockRegionNone; + } + else { + this.dashboardRegion = this.dashboardMain; + this.contentRegion = this.initialSelectValue; + } + + // Bind event handlers to the checkbox and the select dropdown so we can + // keep each up to date when the other changes. + this.$checkbox.bind('change', $.proxy(this, 'checkboxHandler')); + this.$select.bind('change', $.proxy(this, 'selectHandler')); +}; + +/** + * Updates the select dropdown when the user clicks the checkbox. + */ +Drupal.dashboardAdminHandler.prototype.checkboxHandler = function () { + var checked = this.$checkbox.is(':checked'); + if (checked) { + this.$select.val(this.dashboardRegion); + } + else { + this.$select.val(this.contentRegion); + } +}; + +/** + * Updates the checkbox when the user changes the selected region. + */ +Drupal.dashboardAdminHandler.prototype.selectHandler = function () { + var region = this.$select.val(); + // If the user chose "None" as the region, record that as their new region + // preference. + if (region == this.blockRegionNone) { + var checked = this.$checkbox.attr('checked'); + if (checked) { + this.dashboardRegion = this.blockRegionNone; + } + else { + this.contentRegion = this.blockRegionNone; + } + } + else if (this.isDashboardRegion(region)) { + this.dashboardRegion = region; + this.$checkbox.attr('checked', 'checked'); + } + else { + this.contentRegion = region; + this.$checkbox.attr('checked', ''); + } +}; + +/** + * Determines whether a given region is on the dashboard. + */ +Drupal.dashboardAdminHandler.prototype.isDashboardRegion = function (regionName) { + return this.dashboardRegions[regionName] || false; +}; + +/** + * Initializes the object with the list of dashboard region names. + * + * @param {Array} regions + * An array of dashboard region names. + */ +Drupal.dashboardAdminHandler.prototype.initializeDashboardRegions = function (regions) { + var regionNames = {}; + var i; + for (i = 0; i < regions.length; i++) { + regionNames[regions[i]] = regions[i]; + } + this.dashboardRegions = regionNames; +}; Index: modules/dashboard/dashboard.module =================================================================== RCS file: /cvs/drupal/drupal/modules/dashboard/dashboard.module,v retrieving revision 1.40 diff -u -p -r1.40 dashboard.module --- modules/dashboard/dashboard.module 21 Oct 2010 11:55:08 -0000 1.40 +++ modules/dashboard/dashboard.module 23 Oct 2010 03:49:44 -0000 @@ -186,6 +186,33 @@ function dashboard_system_info_alter(&$i } /** + * Implements hook_block_info_alter(). + * + * Saves a list of blocks to be available to the dashboard by default, which + * are defined with $block['properties']['administrative'] = TRUE. + */ +function dashboard_block_info_alter(&$blocks, $theme, $code_blocks) { + if (!$theme = variable_get('admin_theme')) { + global $theme_key; + $theme = $theme_key; + } + $available_blocks = variable_get('dashboard_available_blocks', array()); + + foreach ($blocks as $module => &$module_blocks) { + foreach ($module_blocks as $delta => &$block) { + if ($block['theme'] != $theme) { + // Only process for the admin theme, which displays the dashboard. + continue; + } + if (!empty($code_blocks[$module][$delta]['properties']['administrative']) && !isset($available_blocks[$module][$delta])) { + $available_blocks[$module][$delta] = TRUE; + } + } + } + variable_set('dashboard_available_blocks', $available_blocks); +} + +/** * Implements hook_theme(). */ function dashboard_theme() { @@ -281,14 +308,16 @@ function dashboard_admin_blocks() { module_load_include('inc', 'block', 'block.admin'); // Prepare the blocks for the current theme, and remove those that are - // currently displayed in non-dashboard regions. + // currently displayed in non-dashboard regions, or are not marked as + // available through hook_block_info() or the block configure form. // @todo This assumes the current page is being displayed using the same // theme that the dashboard is displayed in. $blocks = block_admin_display_prepare_blocks($theme_key); $dashboard_regions = dashboard_region_descriptions(); $regions_to_remove = array_diff_key(system_region_list($theme_key, REGIONS_VISIBLE), $dashboard_regions); + $available_blocks = variable_get('dashboard_available_blocks', array()); foreach ($blocks as $id => $block) { - if (isset($regions_to_remove[$block['region']])) { + if (isset($regions_to_remove[$block['region']]) || (empty($available_blocks[$block['module']][$block['delta']]) && !in_array($block['region'], dashboard_regions()))) { unset($blocks[$id]); } } @@ -356,6 +385,68 @@ function dashboard_form_block_admin_conf $region['#options'] = array_diff_key($region['#options'], $dashboard_regions); } } + + // Provide a block setting to control dashboard availability. + $available_blocks = variable_get('dashboard_available_blocks', array()); + $form['settings']['dashboard_availability'] = array( + '#type' => 'checkbox', + '#title' => t('Available on dashboard'), + '#default_value' => !empty($available_blocks[$form['module']['#value']][$form['delta']['#value']]), + ); + + $form['#attached']['js'][] = drupal_get_path('module', 'dashboard') . '/dashboard.admin.js'; + $form['#attached']['js'][] = array( + 'data' => array( + 'dashboardTheme' => $theme_key, + 'dashboardRegions' => dashboard_regions(), + ), + 'type' => 'setting', + ); + $form['#validate'][] = 'dashboard_form_block_admin_configure_validate'; + $form['#submit'] = array_merge(array('dashboard_form_block_admin_configure_submit'), $form['#submit']); +} + +/** + * Form validation handler for block configuration form. + */ +function dashboard_form_block_admin_configure_validate($form, &$form_state) { + global $theme_key; + drupal_theme_initialize(); + + // Ensure that the dashboard availability flag is only settable if the block + // hasn't been assigned to a non-dashboard region. + $selected_region = $form_state['values']['regions'][$theme_key]; + $changed = ($form['settings']['dashboard_availability']['#default_value'] != $form_state['values']['dashboard_availability']); + if ($form_state['values']['dashboard_availability'] && $changed && $selected_region != BLOCK_REGION_NONE && !in_array($selected_region, dashboard_regions())) { + form_set_error('regions][' . $theme_key, t('The block must be removed from a region in !theme to enable it on the dashboard.', array('!theme' => $form['regions'][$theme_key]['#title']))); + } +} + +/** + * Form submission handler for block configuration form. + */ +function dashboard_form_block_admin_configure_submit($form, &$form_state) { + global $theme_key; + drupal_theme_initialize(); + + // Cause the block to be added/removed from the dashboard based on the + // dashboard availability setting, if the state has changed. + if ($form['settings']['dashboard_availability']['#default_value'] != $form_state['values']['dashboard_availability']) { + $region = &$form_state['values']['regions'][$theme_key]; + $available_blocks = variable_get('dashboard_available_blocks', array()); + $available_blocks[$form_state['values']['module']][$form_state['values']['delta']] = $form_state['values']['dashboard_availability']; + variable_set('dashboard_available_blocks', $available_blocks); + if ($form_state['values']['dashboard_availability']) { + // Add the block to the dashboard if not already assigned to a region. + if ($region == BLOCK_REGION_NONE) { + $region = 'dashboard_main'; + drupal_set_message(t('Block added to the dashboard.')); + } + } + elseif (in_array($region, dashboard_regions())) { + $region = BLOCK_REGION_NONE; + } + } } /** @@ -445,9 +536,11 @@ function dashboard_show_disabled() { // Blocks are not necessarily initialized at this point. $blocks = _block_rehash(); - // Limit the list to disabled blocks for the current theme. + // Limit the list to disabled blocks for the current theme which are marked + // as available through hook_block_info() or the block configure form. + $available_blocks = variable_get('dashboard_available_blocks', array()); foreach ($blocks as $key => $block) { - if ($block['theme'] != $theme_key || (!empty($block['status']) && !empty($block['region']))) { + if ($block['theme'] != $theme_key || (!empty($block['status']) && !empty($block['region'])) || empty($available_blocks[$block['module']][$block['delta']])) { unset($blocks[$key]); } } Index: modules/dashboard/dashboard.test =================================================================== RCS file: /cvs/drupal/drupal/modules/dashboard/dashboard.test,v retrieving revision 1.6 diff -u -p -r1.6 dashboard.test --- modules/dashboard/dashboard.test 21 Oct 2010 11:55:09 -0000 1.6 +++ modules/dashboard/dashboard.test 23 Oct 2010 03:49:44 -0000 @@ -42,7 +42,7 @@ class DashboardBlocksTestCase extends Dr $custom_block['info'] = $this->randomName(8); $custom_block['title'] = $this->randomName(8); $custom_block['body[value]'] = $this->randomName(32); - $custom_block['regions[stark]'] = 'dashboard_main'; + $custom_block['dashboard_availability'] = TRUE; $this->drupalPost('admin/structure/block/add', $custom_block, t('Save block')); // Ensure admin access. @@ -79,4 +79,116 @@ class DashboardBlocksTestCase extends Dr $this->assertTrue(empty($elements), t('%region is not an available choice on the block configuration page.', array('%region' => $region))); } } + + /** + * Test that defining a block with ['properties']['administrative'] = TRUE + * adds it as an available block for the dashboard. + */ + function testBlockAvailability() { + global $theme_key; + $this->drupalGet('admin/dashboard/configure'); + // "Recent comments" should be available in the dashboard block admin UI. + $this->assertText(t('Recent comments'), t('Block defined as "administrative" found in the dashboard block admin UI.')); + // "Syndicate" should not show up in the UI since it is not defined + // as "administrative". + $this->assertNoText(t('Syndicate'), t('Blocks not defined as "administrative" are excluded from dashboard UI by default.')); + + // Now test admin/dashboard/drawer in the same way. + $this->drupalGet('admin/dashboard/drawer'); + $this->assertText(t('Recent comments'), t('Drawer of disabled blocks includes the one defined as "administrative".')); + $this->assertNoText(t('Syndicate'), t('Drawer of disabled blocks excludes blocks not defined as "administrative".')); + + // Manipulate availability checkbox and reverse it: make "Recent comments" + // unavailable to the dashboard and make "Syndicate" available. + $values = array(); + $values['dashboard_availability'] = FALSE; + $this->drupalPost('admin/structure/block/manage/comment/recent/configure', $values, t('Save block')); + $this->drupalGet('admin/structure/block/manage/comment/recent/configure'); + $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-dashboard-availability')); + $this->assertTrue(!empty($elements[0]) && empty($elements[0]['checked']), t('Dashboard availability disabled for "Recent comments".')); + $values['dashboard_availability'] = TRUE; + $this->drupalPost('admin/structure/block/manage/node/syndicate/configure', $values, t('Save block')); + $this->drupalGet('admin/structure/block/manage/node/syndicate/configure'); + $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-dashboard-availability')); + $this->assertTrue(!empty($elements[0]) && !empty($elements[0]['checked']), t('Dashboard availability enabled for "Syndicate".')); + + $this->drupalGet('admin/dashboard/configure'); + $this->assertNoText(t('Recent comments'), t('Block dashboard availability can be overridden.')); + $this->assertText(t('Syndicate'), t('Block dashboard availability overridden for a block not defined as "administrative".')); + + // Now test admin/dashboard/drawer in the same way. + $this->drupalGet('admin/dashboard/drawer'); + $this->assertNoText(t('Recent comments'), t('Drawer of disabled blocks excludes blocks defined as "administrative" but overridden through the block UI.')); + $values = array(); + $values['blocks[node_syndicate][region]'] = BLOCK_REGION_NONE; + $this->drupalPost('admin/dashboard/configure', $values, t('Save blocks')); + $this->assertText(t('Syndicate'), t('Drawer of disabled blocks includes blocks not defined as "administrative" but overridden through the UI.')); + } + + /** + * Test that a block can be added and removed from the dashboard via the + * "Available on dashboard" checkbox. Also test the validation error if the + * block is already assigned to a region. + */ + function testDashboardEnableCheckbox() { + // Attempt to put "System help" block on the dashboard. This should fail + // since the block is already assigned to a non-dashboard region of the + // admin theme. + $this->drupalGet('admin/structure/block/manage/system/help/configure'); + $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-dashboard-availability')); + $this->assertTrue(!empty($elements[0]) && empty($elements[0]['checked']), t('Dashboard availability checkbox is present and unchecked.')); + $values = array(); + $values['dashboard_availability'] = TRUE; + $this->drupalPost('admin/structure/block/manage/system/help/configure', $values, t('Save block')); + $stark_label = t('!theme (default theme)', array('!theme' => 'Stark')); + $this->assertText(t('The block must be removed from a region in !theme to enable it on the dashboard.', array('!theme' => $stark_label)), t('Dashboard availability setting not selectable when the block is already in a non-dashboard region for the admin theme.')); + + // Unassign the region, and now we should see a success message. + $values['regions[stark]'] = BLOCK_REGION_NONE; + $this->drupalPost('admin/structure/block/manage/system/help/configure', $values, t('Save block')); + $this->assertText(t('Block added to the dashboard.'), t('Validation error is fixed by unassigning the region.')); + + // Ensure that the block shows up on the dashboard. + $this->drupalGet('admin/dashboard'); + $this->assertText(t('System help'), t('Block is now on the dashboard.')); + $this->drupalGet('admin/dashboard/configure'); + $this->assertText(t('System help'), t('Block displays in dashboard block UI.')); + + // Now remove the block from the dashboard via the checkbox. + $values = array(); + $values['dashboard_availability'] = FALSE; + $this->drupalPost('admin/structure/block/manage/system/help/configure', $values, t('Save block')); + $this->drupalGet('admin/dashboard'); + $this->assertNoText(t('System help'), t('Block was removed from the dashboard via the dashboard availability setting.')); + + // Test a block defined as "administrative" but not initially on + // the dashboard. + $this->drupalGet('admin/dashboard'); + $this->assertNoText(t('Recent comments'), t('Block is not on the dashboard.')); + $this->drupalGet('admin/dashboard/drawer'); + $this->assertText(t('Recent comments'), t('Block is in drawer of disabled blocks.')); + + $this->drupalGet('admin/structure/block/manage/comment/recent/configure'); + $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-dashboard-availability')); + $this->assertTrue(!empty($elements[0]) && !empty($elements[0]['checked']), t('Dashboard availability enabled for "Recent comments".')); + + // Save the "Who's online" block, which should be available to the + // dashboard by default, but not enabled. Change the title, but leave the + // dashboard availability checkbox unchanged. The block should not be + // automatically added to the dashboard unless the user consciously + // changes the checkbox. + $this->drupalGet('admin/structure/block/manage/user/online/configure'); + $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-dashboard-availability')); + $this->assertTrue(!empty($elements[0]) && !empty($elements[0]['checked']), t('Dashboard availability checkbox is checked.')); + $this->drupalGet('admin/dashboard'); + $this->assertNoText(t('Who\'s online'), t('Block is not on the dashboard.')); + $values = array(); + $values['title'] = t('My block'); + $this->drupalPost('admin/structure/block/manage/user/online/configure', $values, t('Save block')); + $this->assertNoText(t('Block added to the dashboard.'), t('Block was not automatically added to dashboard.')); + $this->drupalGet('admin/dashboard'); + $this->assertNoText(t('My block'), t('Block is not on the dashboard.')); + $this->drupalGet('admin/dashboard/drawer'); + $this->assertText(t('My block'), t('Block is in drawer of disabled blocks.')); + } } Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1309 diff -u -p -r1.1309 node.module --- modules/node/node.module 20 Oct 2010 08:15:33 -0000 1.1309 +++ modules/node/node.module 23 Oct 2010 03:49:44 -0000 @@ -2076,6 +2076,7 @@ function node_block_info() { $blocks['syndicate']['cache'] = DRUPAL_NO_CACHE; $blocks['recent']['info'] = t('Recent content'); + $blocks['recent']['properties']['administrative'] = TRUE; return $blocks; } Index: modules/search/search.module =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.module,v retrieving revision 1.365 diff -u -p -r1.365 search.module --- modules/search/search.module 20 Oct 2010 01:31:07 -0000 1.365 +++ modules/search/search.module 23 Oct 2010 03:49:44 -0000 @@ -143,6 +143,8 @@ function search_block_info() { $blocks['form']['info'] = t('Search form'); // Not worth caching. $blocks['form']['cache'] = DRUPAL_NO_CACHE; + $blocks['form']['properties']['administrative'] = TRUE; + return $blocks; } Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.1211 diff -u -p -r1.1211 user.module --- modules/user/user.module 20 Oct 2010 05:32:31 -0000 1.1211 +++ modules/user/user.module 23 Oct 2010 03:49:45 -0000 @@ -1273,10 +1273,13 @@ function user_block_info() { $blocks['login']['cache'] = DRUPAL_NO_CACHE; $blocks['new']['info'] = t('Who\'s new'); + $blocks['new']['properties']['administrative'] = TRUE; // Too dynamic to cache. $blocks['online']['info'] = t('Who\'s online'); $blocks['online']['cache'] = DRUPAL_NO_CACHE; + $blocks['online']['properties']['administrative'] = TRUE; + return $blocks; }