diff --git a/scaffolding_example/images/edit-delete.png b/scaffolding_example/images/edit-delete.png new file mode 100644 index 0000000000000000000000000000000000000000..184f7628513448fc324a4e2e38ae9ffd2b01a9b2 Binary files /dev/null and b/scaffolding_example/images/edit-delete.png differ diff --git a/scaffolding_example/images/text-editor.png b/scaffolding_example/images/text-editor.png new file mode 100644 index 0000000000000000000000000000000000000000..9c25055277bb7ca59445dd0f261020ed457c96db Binary files /dev/null and b/scaffolding_example/images/text-editor.png differ diff --git a/scaffolding_example/scaffolding_example.admin.inc b/scaffolding_example/scaffolding_example.admin.inc new file mode 100644 index 0000000000000000000000000000000000000000..e43965debe651f035273baa678bb4d85cff474d3 --- /dev/null +++ b/scaffolding_example/scaffolding_example.admin.inc @@ -0,0 +1,363 @@ + $record) { + $form['records'][$record_id] = _scaffolding_example_overview_record_field($record); + } + + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save changes'), + '#disabled' => empty($records), + ); + + return $form; +} + +/** + * Builds the fields for a single record on the drag-and-drop overview form. + * + * This internal function should not be called outside the module, unless you're + * feeling particularly cheeky. + * + * @ingroup forms + * @see scaffolding_example_overview_form() + */ +function _scaffolding_example_overview_record_field($record) { + $form['record_id'] = array( + '#type' => 'hidden', + '#value' => $record['record_id'], + ); + + $form['title'] = array( + '#type' => 'markup', + '#value' => check_plain($record['title']), + ); + + $form['weight'] = array( + '#type' => 'weight', + '#default_value' => $record['weight'], + ); + + $form['operations'] = array( + '#type' => 'markup', + '#value' => _scaffolding_example_record_links($record), + ); + + return $form; +} + +/** + * Build the edit and delete links for a single record. + * + * @see scaffolding_example_overview_form() + * @see scaffolding_example_overview_pager() + */ +function _scaffolding_example_record_links($record) { + $path = drupal_get_path('module', 'scaffolding_example') . '/images/'; + $links['edit'] = array( + 'title' => theme('image', $path . 'text-editor.png', t('Edit')), + 'href' => 'admin/build/scaffolding_example/' . $record['record_id'] . '/edit', + 'html' => TRUE, + 'query' => drupal_get_destination(), + ); + $links['delete'] = array( + 'title' => theme('image', $path . 'edit-delete.png', t('Delete')), + 'href' => 'admin/build/scaffolding_example/' . $record['record_id'] . '/delete', + 'html' => TRUE, + 'query' => drupal_get_destination(), + ); + return theme('links', $links); +} + +/** + * General submit handler for the drag-and-drop overview form. + * + * Updates the weights of all records on the form. + * + * @ingroup formapi + * @see scaffolding_example_overview_form() + */ +function scaffolding_example_overview_form_submit($form, &$form_state) { + $records = $form_state['values']['records']; + foreach ($records as $record) { + scaffolding_example_record_save($record); + } +} + +/** + * Theme the drag-and-drop overview form. + * + * Arranges records in a table, and adds the css and js for draggable sorting. + * + * @ingroup themeable + * @ingroup forms + * @see scaffolding_example_overview_form() + */ +function theme_scaffolding_example_overview_form($form) { + // Each record has a 'weight' that can be used to arrange it in relation to + // other records. Drupal's tabledrag.js library allows users to control these + // weights by dragging and dropping the records in a list -- we just need to + // add identifying CSS classes to key elements in the table. + + $rows = array(); + foreach (element_children($form['records']) as $key) { + $row = array(); + + // Render the hidden 'record id' field and the title of the record into the + // same column of the row. + $row[] = drupal_render($form['records'][$key]['record_id']) . drupal_render($form['records'][$key]['title']); + + // Add an identifying CSS class to our weight field, as it's the one + // the tabledrag.js will be controlling. This can be anything we want it to + // be, we'll just tell the tabledrag.js library what it should look for. + $form['records'][$key]['weight']['#attributes']['class'] = 'scaffolding-example-weight'; + $row[] = drupal_render($form['records'][$key]['weight']); + + // Render the edit and delete links into their own column. + $row[] = drupal_render($form['records'][$key]['operations']); + + // Add the new row to our collection of rows, and give it the 'draggable' + // class, indicating that it should be... well, draggable. + $rows[] = array( + 'data' => $row, + 'class' => 'draggable', + ); + } + + // If there were no records found, note the fact so users don't get confused + // by a completely empty table. + if (count($rows) == 0) { + $rows[] = array(t('No records have been added.'), '', ''); + } + + // Render a list of header titles, and our array of rows, into a table. Even + // we've already rendered all of our records, we always call drupal_render() + // on the form itself after we're done, so hidden security fields and other + // elements (like buttons) will appear properly at the bottom of the form. + $header = array(t('Title'), t('Weight'), t('Operations')); + $output = theme('table', $header, $rows, array('id' => 'scaffolding-example-overview')); + $output .= drupal_render($form); + + // Now that we've built our output, tell Drupal to add the tabledrag.js library. + // We'll pass in the ID of the table, the behavior we want it to use, and the + // class that appears on each 'weight' form element it should be controlling. + drupal_add_tabledrag('scaffolding-example-overview', 'order', 'self', 'scaffolding-example-weight'); + + return $output; +} + +/** + * Builds a sortable, paged overview of all records. + * + * This version of the overview page doesn't allow administrators to re-order + * records, but it breaks up long lists into groups of 20, with a convenient + * pager at the bottom of the list of records. This style of overview page is + * very useful when you know you'll be managing dozens (or hundreds) of records. + * + * Because this version of the overview page uses Drupal's helper functions to + * build a paged table with clickable column-headers, it can't use the module's + * build-in scaffolding_example_record_load_all() function. Instead, it will + * build its own SQL in a way that works with the Pager and Sortable Table APIs. + * + * @see scaffolding_example_overview_form() + */ +function scaffolding_example_overview_pager() { + $sql = "SELECT * FROM {scaffolding_record}"; + $header = array( + array('data' => t('Title'), 'field' => 'title', 'sort' => 'asc'), + array('data' => t('Weight'), 'field' => 'weight'), + t('Operations'), + ); + $sql .= tablesort_sql($header); + + $limit = 10; + $result = pager_query($sql, $limit); + + while ($record = db_fetch_array($result)) { + $rows[] = check_plain($record['title']); + $rows[] = check_plain($record['weight']); + $rows[] = _scaffolding_example_record_links($record); + } + if (!isset($rows)) { + $rows[] = array(array('data' => t('No records have been added.'), 'colspan' => 3)); + } + + $output = theme('table', $header, $rows); + $output .= theme('pager', NULL, $limit); + + return $output; +} + +/** + * Build the record editing form. + * + * If a record is passed in, an edit form with both Save and Delete buttons will + * be built. Otherwise, a blank 'add new record' form, without the Delete button, + * will be built. + * + * @ingroup forms + * @see scaffolding_example_form_submit() + * @see scaffolding_example_form_delete() + */ +function scaffolding_example_form(&$form_state, $record = array()) { + // Set the default values for a new item. By using += rather than =, we + // only overwrite array keys that have not yet been set. It's safe to use + // on both an empty array, and an incoming array with full or partial data. + $record += array( + 'title' => '', + 'content' => '', + 'weight' => 0, + ); + + // If we're editing an existing record, we'll add a value field to the form + // containing the record's unique ID. + if (!empty($record['record_id'])) { + $form['record_id'] = array( + '#type' => 'value', + '#value' => $record['record_id'], + ); + } + + $form['title'] = array( + '#type' => 'textfield', + '#title' => t('Title'), + '#required' => TRUE, + '#default_value' => $record['title'], + ); + + $form['weight'] = array( + '#type' => 'weight', + '#title' => t('Weight'), + '#default_value' => $record['weight'], + ); + + $form['content'] = array( + '#type' => 'textarea', + '#title' => t('Content'), + '#required' => TRUE, + '#default_value' => $record['content'], + ); + + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + // Only show the delete button if we already have an ID. Set the delete + // button's submit handler to a custom function that should only fire if + // this button is clicked. In all other cases, the form will fall back to + // the default $form_id_submit() function. + if (!empty($record['record_id'])) { + $form['buttons']['delete'] = array( + '#type' => 'submit', + '#value' => t('Delete'), + '#submit' => array('scaffolding_example_form_delete'), + ); + } + + return $form; +} + +/** + * General submit handler for Scaffolding's add/edit form. + * + * Simply passes incoming form values on to the module's CRUD save function, + * then redirects to the overview form. + * + * @ingroup formapi + * @see scaffolding_example_form() + */ +function scaffolding_example_form_submit($form, &$form_state) { + $record = $form_state['values']; + scaffolding_example_record_save($record); + $form_state['redirect'] = 'admin/build/scaffolding_example'; +} + +/** + * Delete button submit handler for Scaffolding's add/edit form. + * + * Redirects to the 'delete record' confirmation page without performing any + * operations. + * + * @ingroup formapi + * @see scaffolding_example_form() + */ +function scaffolding_example_form_delete($form, &$form_state) { + $form_state['redirect'] = 'admin/build/scaffolding_example/' . $form_state['values']['record_id'] . '/delete'; +} + +/** + * Build the delete confirmation form. + * + * A simple wrapper around Drupal's core confirm_form() function. Adds a value + * field to store the ID of the record being deleted. + * + * @ingroup forms + * @see scaffolding_example_delete_confirm_submit() + * @see confirm_form() + */ +function scaffolding_example_delete_confirm(&$form_state, $record) { + $form['record_id'] = array( + '#type' => 'value', + '#value' => $record['record_id'], + ); + + return confirm_form($form, + t('Are you sure you want to delete %title?', array('%title' => $record['title'])), + isset($_GET['destination']) ? $_GET['destination'] : 'admin/build/scaffolding_example', + t('This action cannot be undone.'), + t('Delete'), + t('Cancel') + ); +} + +/** + * General submit handler for the delete confirmation form. + * + * Core's confirm_form() function adds the 'confirm' value element we check + * against to ensure the form was properly submitted. If it's there, delete + * the record and redirect to the overview form. + * + * @ingroup formapi + * @see scaffolding_example_form() + */ + +function scaffolding_example_delete_confirm_submit($form, &$form_state) { + if ($form_state['values']['confirm']) { + scaffolding_example_record_delete($form_state['values']['record_id']); + drupal_set_message(t('Your record was deleted.')); + } + $form_state['redirect'] = 'admin/build/scaffolding_example'; +} diff --git a/scaffolding_example/scaffolding_example.info b/scaffolding_example/scaffolding_example.info new file mode 100644 index 0000000000000000000000000000000000000000..4dfd13bf43cc4c80e4de7e75e1a4c2da44fd0591 --- /dev/null +++ b/scaffolding_example/scaffolding_example.info @@ -0,0 +1,4 @@ +; $Id$ +name = Scaffolding +description = Provides example scaffolding for a module that maintains its own database table, and exposes an editing interface for the module's data. +core = 6.x diff --git a/scaffolding_example/scaffolding_example.install b/scaffolding_example/scaffolding_example.install new file mode 100644 index 0000000000000000000000000000000000000000..2b0ac4d5ca9ddda524915e67b3129d7ebbafa13d --- /dev/null +++ b/scaffolding_example/scaffolding_example.install @@ -0,0 +1,106 @@ + t('Stores custom links to be added to nodes.'), + 'fields' => array( + 'record_id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => t('Unique identifier for the {scaffolding_record}.'), + ), + 'title' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => t("The visible title of the {scaffolding_record}.") + ), + 'content' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'description' => t('A description of the term.'), + ), + 'weight' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + 'description' => t('The weight of this {scaffolding_record}.'), + ), + ), + 'primary key' => array('record_id'), + ); + return $schema; +} + +/** + * Implementation of hook_update_N(). + * + * This function is responsible for updating the module's database tables when + * a new version requires changes. (For example, if version 1.1 of the module + * added a new field to the database). + * + * The numbers of your module's update functions should follow the pattern + * hook_update_XYZZ, where X is the version of Drupal your module is compatible + * with, Y is the major version of your module, and ZZ is the number of the + * update. For example, the first update for version 1.x of this module would + * be numbered 6100, while the first update for the 2.x version of the module + * would be numbered 6200. For more details on update numbering conventions, + * see http://drupal.org/node/114774#update-n. + * + * hook_update_N() functions only run when upgrading an already-installed module + * to a new version, NOT when initially installing the module. + */ +function scaffolding_example_update_6100() { + $new_column = array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + 'description' => t('The weight of this {scaffolding_record}.'), + ); + + $ret = array(); + db_add_field($ret, 'scaffolding_record', 'weight', $new_column); + return $ret; +} + +/** + * Implementation of hook_uninstall(). + * + * This hook is called when the already-disabled module is explicitly uninstalled + * by the administrator -- simple disabling the module will trigger hook_disable(). + * It should delete any database tables added by the module, remove any variables + * that are unique to the module, and clear out any cached data. + */ +function scaffolding_example_uninstall() { + drupal_uninstall_schema('scaffolding_example'); + cache_clear_all('scaffolding_example:*', 'cache', TRUE); + variable_del('scaffolding_example_setting'); +} diff --git a/scaffolding_example/scaffolding_example.module b/scaffolding_example/scaffolding_example.module new file mode 100644 index 0000000000000000000000000000000000000000..b0cff4298f849dfcd88a3a93d7f8b57dcf5ae81e --- /dev/null +++ b/scaffolding_example/scaffolding_example.module @@ -0,0 +1,200 @@ + 'Scaffolding', + 'description' => 'Manage records', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('scaffolding_example_overview_form'), + 'access arguments' => array('administer scaffolding data'), + 'file' => 'scaffolding_example.admin.inc', + ); + + $items['admin/build/scaffolding_example/sorted'] = array( + 'title' => 'Sorted list', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + + $items['admin/build/scaffolding_example/pager'] = array( + 'title' => 'Paged list', + 'description' => 'Manage records', + 'page callback' => 'scaffolding_example_overview_pager', + 'access arguments' => array('administer scaffolding data'), + 'type' => MENU_LOCAL_TASK, + 'weight' => -5, + 'file' => 'scaffolding_example.admin.inc', + ); + + $items['admin/build/scaffolding_example/add'] = array( + 'title' => 'Add record', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('scaffolding_example_form'), + 'access arguments' => array('administer scaffolding data'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'scaffolding_example.admin.inc', + ); + + $items['admin/build/scaffolding_example/%scaffolding_example_record/edit'] = array( + 'title' => 'Edit record', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('scaffolding_example_form', 3), + 'access arguments' => array('administer scaffolding data'), + 'type' => MENU_CALLBACK, + 'file' => 'scaffolding_example.admin.inc', + ); + + $items['admin/build/scaffolding_example/%scaffolding_example_record/delete'] = array( + 'title' => 'Delete record', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('scaffolding_example_delete_confirm', 3), + 'access arguments' => array('administer scaffolding data'), + 'type' => MENU_CALLBACK, + 'file' => 'scaffolding_example.admin.inc', + ); + + $items['scaffolding_example'] = array( + 'title' => 'Scaffolding records', + 'description' => 'Lists records saved by the Scaffolding Example module.', + 'page callback' => 'scaffolding_example_page', + 'access arguments' => array('view content'), + 'file' => 'scaffolding_example.pages.inc', + ); + return $items; +} + +/** + * Implementation of hook_perm(). + * + * Defines access permissions that may be assigned to roles and used to restrict + * access. + */ +function scaffolding_example_perm() { + return array('administer scaffolding data'); +} + +/** + * Implementation of hook_theme(). + * + * Returns information about every themable function defined by the module. + */ +function scaffolding_example_theme() { + $items = array(); + $items['scaffolding_example_overview_form'] = array( + 'arguments' => array('form' => array()), + 'file' => 'scaffolding_example.admin.inc', + ); + $items['scaffolding_example_record'] = array( + 'arguments' => array('record' => array()), + 'file' => 'scaffolding_example.pages.inc', + ); + return $items; +} + +/** + * Loader function for individual records. + * + * Because we use '%scaffolding_example_record' as a wildcard in our hook_menu() + * handler, this function will also be called automatically when we go to edit + * or delete a record. Thanks, Menu API!. + * + * @param $record_id + * An int containing the ID of a record. + * @return + * A single record in array format, or FALSE if none matched the incoming ID. + */ +function scaffolding_example_record_load($record_id) { + $sql = "SELECT * FROM {scaffolding_record} WHERE record_id = %d"; + $result = db_query($sql, $record_id); + if ($record = db_fetch_array($result)) { + return $record; + } + else { + return FALSE; + } +} + +/** + * Public loader function for the full collection of records. + * + * In situations where the module's data rarely changes, or is being used + * frequently (for example, loaded and processed on every page load), this + * is a prime candidate for caching. See The Beginner's Guide to Caching at + * http://www.lullabot.com/articles/a_beginners_guide_to_caching_data for more + * details. + * + * This function assumes that results should be sorted by 'weight' -- if your + * module doesn't store a weight column on its records, or if you need to sort + * on some other property, this function's SQL should be updated as well. + * + * @return + * An array of all records, keyed by id. + */ +function scaffolding_example_record_load_all() { + $sql = 'SELECT * FROM {scaffolding_record} ORDER BY weight ASC'; + $result = db_query($sql); + + $records = array(); + while ($record = db_fetch_array($result)) { + $records[$record['record_id']] = $record; + } + return $records; +} + +/** + * Inserts a new record, or updates an existing one. + * + * Automatically inserts or updates, based on whether the record's unique ID has + * been set yet. Because drupal_write_record() updates the record itself (adding + * the unique ID after the database has been updated), we return the record + * after saving it. + * + * This allows any calling function to check the id of the returned record and + * act on the ID as needed (redirecting to a 'view' page after inserting, etc). + * + * @param $record + * A record to be saved. If $record['record_id'] is set, the record will be updated. + * Otherwise, a new record will be inserted into the database. + * @return + * The saved record, with its ID set. + */ +function scaffolding_example_record_save($record) { + if (isset($record['record_id'])) { + drupal_write_record('scaffolding_record', $record, 'record_id'); + } + else { + drupal_write_record('scaffolding_record', $record); + } + return $record; +} + +/** + * Deletes a record, given its unique ID. + * + * @param $record_id + * An int containing the ID of a record. + */ +function scaffolding_example_record_delete($record_id) { + $sql = 'DELETE FROM {scaffolding_record} WHERE record_id = %d'; + db_query($sql, $record_id); +} diff --git a/scaffolding_example/scaffolding_example.pages.inc b/scaffolding_example/scaffolding_example.pages.inc new file mode 100644 index 0000000000000000000000000000000000000000..78ca7f7c97aa5d6327726a264870442c74098c84 --- /dev/null +++ b/scaffolding_example/scaffolding_example.pages.inc @@ -0,0 +1,42 @@ +'; + $output .= '

' . check_plain($record['title']) . '

'; + $output .= '

' . check_markup($record['content']) . '

'; + $output .= ''; + + return $output; +}