diff --git css/export-ui-list.css css/export-ui-list.css new file mode 100644 index 0000000..3a778ab --- /dev/null +++ css/export-ui-list.css @@ -0,0 +1,28 @@ +/* $Id: panels-item.css,v 1.1.2.1 2010/02/17 01:09:46 merlinofchaos Exp $ */ +body form#ctools-export-ui-list-form { + margin: 0 0 20px 0; +} + +#ctools-export-ui-list-form .form-item { + padding-right: 1em; /* LTR */ + float: left; /* LTR */ + margin-top: 0; + margin-bottom: 0; +} + +#ctools-export-ui-list-items { + width: 100%; +} + +#edit-order-wrapper { + clear: left; /* LTR */ +} + +#ctools-export-ui-list-form .form-submit { + margin-top: 1.65em; + float: left; /* LTR */ +} + +tr.ctools-export-ui-disabled { + color: #999; +} diff --git ctools.module ctools.module index 4d120b3..c7a6b15 100644 --- ctools.module +++ ctools.module @@ -284,7 +284,7 @@ function _ctools_passthrough(&$items, $type = 'theme') { require_once './' . $file->filename; list($tool) = explode('.', $file->name, 2); - $function = 'ctools_' . $tool . '_' . $type; + $function = 'ctools_' . str_replace ('-', '_', $tool) . '_' . $type; if (function_exists($function)) { $function($items); } @@ -760,3 +760,38 @@ function ctools_file_check_directory(&$directory, $mode = 0, $form_item = NULL) return TRUE; } + +/** + * Menu loader; Load exportables when used with export-ui. + */ +function ctools_export_ui_load($item_name, $plugin_name) { + $return = &ctools_static(__FUNCTION__, FALSE); + + if (!$return) { + ctools_include('export-ui'); + $plugin = ctools_get_export_ui($plugin_name); + + if ($plugin) { + // Get the load callback. + return ctools_export_crud_load($plugin['schema'], $item_name); + } + } + + return $return; +} + +/** + * Menu access callback for various tasks of export-ui. + */ +function ctools_export_ui_task_access($plugin_name, $op, $item = NULL) { + ctools_include('export-ui'); + $plugin = ctools_get_export_ui($plugin_name); + $handler = ctools_export_ui_get_handler($plugin); + + if ($handler) { + return $handler->access($op, $item); + } + + // Deny access if the handler cannot be found. + return FALSE; +} diff --git includes/export-ui.inc includes/export-ui.inc new file mode 100644 index 0000000..d03adf7 --- /dev/null +++ includes/export-ui.inc @@ -0,0 +1,404 @@ + 'ctools_export_ui_defaults', + ); +} + +/** + * Provide defaults for an export-ui plugin. + */ +function ctools_export_ui_defaults($info, &$plugin) { + ctools_include('export'); + + $plugin += array( + 'has menu' => TRUE, + 'title' => $plugin['name'], + 'export' => array(), + 'allowed operations' => array(), + 'menu' => array(), + 'form' => array(), + 'list' => NULL, + 'access' => 'administer site configuration', + ); + + if (empty($plugin['schema'])) { + if ($plugin['has menu']) { + // We need to issue a warning as schema is a required key. + drupal_set_message(t('The plugin definition of @plugin is missing the "schema" key.', array('@plugin' => $plugin['name'])), 'error'); + } + } + else { + $schema = ctools_export_get_schema($plugin['schema']); + + $plugin['export'] += array( + // Add the identifier key from the schema so we don't have to call + // ctools_export_get_schema() just for that. + 'key' => $schema['export']['key'], + ); + + // Add some default fields that appear often in exports + // If these use different keys they can easily be specified in the + // $plugin. + + if (empty($plugin['export']['admin_title']) && !empty($schema['fields']['admin_title'])) { + $plugin['export']['admin_title'] = 'admin_title'; + } + if (empty($plugin['export']['admin_description']) && !empty($schema['fields']['admin_description'])) { + $plugin['export']['admin_description'] = 'admin_description'; + } + } + + // Define allowed operations, and the name of the operations. + $plugin['allowed operations'] += array( + 'edit' => array('title' => t('Edit')), + 'enable' => array('title' => t('Enable'), 'ajax' => TRUE, 'token' => TRUE), + 'disable' => array('title' => t('Disable'), 'ajax' => TRUE, 'token' => TRUE), + 'revert' => array('title' => t('Revert')), + 'delete' => array('title' => t('Delete')), + 'clone' => array('title' => t('Clone')), + 'import' => array('title' => t('Import')), + 'export' => array('title' => t('Export')), + ); + + if ($plugin['has menu']) { + $plugin['menu'] += array( + 'menu item' => str_replace(' ', '-', $plugin['name']), + 'menu prefix' => 'admin/build', + ); + + $prefix_count = count(explode('/', $plugin['menu']['menu prefix'])); + + $plugin['menu'] += array( + // Default menu items that should be declared. + 'items' => array(), + ); + + $plugin['menu']['items'] += array( + 'list callback' => array( + 'path' => '', + // Menu items are translated by the menu system. + // TODO: We need more flexibility in title. The title of the admin page + // is not necessarily the title of the object, plus we need + // plural, singular, proper, not proper, etc. + 'title' => $plugin['title'], + 'description' => 'List ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'list'), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'list'), + 'type' => MENU_NORMAL_ITEM, + ), + 'list' => array( + 'path' => 'list', + 'title' => 'List', + 'description' => 'List ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'list'), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + ), + 'add' => array( + 'path' => 'add', + 'title' => 'Add', + 'description' => 'Add a new ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'add'), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'add'), + 'type' => MENU_LOCAL_TASK, + ), + 'edit callback' => array( + 'path' => 'list/%ctools_export_ui', + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'edit', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'edit', $prefix_count + 2), + 'type' => MENU_CALLBACK, + ), + 'edit' => array( + 'path' => 'list/%ctools_export_ui/edit', + 'title' => 'Edit', + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'edit', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'edit', $prefix_count + 2), + 'type' => MENU_DEFAULT_LOCAL_TASK, + ), + ); + + if ($plugin['allowed operations']['import']) { + $plugin['menu']['items'] += array( + 'import' => array( + 'path' => 'import', + 'title' => 'Import', + 'description' => 'Import ' . $plugin['title'] . ' to your site.', + // We allow permissions to import only to users that are allowed to + // execute php. + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'import'), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'import'), + 'type' => MENU_LOCAL_TASK, + ), + ); + } + + if ($plugin['allowed operations']['export']) { + $plugin['menu']['items'] += array( + 'export' => array( + 'path' => 'list/%ctools_export_ui/export', + 'title' => 'Export', + 'description' => 'Export ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'export', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'export', $prefix_count + 2), + 'type' => MENU_LOCAL_TASK, + ), + ); + } + + if ($plugin['allowed operations']['revert']) { + $plugin['menu']['items'] += array( + 'revert' => array( + 'path' => 'list/%ctools_export_ui/revert', + 'title' => 'Revert', + 'description' => 'Revert ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + // Note: Yes, 'delete' op is correct. + 'page arguments' => array($plugin['name'], 'delete', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'revert', $prefix_count + 2), + 'type' => MENU_CALLBACK, + ), + ); + } + + if ($plugin['allowed operations']['delete']) { + $plugin['menu']['items'] += array( + 'delete' => array( + 'path' => 'list/%ctools_export_ui/delete', + 'title' => 'Delete', + 'description' => 'Delete ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'delete', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'delete', $prefix_count + 2), + 'type' => MENU_CALLBACK, + ), + ); + } + + if ($plugin['allowed operations']['clone']) { + $plugin['menu']['items'] += array( + 'clone' => array( + 'path' => 'list/%ctools_export_ui/clone', + 'title' => 'Clone', + 'description' => 'Revert ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'clone', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'clone', $prefix_count + 2), + 'type' => MENU_CALLBACK, + ), + ); + } + + if ($plugin['allowed operations']['enable']) { + $plugin['menu']['items'] += array( + 'enable' => array( + 'path' => 'list/%ctools_export_ui/enable', + 'title' => 'Enable', + 'description' => 'Enable ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'enable', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'enable', $prefix_count + 2), + 'type' => MENU_CALLBACK, + ), + ); + } + + if ($plugin['allowed operations']['disable']) { + $plugin['menu']['items'] += array( + 'disable' => array( + 'path' => 'list/%ctools_export_ui/disable', + 'title' => 'Disable', + 'description' => 'Disable ' . $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'disable', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'disable', $prefix_count + 2), + 'type' => MENU_CALLBACK, + ), + ); + } + } + + // Define form elements. + $plugin['form'] += array( + 'settings' => function_exists($plugin['name'] . '_form') ? $plugin['name'] . '_form' : '', + 'string' => array( + // Strings used in drupal_set_title(). + 'title' => array( + 'add' => t('Add a new @plugin', array('@plugin' => $plugin['title'])), + // The "%title" will be replaced in ctools_export_ui_form(), as in this + // stage we dont have the specific exportable object. + 'edit' => t('Editing @plugin %title', array('@plugin' => $plugin['title'])), + 'view' => t('Viewing @plugin %title', array('@plugin' => $plugin['title'])), + 'clone' => t('Cloning @plugin %title', array('@plugin' => $plugin['title'])), + + 'import' => t('Import @plugin', array('@plugin' => $plugin['title'])), + 'export' => t('Export @plugin', array('@plugin' => $plugin['title'])), + ), + // Strings used in confirmation pages. + 'confirmation' => array( + // The "!action" and "%title" will be replaced in + // ctools_export_ui_form(). + 'question' => t('Are you sure you want to !action the @plugin %title?', array('@plugin' => $plugin['title'])), + 'revert' => t('This action will permanently remove any customizations made to this item.'), + 'delete' => t('This action will remove this item permanently from your site.'), + ), + // Strings used in $forms. + 'help' => array( + 'import' => t('You can import an exported definition by pasting the exported object code into the field below.'), + ), + // Strings used in drupal_set_message(). + 'message' => array( + 'enable' => t('@plugin %title was enabled.', array('@plugin' => $plugin['title'])), + 'disable' => t('@plugin %title was disabled.', array('@plugin' => $plugin['title'])), + ), + ), + ); +} + +/** + * Get the class to handle creating a list of exportable items. + * + * @return + * Either the lister class or FALSE if one could not be had. + */ +function ctools_export_ui_get_handler($plugin) { + $cache = &ctools_static(__FUNCTION__, array()); + if (empty($cache[$plugin['name']])) { + // If a list class is not specified by the plugin, fall back to the + // default ctools_export_ui plugin instead. + if (empty($plugin['list'])) { + $default = ctools_get_export_ui('ctools_export_ui'); + $class = ctools_plugin_get_class($default, 'handler'); + } + else { + $class = ctools_plugin_get_class($plugin, 'handler'); + } + + if ($class) { + $cache[$plugin['name']] = new $class(); + $cache[$plugin['name']]->init($plugin); + } + } + return !empty($cache[$plugin['name']]) ? $cache[$plugin['name']] : FALSE; +} + +/** + * Get the base path from a plugin. + * + * @param $plugin + * The plugin. + * + * @return + * The menu path to the plugin's list. + */ +function ctools_export_ui_plugin_base_path($plugin) { + return $plugin['menu']['menu prefix'] . '/' . $plugin['menu']['menu item']; +} + +/** + * Get the path to a specific menu item from a plugin. + * + * @param $plugin + * The plugin name. + * @param $item_id + * The id in the menu items from the plugin. + * @param $export_key + * The export key of the item being edited, if it exists. + * @return + * The menu path to the plugin's list. + */ +function ctools_export_ui_plugin_menu_path($plugin, $item_id, $export_key = NULL) { + $path = $plugin['menu']['items'][$item_id]['path']; + if ($export_key) { + $path = str_replace('%ctools_export_ui', $export_key, $path); + } + return ctools_export_ui_plugin_base_path($plugin) . '/' . $path; +} + +/** + * Helper function to include CTools plugins and get an export-ui exportable. + * + * @param $plugin_name + * The plugin that should be laoded. + */ +function ctools_get_export_ui($plugin_name) { + ctools_include('plugins'); + return ctools_get_plugins('ctools', 'export_ui', $plugin_name); + +} + +/** + * Helper function to include CTools plugins and get all export-ui exportables. + */ +function ctools_get_export_uis() { + ctools_include('plugins'); + return ctools_get_plugins('ctools', 'export_ui'); +} + +/** + * Main page callback to manipulate exportables. + * + * This simply loads the object defined in the plugin and hands it off to + * a method based upon the name of the operation in use. This can easily + * be used to add more ops. + */ +function ctools_export_ui_switcher_page($plugin_name, $op) { + $args = func_get_args(); + $js = !empty($_REQUEST['ctools_ajax']); + + // Load the $plugin information + $plugin = ctools_get_export_ui($plugin_name); + + // If we need to do a token test, do it here. + if (!empty($plugin['allowed operations'][$op]['token']) && (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], $op))) { + return MENU_ACCESS_DENIED; + } + + $handler = ctools_export_ui_get_handler($plugin); + if ($handler) { + $method = $op . '_page'; + if (method_exists($handler, $method)) { + // replace the first two arguments: + $args[0] = $js; + $args[1] = $_POST; + return call_user_func_array(array($handler, $method), $args); + } + } + else { + return t('Configuration error. No handler found.'); + } +} diff --git includes/export-ui.menu.inc includes/export-ui.menu.inc new file mode 100644 index 0000000..fdeb2aa --- /dev/null +++ includes/export-ui.menu.inc @@ -0,0 +1,16 @@ +hook_menu($items); + } + } +} diff --git includes/export.inc includes/export.inc index b465d38..ef3c45a 100644 --- includes/export.inc +++ includes/export.inc @@ -19,6 +19,242 @@ define('EXPORT_IN_DATABASE', 0x01); define('EXPORT_IN_CODE', 0x02); /** + * @defgroup export_crud CRUD functions for export. + * @{ + * export.inc supports a small number of CRUD functions that should always + * work for every exportable object, no matter how complicated. These + * functions allow complex objects to provide their own callbacks, but + * in most cases, the default callbacks will be used. + * + * Note that defaults are NOT set in the $schema because it is presumed + * that a module's personalized CRUD functions will already know which + * $table to use and not want to clutter up the arguments with it. + */ + +/** + * Create a new object for the given $table. + * + * @param $table + * The name of the table to use to retrieve $schema values. This table + * must have an 'export' section containing data or this function + * will fail. + * @param $set_defaults + * If TRUE, which is the default, then default values will be retrieved + * from schema fields and set on the object. + * + * @return + * The loaded object. + */ +function ctools_export_crud_new($table, $set_defaults = TRUE) { + $schema = ctools_export_get_schema($table); + $export = $schema['export']; + + if (!empty($export['create callback']) && function_exists($export['create callback'])) { + return $export['create callback']($set_defaults); + } + else { + return ctools_export_new_object($table, $set_defaults); + } +} + +/** + * Load a single exportable object. + * + * @param $table + * The name of the table to use to retrieve $schema values. This table + * must have an 'export' section containing data or this function + * will fail. + * @param $name + * The unique ID to load. The field for this ID will be specified by + * the export key, which normally defaults to 'name'. + * + * @return + * The loaded object. + */ +function ctools_export_crud_load($table, $name) { + $schema = ctools_export_get_schema($table); + $export = $schema['export']; + + if (!empty($export['load callback']) && function_exists($export['load callback'])) { + return $export['load callback']($name); + } + else { + $result = ctools_export_load_object($table, 'names', array($name)); + if (isset($result[$name])) { + return $result[$name]; + } + } +} + +/** + * Load all exportable objects of a given type. + * + * @param $table + * The name of the table to use to retrieve $schema values. This table + * must have an 'export' section containing data or this function + * will fail. + * @param $reset + * If true, the static cache of all objects will be flushed prior to + * loading all. This can be important on listing pages where items + * might have changed on the page load. + * @return + * An array of all loaded objects, keyed by the unique IDs of the export key. + */ +function ctools_export_crud_load_all($table, $reset = FALSE) { + $schema = ctools_export_get_schema($table); + $export = $schema['export']; + + if (!empty($export['load all callback']) && function_exists($export['load all callback'])) { + return $export['load all callback']($reset); + } + else { + return ctools_export_load_object($table, 'all'); + } +} + +/** + * Save a single exportable object. + * + * @param $table + * The name of the table to use to retrieve $schema values. This table + * must have an 'export' section containing data or this function + * will fail. + * @param $object + * The fully populated object to save. + * + * @return + * Failure to write a record will return FALSE. Otherwise SAVED_NEW or + * SAVED_UPDATED is returned depending on the operation performed. The + * $object parameter contains values for any serial fields defined by the $table + */ +function ctools_export_crud_save($table, &$object) { + $schema = ctools_export_get_schema($table); + $export = $schema['export']; + + if (!empty($export['save callback']) && function_exists($export['save callback'])) { + return $export['save callback']($object); + } + else { + // Objects should have a serial primary key. If not, simply fail to write. + if (empty($export['primary key'])) { + return FALSE; + } + + $key = $export['primary key']; + $update = (isset($object->{$key})) ? array($key) : array(); + return drupal_write_record($table, $object, $update); + } +} + +/** + * Delete a single exportable object. + * + * This only deletes from the database, which means that if an item is in + * code, then this is actually a revert. + * + * @param $table + * The name of the table to use to retrieve $schema values. This table + * must have an 'export' section containing data or this function + * will fail. + * @param $object + * The fully populated object to delete, or the export key. + */ +function ctools_export_crud_delete($table, $object) { + $schema = ctools_export_get_schema($table); + $export = $schema['export']; + + if (!empty($export['delete callback']) && function_exists($export['delete callback'])) { + return $export['delete callback']($object); + } + else { + // If we were sent an object, get the export key from it. Otherwise + // assume we were sent the export key. + $value = is_object($object) ? $object->{$export['key']} : $object; + db_query("DELETE FROM {$table} WHERE " . $export['key'] . " = '%s'", $value); + } +} + +/** + * Get the exported code of a single exportable object. + * + * @param $table + * The name of the table to use to retrieve $schema values. This table + * must have an 'export' section containing data or this function + * will fail. + * @param $object + * The fully populated object to delete, or the export key. + * @param $indent + * Any indentation to apply to the code, in case this object is embedded + * into another, for example. + * + * @return + * A string containing the executable export of the object. + */ +function ctools_export_crud_export($table, $object, $indent = '') { + $schema = ctools_export_get_schema($table); + $export = $schema['export']; + + if (!empty($export['export callback']) && function_exists($export['export callback'])) { + return $export['export callback']($object, $indent); + } + else { + return ctools_export_object($table, $object, $indent); + } +} + +/** + * Turn exported code into an object. + * + * Note: If the code is poorly formed, this could crash and there is no + * way to prevent this. + * + * @param $table + * The name of the table to use to retrieve $schema values. This table + * must have an 'export' section containing data or this function + * will fail. + * @param $code + * The code to eval to create the object. + * + * @return + * An object created from the export. This object will NOT have been saved + * to the database. In the case of failure, a string containing all errors + * that the system was able to determine. + */ +function ctools_export_crud_import($table, $code) { + $schema = ctools_export_get_schema($table); + $export = $schema['export']; + + if (!empty($export['import callback']) && function_exists($export['import callback'])) { + return $export['import callback']($object, $indent); + } + else { + ob_start(); + eval($code); + ob_end_clean(); + + if (empty(${$export['identifier']})) { + $errors = ob_get_contents(); + if (empty($errors)) { + $errors = t('No item found.'); + } + return $errors; + } + + $item = ${$export['identifier']}; + + // Set these defaults just the same way that ctools_export_new_object sets them. + $item->export_type = EXPORT_IN_DATABASE; + $item->type = t('Local'); + + return $item; + } +} + +/** + * @} + */ + +/** * Load some number of exportable objects. * * This function will cache the objects, load subsidiary objects if necessary, @@ -442,27 +678,47 @@ function ctools_export_object($table, $object, $indent = '', $identifier = NULL, * that it's easily available. */ function ctools_export_get_schema($table) { - $schema = drupal_get_schema($table); - - if (!isset($schema['export'])) { - $schema['export'] = array(); - } - - // Add some defaults - $schema['export'] += array( - 'key' => 'name', - 'object' => 'stdClass', - 'status' => 'default_' . $table, - 'default hook' => 'default_' . $table, - 'can disable' => TRUE, - 'identifier' => $table, - 'bulk export' => TRUE, - 'export callback' => "$schema[module]_export_{$table}", - 'list callback' => "$schema[module]_{$table}_list", - 'to hook code callback' => "$schema[module]_{$table}_to_hook_code", - ); + $cache = &ctools_static(__FUNCTION__); + if (empty($cache[$table])) { + $schema = drupal_get_schema($table); + + if (!isset($schema['export'])) { + $schema['export'] = array(); + } - return $schema; + // Add some defaults + $schema['export'] += array( + 'key' => 'name', + 'object' => 'stdClass', + 'status' => 'default_' . $table, + 'default hook' => 'default_' . $table, + 'can disable' => TRUE, + 'identifier' => $table, + 'bulk export' => TRUE, + 'list callback' => "$schema[module]_{$table}_list", + 'to hook code callback' => "$schema[module]_{$table}_to_hook_code", + ); + + // Notes: + // The following callbacks may be defined to override default behavior + // when using CRUD functions: + // + // create callback + // load callback + // load all callback + // save callback + // delete callback + // export callback + // import callback + // + // See the appropriate ctools_export_crud function for details on what + // arguments these callbacks should accept. Please do not call these + // directly, always use the ctools_export_crud_* wrappers to ensure + // that default implementations are honored. + $cache[$table] = $schema; + } + + return $cache[$table]; } /** @@ -609,7 +865,7 @@ function ctools_export_to_hook_code(&$code, $table, $names = array(), $name = 'f $output .= "function " . $name . "_{$export['default hook']}() {\n"; $output .= " \${$export['identifier']}s = array();\n\n"; foreach ($objects as $object) { - $output .= $export['export callback']($object, ' '); // if this function does not exist, better to error out than fail silently + $output .= ctools_export_crud_export($table, $object, ' '); $output .= " \${$export['identifier']}s['" . check_plain($object->$export['key']) . "'] = \${$export['identifier']};\n\n"; } $output .= " return \${$export['identifier']}s;\n"; diff --git includes/form.inc includes/form.inc index 3d6f7d5..2c4500b 100644 --- includes/form.inc +++ includes/form.inc @@ -303,6 +303,10 @@ function ctools_validate_form($form_id, $form, &$form_state) { } } + if (!empty($form_state['clicked_button']['#skip validation'])) { + return; + } + _form_validate($form, $form_state, $form_id); $validated_forms[$form_id] = TRUE; } diff --git includes/wizard.inc includes/wizard.inc index c5b1130..c3ec6b9 100644 --- includes/wizard.inc +++ includes/wizard.inc @@ -232,6 +232,7 @@ function ctools_wizard_wrapper(&$form, &$form_state) { '#next' => $form_state['previous'], '#wizard type' => 'next', '#weight' => -2000, + '#skip validation' => TRUE, // hardcode the submit so that it doesn't try to save data. '#submit' => array('ctools_wizard_submit'), '#attributes' => $button_attributes, @@ -320,14 +321,14 @@ function ctools_wizard_wrapper(&$form, &$form_state) { if (count($params) > 1) { $url = array_shift($params); $options = array(); - + $keys = array(0 => 'query', 1 => 'fragment'); foreach ($params as $key => $value) { if (isset($keys[$key]) && isset($value)) { $options[$keys[$key]] = $value; } } - + $params = array($url, $options); } $form['#action'] = call_user_func_array('url', $params); @@ -379,7 +380,7 @@ function ctools_wizard_get_path($form_info, $step) { if (is_array($form_info['path'])) { foreach ($form_info['path'] as $id => $part) { $form_info['path'][$id] = str_replace('%step', $step, $form_info['path'][$id]); - } + } return $form_info['path']; } else { diff --git js/auto-submit.js js/auto-submit.js new file mode 100644 index 0000000..fa35bfb --- /dev/null +++ js/auto-submit.js @@ -0,0 +1,66 @@ +// $Id: auto-submit.js,v 1.1.2.1 2010/02/17 01:09:46 merlinofchaos Exp $ + +/** + * To make a form auto submit, all you have to do is 3 things: + * + * ctools_add_js('auto-submit'); + * + * On gadgets you want to auto-submit when changed, add the ctools-auto-submit + * class. With FAPI, add: + * @code + * '#attributes' => array('class' => 'ctools-auto-submit'), + * @endcode + * + * Finally, you have to identify which button you want clicked for autosubmit. + * The behavior of this button will be honored if it's ajaxy or not: + * @code + * '#attributes' => array('class' => 'ctools-use-ajax ctools-auto-submit-click'), + * @endcode + * + * Currently only 'select' and 'textfield' types are supported. We probably + * could use additional support for radios and checkboxes. + */ + +Drupal.behaviors.CToolsAutoSubmit = function() { + var timeoutID = 0; + + // Bind to any select widgets that will be auto submitted. + $('select.ctools-auto-submit:not(.ctools-auto-submit-processed)') + .addClass('.ctools-auto-submit-processed') + .change(function() { + $(this.form).find('.ctools-auto-submit-click').click(); + }); + + // Bind to any textfield widgets that will be auto submitted. + $('input[type=text].ctools-auto-submit:not(.ctools-auto-submit-processed)') + .addClass('.ctools-auto-submit-processed') + .keyup(function(e) { + var form = this.form; + switch (e.keyCode) { + case 16: // shift + case 17: // ctrl + case 18: // alt + case 20: // caps lock + case 33: // page up + case 34: // page down + case 35: // end + case 36: // home + case 37: // left arrow + case 38: // up arrow + case 39: // right arrow + case 40: // down arrow + case 9: // tab + case 13: // enter + case 27: // esc + return false; + default: + if (!$(form).hasClass('ctools-ajaxing')) { + if ((timeoutID)) { + clearTimeout(timeoutID); + } + + timeoutID = setTimeout(function() { $(form).find('.ctools-auto-submit-click').click(); }, 300); + } + } + }); +} diff --git plugins/export_ui/ctools_export_ui.class.php plugins/export_ui/ctools_export_ui.class.php new file mode 100644 index 0000000..666fa17 --- /dev/null +++ plugins/export_ui/ctools_export_ui.class.php @@ -0,0 +1,1094 @@ +plugin = $plugin; + } + + // ------------------------------------------------------------------------ + // Menu item manipulation + + /** + * hook_menu() entry point. + * + * Child implementations that need to add or modify menu items should + * probably call parent::hook_menu($items) and then modify as needed. + */ + function hook_menu(&$items) { + $prefix = ctools_export_ui_plugin_base_path($this->plugin); + + $my_items = array(); + foreach ($this->plugin['menu']['items'] as $item) { + // Add menu item defaults. + $item += array( + 'file' => 'export-ui.inc', + 'file path' => drupal_get_path('module', 'ctools') . '/includes', + ); + + $path = !empty($item['path']) ? $prefix . '/' . $item['path'] : $prefix; + unset($item['path']); + $my_items[$path] = $item; + } + + $items += $my_items; + } + + /** + * Menu callback to determine if an operation is accessible. + * + * This function enforces a basic access check on the configured perm + * string, and then additional checks as needed. + * + * @param $op + * The 'op' of the menu item, which is defined by 'allowed operations' + * and embedded into the arguments in the menu item. + * @param $item + * If an op that works on an item, then the item object, otherwise NULL. + * + * @return + * TRUE if the current user has access, FALSE if not. + */ + function access($op, $item) { + if (!user_access($this->plugin['access'])) { + return FALSE; + } + + switch ($op) { + case 'import': + return user_access('use PHP for block visibility'); + case 'revert': + return ($item->export_type & EXPORT_IN_DATABASE) && ($item->export_type & EXPORT_IN_CODE); + case 'delete': + return ($item->export_type & EXPORT_IN_DATABASE) && !($item->export_type & EXPORT_IN_CODE); + case 'disable': + return empty($item->disabled); + case 'enable': + return !empty($item->disabled); + default: + return TRUE; + } + } + + // ------------------------------------------------------------------------ + // These methods are the API for generating the list of exportable items. + + /** + * Master entry point for handling a list. + * + * It is unlikely that a child object will need to override this method, + * unless the listing mechanism is going to be highly specialized. + */ + function list_page($js, $input) { + $this->items = ctools_export_crud_load_all($this->plugin['schema']); + + // Respond to a reset command by clearing session and doing a drupal goto + // back to the base URL. + if (isset($input['op']) && $input['op'] == t('Reset')) { + unset($_SESSION['ctools_export_ui'][$this->plugin['name']]); + if (!$js) { + return drupal_goto($_GET['q']); + } + // clear everything but form id, form build id and form token: + $keys = array_keys($input); + foreach ($keys as $id) { + if (!in_array($id, array('form_id', 'form_build_id', 'form_token'))) { + unset($input[$id]); + } + } + $replace_form = TRUE; + } + + // If there is no input, check to see if we have stored input in the + // session. + if (!isset($input['form_id'])) { + if (isset($_SESSION['ctools_export_ui'][$this->plugin['name']]) && is_array($_SESSION['ctools_export_ui'][$this->plugin['name']])) { + $input = $_SESSION['ctools_export_ui'][$this->plugin['name']]; + } + } + else { + $_SESSION['ctools_export_ui'][$this->plugin['name']] = $input; + unset($_SESSION['ctools_export_ui'][$this->plugin['name']]['q']); + } + + // This is where the form will put the output. + $this->rows = array(); + $this->sorts = array(); + + $form_state = array( + 'plugin' => $this->plugin, + 'input' => $input, + 'rerender' => TRUE, + 'no_redirect' => TRUE, + 'object' => &$this, + ); + + ctools_include('form'); + $form = ctools_build_form('ctools_export_ui_list_form', $form_state); + + $output = $this->list_header($form_state) . $this->list_render($form_state) . $this->list_footer($form_state); + + if (!$js) { + $this->list_css(); + return $form . $output; + } + + ctools_include('ajax'); + $commands = array(); + $commands[] = ctools_ajax_command_replace('#ctools-export-ui-list-items', $output); + if (!empty($replace_form)) { + $commands[] = ctools_ajax_command_replace('#ctools-export-ui-list-form', $form); + } + ctools_ajax_render($commands); + } + + /** + * Create the filter/sort form at the top of a list of exports. + * + * This handles the very default conditions, and most lists are expected + * to override this and call through to parent::list_form() in order to + * get the base form and then modify it as necessary to add search + * gadgets for custom fields. + */ + function list_form(&$form, &$form_state) { + // This forces the form to *always* treat as submitted which is + // necessary to make it work. + $form['#token'] = FALSE; + if (empty($form_state['input'])) { + $form["#post"] = TRUE; + } + + // Add the 'q' in if we are not using clean URLs or it can get lost when + // using this kind of form. + if (!variable_get('clean_url', FALSE)) { + $form['q'] = array( + '#type' => 'hidden', + '#value' => $_GET['q'], + ); + } + + $all = array('all' => t('- All -')); + + $form['top row'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + + $form['bottom row'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + + $form['top row']['storage'] = array( + '#type' => 'select', + '#title' => t('Storage'), + '#options' => $all + array( + t('Normal') => t('Normal'), + t('Default') => t('Default'), + t('Overridden') => t('Overridden'), + ), + '#default_value' => 'all', + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['top row']['disabled'] = array( + '#type' => 'select', + '#title' => t('Enabled'), + '#options' => $all + array( + '0' => t('Enabled'), + '1' => t('Disabled') + ), + '#default_value' => 'all', + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['top row']['search'] = array( + '#type' => 'textfield', + '#title' => t('Search'), + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['bottom row']['order'] = array( + '#type' => 'select', + '#title' => t('Sort by'), + '#options' => $this->list_sort_options(), + '#default_value' => 'disabled', + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['bottom row']['sort'] = array( + '#type' => 'select', + '#title' => t('Order'), + '#options' => array( + 'asc' => t('Up'), + 'desc' => t('Down'), + ), + '#default_value' => 'asc', + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['bottom row']['submit'] = array( + '#type' => 'submit', + '#id' => 'ctools-export-ui-list-items-apply', + '#value' => t('Apply'), + '#attributes' => array('class' => 'ctools-use-ajax ctools-auto-submit-click'), + ); + + $form['bottom row']['reset'] = array( + '#type' => 'submit', + '#id' => 'ctools-export-ui-list-items-apply', + '#value' => t('Reset'), + '#attributes' => array('class' => 'ctools-use-ajax'), + ); + + ctools_add_js('ajax-responder'); + ctools_add_js('auto-submit'); + drupal_add_js('misc/jquery.form.js'); + ctools_add_js('export-ui-list.js'); + + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + } + + /** + * Validate the filter/sort form. + * + * It is very rare that a filter form needs validation, but if it is + * needed, override this. + */ + function list_form_validate(&$form, &$form_state) { } + + /** + * Submit the filter/sort form. + * + * This submit handler is actually responsible for building up all of the + * rows that will later be rendered, since it is doing the filtering and + * sorting. + * + * For the most part, you should not need to override this method, as the + * fiddly bits call through to other functions. + */ + function list_form_submit(&$form, &$form_state) { + // Filter and re-sort the pages. + $plugin = $this->plugin; + + $prefix = ctools_export_ui_plugin_base_path($plugin); + + foreach ($this->items as $name => $item) { + // Call through to the filter and see if we're going to render this + // row. If it returns TRUE, then this row is filtered out. + if ($this->list_filter($form_state, $item)) { + continue; + } + + // Note: Creating this list seems a little clumsy, but can't think of + // better ways to do this. + $allowed_operations = drupal_map_assoc(array_keys($plugin['allowed operations'])); + $not_allowed_operations = array(); + + if ($item->type == t('Normal')) { + $not_allowed_operations[] = 'revert'; + } + elseif ($item->type == t('Overridden')) { + $not_allowed_operations[] = 'delete'; + } + else { + $not_allowed_operations[] = 'revert'; + $not_allowed_operations[] = 'delete'; + } + + $not_allowed_operations[] = empty($item->disabled) ? 'enable' : 'disable'; + + foreach ($not_allowed_operations as $op) { + // Remove the operations that are not allowed for the specific + // exportable. + unset($allowed_operations[$op]); + } + + $operations = array(); + + foreach ($allowed_operations as $op) { + $operations[$op] = array( + 'title' => $plugin['allowed operations'][$op]['title'], + 'href' => ctools_export_ui_plugin_menu_path($plugin, $op, $name), + ); + if (!empty($plugin['allowed operations'][$op]['ajax'])) { + $operations[$op]['attributes'] = array('class' => 'ctools-use-ajax'); + } + if (!empty($plugin['allowed operations'][$op]['token'])) { + $operations[$op]['query'] = array('token' => drupal_get_token($op)); + } + } + + $this->list_build_row($item, $form_state, $operations); + } + + // Now actually sort + if ($form_state['values']['sort'] == 'desc') { + arsort($this->sorts); + } + else { + asort($this->sorts); + } + + // Nuke the original. + $rows = $this->rows; + $this->rows = array(); + // And restore. + foreach ($this->sorts as $name => $title) { + $this->rows[$name] = $rows[$name]; + } + } + + /** + * Determine if a row should be filtered out. + * + * This handles the default filters for the export UI list form. If you + * added additional filters in list_form() then this is where you should + * handle them. + * + * @return + * TRUE if the item should be excluded. + */ + function list_filter($form_state, $item) { + if ($form_state['values']['storage'] != 'all' && $form_state['values']['storage'] != $item->type) { + return TRUE; + } + + if ($form_state['values']['disabled'] != 'all' && $form_state['values']['disabled'] != !empty($item->disabled)) { + return TRUE; + } + + if ($form_state['values']['search']) { + $search = strtolower($form_state['values']['search']); + foreach ($this->list_search_fields() as $field) { + if (strpos(strtolower($item->$field), $search) !== FALSE) { + $hit = TRUE; + break; + } + } + if (empty($hit)) { + return TRUE; + } + } + } + + /** + * Provide a list of fields to test against for the default "search" widget. + * + * This widget will search against whatever fields are configured here. By + * default it will attempt to search against the name, title and description fields. + */ + function list_search_fields() { + $fields = array( + $this->plugin['export']['key'], + ); + + if (!empty($this->plugin['export']['admin_title'])) { + $fields[] = $this->plugin['export']['admin_title']; + } + if (!empty($this->plugin['export']['admin_description'])) { + $fields[] = $this->plugin['export']['admin_description']; + } + + return $fields; + } + + /** + * Provide a list of sort options. + * + * Override this if you wish to provide more or change how these work. + * The actual handling of the sorting will happen in build_row(). + */ + function list_sort_options() { + if (!empty($this->plugin['export']['admin_title'])) { + $options = array( + 'disabled' => t('Enabled, title'), + $this->plugin['export']['admin_title'] => t('Title'), + ); + } + else { + $options = array( + 'disabled' => t('Enabled, name'), + ); + } + + $options += array( + 'name' => t('Name'), + 'storage' => t('Storage'), + ); + + return $options; + } + + /** + * Add listing CSS to the page. + * + * Override this if you need custom CSS for your list. + */ + function list_css() { + ctools_add_css('export-ui-list'); + } + + /** + * Build a row based on the item. + * + * By default all of the rows are placed into a table by the render + * method, so this is building up a row suitable for theme('table'). + * This doesn't have to be true if you override both. + */ + function list_build_row($item, &$form_state, $operations) { + // Set up sorting + $name = $item->{$this->plugin['export']['key']}; + + // Note: $item->type should have already been set up by export.inc so + // we can use it safely. + switch ($form_state['values']['order']) { + case 'disabled': + $this->sorts[$name] = empty($item->disabled) . $name; + break; + case 'title': + $this->sorts[$name] = $item->{$this->plugin['export']['admin_title']}; + break; + case 'name': + $this->sorts[$name] = $name; + break; + case 'storage': + $this->sorts[$name] = $item->type . $name; + break; + } + + $class = + + $this->rows[$name]['data'] = array(); + $this->rows[$name]['class'] = !empty($item->disabled) ? 'ctools-export-ui-disabled' : 'ctools-export-ui-enabled'; + + // If we have an admin title, make it the first row. + if ($this->plugin['export']['admin_title']) { + $this->rows[$name]['data'][] = array('data' => check_plain($item->{$this->plugin['export']['admin_title']}), 'class' => 'ctools-export-ui-title'); + } + $this->rows[$name]['data'][] = array('data' => check_plain($name), 'class' => 'ctools-export-ui-name'); + $this->rows[$name]['data'][] = array('data' => check_plain($item->type), 'class' => 'ctools-export-ui-storage'); + $this->rows[$name]['data'][] = array('data' => theme('links', $operations), 'class' => 'ctools-export-ui-operations'); + + // Add an automatic mouseover of the description if one exists. + if (!empty($this->plugin['export']['admin_description'])) { + $this->rows[$name]['title'] = $item->{$this->plugin['export']['admin_description']}; + } + } + + /** + * Provide the table header. + * + * If you've added columns via list_build_row() but are still using a + * table, override this method to set up the table header. + */ + function list_table_header() { + $header = array(); + if ($this->plugin['export']['admin_title']) { + $header[] = array('data' => t('Title'), 'class' => 'ctools-export-ui-title'); + } + + $header[] = array('data' => t('Name'), 'class' => 'ctools-export-ui-name'); + $header[] = array('data' => t('Storage'), 'class' => 'ctools-export-ui-storage'); + $header[] = array('data' => t('Operations'), 'class' => 'ctools-export-ui-operations'); + + return $header; + } + + /** + * Render all of the rows together. + * + * By default we place all of the rows in a table, and this should be the + * way most lists will go. + * + * Whatever you do if this method is overridden, the ID is important for AJAX + * so be sure it exists. + */ + function list_render(&$form_state) { + return theme('table', $this->list_table_header(), $this->rows, array('id' => 'ctools-export-ui-list-items')); + } + + /** + * Render a header to go before the list. + * + * This will appear after the filter/sort widgets. + */ + function list_header($form_state) { } + + /** + * Render a footer to go after thie list. + * + * This is a good place to add additional links. + */ + function list_footer($form_state) { } + + // ------------------------------------------------------------------------ + // These methods are the API for adding/editing exportable items + + function add_page($js, $input) { + $form_state = array( + 'plugin' => $this->plugin, + 'object' => &$this, + 'ajax' => $js, + 'item' => ctools_export_crud_new($this->plugin['schema']), + 'op' => 'add', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + ); + + return $this->edit_execute_form($form_state); + } + + /** + * Main entry point to edit an item. + * + * The default implementation simply uses a form, so this should be + * overridden for more complex implentations that need more than to display + * a simple form (like a view or a page manager page). + */ + function edit_page($js, $input, $item) { + $form_state = array( + 'plugin' => $this->plugin, + 'object' => &$this, + 'ajax' => $js, + 'item' => $item, + 'op' => 'edit', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + ); + + return $this->edit_execute_form($form_state); + } + + function clone_page($js, $input, $item) { + // To make a clone of an item, we first export it and then re-import it. + // Export the handler, which is a fantastic way to clean database IDs out of it. + $export = ctools_export_crud_export($this->plugin['schema'], $item); + $item = ctools_export_crud_import($this->plugin['schema'], $export); + $item->{$this->plugin['export']['key']} = 'clone_of_' . $item->name; + + $form_state = array( + 'plugin' => $this->plugin, + 'object' => &$this, + 'ajax' => $js, + 'item' => $item, + 'op' => 'add', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + ); + + return $this->edit_execute_form($form_state); + } + + /** + * Execute the form. + * + * Add and Edit both funnel into this, but they have a few different + * settings. + */ + function edit_execute_form($form_state) { + ctools_include('form'); + $output = ctools_build_form('ctools_export_ui_edit_item_form', $form_state); + if (!empty($form_state['executed'])) { + $this->edit_save_form($form_state); + } + + return $output; + } + + /** + * Called to save the final product from the edit form. + */ + function edit_save_form($form_state) { + $item = &$form_state['item']; + $export_key = $this->plugin['export']['key']; + + $result = ctools_export_crud_save($this->plugin['schema'], $item); + + if ($result) { + drupal_set_message(t('Saved @plugin %title.', array('@plugin' => $this->plugin['title'], '%title' => $item->{$export_key}))); + } + else { + drupal_set_message(t('Could not save @plugin %title.', array('@plugin' => $this->plugin['title'], '%title' => $item->{$export_key})), 'error'); + } + } + + /** + * Provide the actual editing form. + */ + function edit_form(&$form, &$form_state) { + $export_key = $this->plugin['export']['key']; + $item = $form_state['item']; + + // Set title. + if ($form_state['op'] == 'edit') { + if ($item->export_type & EXPORT_IN_DATABASE) { + $title_op = $form_state['op']; + } + else { + $title_op = 'view'; + } + } + else { + $title_op = $form_state['op']; + } + + // Replace %title that might be there with the exportable title. + drupal_set_title(str_replace('%title', $item->{$export_key}, $this->plugin['form']['string']['title'][$title_op])); + + // TODO: Drupal 7 has a nifty method of auto guessing names from + // titles that is standard. We should integrate that here as a + // nice standard. + // Guess at a couple of our standard fields. + if (!empty($this->plugin['export']['admin_title'])) { + $form['info'][$this->plugin['export']['admin_title']] = array( + '#type' => 'textfield', + '#title' => t('Administrative title'), + '#description' => t('This will appear in the administrative interface to easily identify it.'), + '#default_value' => $item->{$this->plugin['export']['admin_title']}, + ); + } + + $form['info'][$export_key] = array( + // TODO: Add human readable name on key in export.inc? + '#title' => ucfirst($export_key), + '#type' => 'textfield', + '#default_value' => $item->{$export_key}, + '#description' => t('The unique ID for this @export', array('@export' => $this->plugin['title'])), + '#required' => TRUE, + '#maxlength' => 255, + ); + + if ($form_state['op'] === 'edit') { + $form['info'][$export_key]['#disabled'] = TRUE; + $form['info'][$export_key]['#value'] = $item->{$export_key}; + } + else { + $form['info'][$export_key]['#element_validate'] = array('ctools_export_ui_edit_name_validate'); + } + + if (!empty($this->plugin['export']['admin_description'])) { + $form['info'][$this->plugin['export']['admin_description']] = array( + '#type' => 'textarea', + '#title' => t('Administrative description'), + '#default_value' => $item->{$this->plugin['export']['admin_description']}, + ); + } + + // Add plugin's form definitions. + if (!empty($this->plugin['form']['settings'])) { + // Pass $form by reference. + $this->plugin['form']['settings']($form, $this->plugin, $form_state['op'], $item); + } + + // Add the buttons if the wizard is not in use. + if (empty($form_state['form_info'])) { + // Make sure that whatever happens, the buttons go to the bottom. + $form['buttons']['#weight'] = 100; + + // Add buttons. + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + $form['buttons']['delete'] = array( + '#type' => 'submit', + '#value' => $item->export_type & EXPORT_IN_CODE ? t('Revert') : t('Delete'), + '#access' => $form_state['op'] === 'edit' && $item->export_type & EXPORT_IN_DATABASE, + '#submit' => 'ctools_export_ui_edit_name_validate', + ); + } + } + + function edit_form_validate(&$form, &$form_state) { } + + /** + * Handle the submission of the edit form. + * + * At this point, submission is successful. Our only responsibility is + * to copy anything out of values onto the item that we are able to edit. + * + * If the keys all match up to the schema, this method will not need to be + * overridden. + */ + function edit_form_submit(&$form, &$form_state) { + $plugin = $form_state['plugin']; + $export_key = $plugin['export']['key']; + $item = $form_state['item']; + + $schema = ctools_export_get_schema($plugin['schema']); + foreach (array_keys($schema['fields']) as $key) { + if(isset($form_state['values'][$key])) { + $item->{$key} = $form_state['values'][$key]; + } + } + } + + // ------------------------------------------------------------------------ + // These methods are the API for 'other' stuff with exportables such as + // enable, disable, import, export + + /** + * Callback to enable a page. + */ + function enable_page($js, $input, $item) { + return $this->set_item_state(FALSE, $js, $input, $item); + } + + /** + * Callback to disable a page. + */ + function disable_page($js, $input, $item) { + return $this->set_item_state(TRUE, $js, $input, $item); + } + + /** + * Set an item's state to enabled or disabled and output to user. + * + * If javascript is in use, this will rebuild the list and send that back + * as though the filter form had been executed. + */ + function set_item_state($state, $js, $input, $item) { + ctools_export_set_object_status($item, $state); + + if (!$js) { + drupal_goto(ctools_export_ui_plugin_base_path($this->plugin)); + } + else { + return $this->list_page($js, $input); + } + } + + /** + * Page callback to delete an exportable item. + */ + function delete_page($js, $input, $item) { + $form_state = array( + 'plugin' => $this->plugin, + 'object' => &$this, + 'ajax' => $js, + 'item' => $item, + 'op' => $item->export_type & EXPORT_IN_CODE ? 'revert' : 'delete', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + ); + + ctools_include('form'); + + $output = ctools_build_form('ctools_export_ui_delete_confirm_form', $form_state); + if (!empty($form_state['executed'])) { + ctools_export_crud_delete($this->plugin['schema'], $item); + // TODO: set up the right drupal_set_message here. + drupal_goto(ctools_export_ui_plugin_base_path($this->plugin)); + } + + return $output; + } + + /** + * Page callback to display export information for an exportable item. + */ + function export_page($js, $input, $item) { + drupal_set_title(str_replace('%title', $this->plugin['title'], $this->plugin['form']['string']['title']['export'])); + return drupal_get_form('ctools_export_form', ctools_export_crud_export($this->plugin['schema'], $item), t('Export')); + } + + /** + * Page callback to import information for an exportable item. + */ + function import_page($js, $input, $step = 'begin') { + // Import is basically a multi step wizard form, so let's go ahead and + // use CTools' wizard.inc for it. + + $form_info = array( + 'id' => 'ctools_export_ui_import', + 'path' => ctools_export_ui_plugin_base_path($this->plugin) . '/' . $this->plugin['menu']['items']['import']['path'] . '/%step', + 'return path' => ctools_export_ui_plugin_base_path($this->plugin), + 'show trail' => TRUE, + 'show back' => TRUE, + 'show return' => FALSE, + 'finish callback' => 'ctools_export_ui_import_finish', + 'cancel callback' => 'ctools_export_ui_import_cancel', + 'order' => array( + 'code' => t('Import code'), + 'edit' => t('Edit'), + ), + 'forms' => array( + 'code' => array( + 'form id' => 'ctools_export_ui_import_code' + ), + 'edit' => array( + 'form id' => 'ctools_export_ui_import_edit' + ), + ), + ); + + $form_state = array( + 'plugin' => $this->plugin, + 'input' => $input, + 'rerender' => TRUE, + 'no_redirect' => TRUE, + 'object' => &$this, + 'export' => '', + 'overwrite' => FALSE, + ); + + if ($step == 'code') { + // This is only used if the BACK button was hit. + if (!empty($_SESSION['ctools_export_ui_import'][$this->plugin['name']])) { + $form_state['item'] = $_SESSION['ctools_export_ui_import'][$this->plugin['name']]; + $form_state['export'] = $form_state['item']->export_ui_code; + $form_state['overwrite'] = $form_state['item']->export_ui_allow_overwrite; + } + } + else if ($step == 'begin') { + $step = 'code'; + if (!empty($_SESSION['ctools_export_ui_import'][$this->plugin['name']])) { + unset($_SESSION['ctools_export_ui_import'][$this->plugin['name']]); + } + } + else if ($step != 'code') { + $form_state['item'] = $_SESSION['ctools_export_ui_import'][$this->plugin['name']]; + $form_state['op'] = 'add'; + if (!empty($form_state['item']->export_ui_allow_overwrite)) { + // if allow overwrite was enabled, set this to 'edit' only if the key already existed. + $export_key = $this->plugin['export']['key']; + + if (ctools_export_crud_load($this->plugin['schema'], $form_state['item']->{$export_key})) { + $form_state['op'] = 'edit'; + } + } + } + + ctools_include('wizard'); + return ctools_wizard_multistep_form($form_info, $step, $form_state); + } + +} + +// ----------------------------------------------------------------------- +// Forms to be used with this class. +// +// Since Drupal's forms are completely procedural, these forms will +// mostly just be pass-throughs back to the object. + +/** + * Form callback to handle the filter/sort form when listing items. + * + * This simply loads the object defined in the plugin and hands it off. + */ +function ctools_export_ui_list_form(&$form_state) { + $form = array(); + $form_state['object']->list_form($form, $form_state); + return $form; +} + +/** + * Validate handler for ctools_export_ui_list_form. + */ +function ctools_export_ui_list_form_validate(&$form, &$form_state) { + $form_state['object']->list_form_validate($form, $form_state); +} + +/** + * Submit handler for ctools_export_ui_list_form. + */ +function ctools_export_ui_list_form_submit(&$form, &$form_state) { + $form_state['object']->list_form_submit($form, $form_state); +} + +/** + * Form callback to edit an exportable item. + * + * This simply loads the object defined in the plugin and hands it off. + */ +function ctools_export_ui_edit_item_form(&$form_state) { + $form = array(); + $form_state['object']->edit_form($form, $form_state); + return $form; +} + +/** + * Validate handler for ctools_export_ui_edit_item_form. + */ +function ctools_export_ui_edit_item_form_validate(&$form, &$form_state) { + $form_state['object']->edit_form_validate($form, $form_state); +} + +/** + * Submit handler for ctools_export_ui_edit_item_form. + */ +function ctools_export_ui_edit_item_form_submit(&$form, &$form_state) { + $form_state['object']->edit_form_submit($form, $form_state); +} + +/** + * Submit handler to delete for ctools_export_ui_edit_item_form + */ +function ctools_export_ui_edit_item_form_delete(&$form, &$form_state) { + $plugin = $form_state['plugin']; + $export_key = $plugin['export']['key']; + $item = $form_state['item']; + + $menu_prefix = ctools_export_ui_plugin_base_path($plugin); + $form_state['redirect'] = "$menu_prefix/list/$item->{$export_key}/delete"; +} + +/** + * Validate that an export item name is acceptable and unique during add. + */ +function ctools_export_ui_edit_name_validate($element, &$form_state) { + $plugin = $form_state['plugin']; + // Check for string identifier sanity + if (!preg_match('!^[a-z0-9_]+$!', $element['#value'])) { + form_error($element, t('The export id can only consist of lowercase letters, underscores, and numbers.')); + return; + } + + // Check for name collision + if ($exists = ctools_export_crud_load($plugin['schema'], $element['#value'])) { + form_error($element, t('A @plugin with this name already exists. Please choose another name or delete the existing export before creating a new one.', array('@plugin' => $plugin['title']))); + } +} + +/** + * Delete/Revert confirm form. + */ +function ctools_export_ui_delete_confirm_form(&$form_state) { + $plugin = $form_state['plugin']; + $item = $form_state['item']; + + $form = array(); + + $export_key = $plugin['export']['key']; + $question = str_replace('!action', $plugin['allowed operations'][$form_state['op']]['title'], $plugin['form']['string']['confirmation']['question']); + $question = str_replace('%title', $item->{$export_key}, $question); + + $form = confirm_form($form, + $question, + ctools_export_ui_plugin_base_path($plugin), + $plugin['form']['string']['confirmation'][$form_state['op']], + drupal_ucfirst($plugin['allowed operations'][$form_state['op']]['title']), t('Cancel') + ); + return $form; +} + +/** + * Import form. Provides simple helptext instructions and textarea for + * pasting a export definition. + * + * This is a wizard form so its input is slightly different. + */ +function ctools_export_ui_import_code(&$form, &$form_state) { + $plugin = $form_state['plugin']; + + $form['help'] = array( + '#type' => 'item', + '#value' => $plugin['form']['string']['help']['import'], + ); + + $form['import'] = array( + '#title' => t('@plugin object', array('@plugin' => $plugin['title'])), + '#type' => 'textarea', + '#rows' => 10, + '#required' => TRUE, + '#default_value' => $form_state['export'], + ); + + $form['overwrite'] = array( + '#title' => t('Allow import to overwrite an existing record.'), + '#type' => 'checkbox', + '#default_value' => $form_state['overwrite'], + ); +} + +/** + * Import edit form + * + * This is a wizard form so its input is slightly different. But it just + * passes through to the normal edit form. + */ +function ctools_export_ui_import_edit(&$form, &$form_state) { + $form_state['object']->edit_form($form, $form_state); +} + +/** + * Validate handler for ctools_export_ui_import_edit. + */ +function ctools_export_ui_import_edit_validate(&$form, &$form_state) { + $form_state['object']->edit_form_validate($form, $form_state); +} + +/** + * Submit handler for ctools_export_ui_import_edit. + */ +function ctools_export_ui_import_edit_submit(&$form, &$form_state) { + $form_state['object']->edit_form_submit($form, $form_state); +} + +/** + * Import form validate handler. + * + * Evaluates code and make sure it creates an object before we continue. + */ +function ctools_export_ui_import_code_validate($form, &$form_state) { + $plugin = $form_state['plugin']; + $item = ctools_export_crud_import($plugin['schema'], $form_state['values']['import']); + if (is_string($item)) { + form_error($form['import'], t('Unable to get an import from the code. Errors reported: @errors', array('@errors' => $item))); + return; + } + + $form_state['item'] = $item; + $form_state['item']->export_ui_allow_overwrite = $form_state['values']['overwrite']; + $form_state['item']->export_ui_code = $form_state['values']['import']; +} + +/** + * Submit callback for import form. + * + * Stores the item in the session. + */ +function ctools_export_ui_import_code_submit($form, &$form_state) { + $_SESSION['ctools_export_ui_import'][$form_state['plugin']['name']] = $form_state['item']; +} + +/** + * Wizard finish callback for import of exportable item. + */ +function ctools_export_ui_import_finish(&$form_state) { + // This indicates that overwrite was allowed, so we should delete the + // original item. + if ($form_state['op'] == 'edit') { + ctools_export_crud_delete($this->plugin['schema'], $form_state['item']); + } + + $form_state['object']->edit_save_form($form_state); + + // Clear temporary data from session. + unset($_SESSION['ctools_export_ui_import'][$form_state['plugin']['name']]); +} + +/** + * Wizard cancel callback for import of exportable item. + */ +function ctools_export_ui_import_cancel(&$form_state) { + // Clear temporary data from session. + unset($_SESSION['ctools_export_ui_import'][$form_state['plugin']['name']]); +} diff --git plugins/export_ui/ctools_export_ui.inc plugins/export_ui/ctools_export_ui.inc new file mode 100644 index 0000000..1f93710 --- /dev/null +++ plugins/export_ui/ctools_export_ui.inc @@ -0,0 +1,19 @@ + array( + 'class' => 'ctools_export_ui', + ), + // As this is the base class plugin, it shouldn't declare any menu items. + 'has menu' => FALSE, +);