'. t("Drupal, by default, does not take into account the possibility of multiple users wanting to update the same document (node) at the same time. There is consequently a danger that two users try to edit a node at the same time (a race condition), with one user overwriting the other's modifications.") .'

'; $output .= '

'. t("The checkout module offers active protection against concurrent edits. When a user begins to modify a document, it is considered being 'checked out' of the system for exclusive editing access by that user. It only becomes available to other users once the editor navigates out of the edit page - by submitting the content, viewing the node, or selecting another menu item. The system also takes into account that some users edit multiple documents at once, so there is a direct correlation between a browser session and the locks that are placed upon a document.") .'

'; $output .= '

'. t('If permission has been given at the access control page, it is possible for users to keep a document checked out when they submit their changes; they can therefore be sure that their document is locked between sessions.', array('!uri' => url('admin/user/access'))) .'

'; $output .= '

'. t('A cron job is provided which will automatically check in documents that have been checked out beyond a configurable period of time.') .'

'; return $output; case 'admin/content/node/checkout': return '

'. t("Below is a list of all checked out documents. Click on 'check-in' to release a editing lock.") .'

'; } if (arg(0) == 'user' && is_numeric(arg(1)) && arg(2) == 'checkout') { return '

'. t("Below is a list of all documents checked out by you. Click on 'check-in' to release a editing lock.") .'

'; } } /** * Implementation of hook_menu(). */ function checkout_menu($may_cache) { global $user; $items = array(); if ($may_cache) { $admin_access = user_access('administer checked out documents'); $items[] = array('path' => 'admin/content/node/checkout', 'title' => t('Checked out'), 'callback' => 'checkout_admin_overview', 'access' => $admin_access, 'weight' => 5, 'type' => MENU_LOCAL_TASK); $items[] = array('path' => 'admin/content/node/checkout/release', 'title' => t('Check-in content'), 'callback' => 'checkout_admin_release', 'access' => $admin_access, 'type' => MENU_CALLBACK); } else { if (arg(0) == 'user' && is_numeric(arg(1)) && $user->uid == arg(1)) { $user_access = user_access('check out documents'); $items[] = array('path' => 'user/'. arg(1) .'/checkout', 'title' => t('Track check-outs'), 'callback' => 'checkout_user_overview', 'callback arguments' => array(arg(1)), 'access' => $user_access, 'weight' => 5, 'type' => MENU_LOCAL_TASK); $items[] = array('path' => 'user/'. arg(1) .'/checkout/release', 'title' => t('Check-in content'), 'callback' => 'checkout_user_release', 'callback arguments' => array(arg(1)), 'access' => $user_access, 'type' => MENU_CALLBACK); } } return $items; } /** * Implementation of hook_form_alter(). */ function checkout_form_alter($form_id, &$form) { if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) { if (user_access('keep documents checked out')) { $node = $form['#node']; if ($node->nid) { // Place checkbox immediately after the log message $form['checkout'] = array( '#type' => 'checkbox', '#title' => t('Keep document checked out'), '#return_value' => 1, '#weight' => 20.1, '#default_value' => FALSE, '#description' => t('Check this box if you wish to keep this document locked for editing after submit.'), ); } } } else if ($form_id == 'node_configure') { // Make sure our element appears before the submit buttons $form['buttons']['#weight'] = 10; $period = array(0 => t('Disabled')) + drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval'); $form['checkout_clear'] = array( '#type' => 'select', '#title' => t('Automatic check-in'), '#default_value' => variable_get('checkout_clear', 0), '#options' => $period, '#description' => t('The period after which checked out documents will be automatically released.'), ); } } /** * Implementation of hook_nodeapi(). */ function checkout_nodeapi(&$node, $op, $teaser, $page) { global $user; switch ($op) { case 'prepare': if ($node->nid > 0) { $data = checkout_checkout($node->nid); if ($data && $data->uid != $user->uid) { checkout_message($data); // won't return } } break; case 'update': if (empty($node->checkout)) { checkout_release($node->nid); } else { db_query("UPDATE {checkout} SET persistent = 1 WHERE nid = %d", $node->nid); } break; case 'delete': checkout_release($node->nid); break; } } /** * Implementation of hook_cron(). * * Release nodes that have been locked longer than the configured period. */ function checkout_cron() { $checkout_clear = variable_get('checkout_clear', 0); if ($checkout_clear > 0) { $result = db_query('DELETE FROM {checkout} WHERE timestamp < %d', time() - $checkout_clear); if ($num = db_affected_rows($result)) { $message = t('Released @count document(s) checked out for more than @period.', array('@count' => $num, '@period' => format_interval($checkout_clear))); drupal_set_message($message); watchdog('checkout', $message); } } } /** * Implementation of hook_exit(). * * Release the node that has been last edited, but only if we're coming * directly from an edit page to protect against unlocking nodes while * editing/surfing in concurrent browser windows. */ function checkout_exit() { global $user; if ($user->uid) { $edit_types = array('edit', 'classify', 'outline'); $request = array_reverse(explode('/', request_uri())); $referer = array_reverse(explode('/', referer_uri())); if (!in_array($request[0], $edit_types) && in_array($referer[0], $edit_types)) { $nid = $referer[1]; if (!db_result(db_query("SELECT persistent FROM {checkout} WHERE nid = %d", $nid))) { checkout_release($nid); } } } } /** * Lock a node for editing. * * If the document isn't currently checked out the returned info object has * just one property: the user id of the current user. Otherwise it contains * the information about when and by whom the document was checked out. * * @param integer $nid * A node id. * * @return object A check-out info object. */ function checkout_checkout($nid) { global $user; db_lock_table('checkout'); $result = db_query("SELECT nid, uid, timestamp FROM {checkout} WHERE nid = %d", $nid); if (db_num_rows($result)) { $info = db_fetch_object($result); } else if (user_access('check out documents')) { db_query("INSERT INTO {checkout} (nid, uid, persistent, timestamp) VALUES (%d, %d, %d, %d)", $nid, $user->uid, 0, time()); $info = new stdClass; $info->uid = $user->uid; } else { $info = NULL; } db_unlock_tables(); return $info; } /** * Display an error message and send the user back to the node view. * * @param object $info * A check-out info object. * * @return void This function doesn't return. * * @see checkout_checkout() */ function checkout_message($info) { $username = theme('username', user_load(array('uid' => $info->uid))); $date = format_date($info->timestamp, 'medium'); $message = t('This document is locked for editing by !name since @date.', array('!name' => $username, '@date' => $date)); if (user_access('administer checked out documents')) { $uri = url("admin/content/node/checkout/release/$info->nid", 'destination='. request_uri()); $message .= '
'. t('Click here to check back in now.', array('!uri' => $uri)); } drupal_set_message($message, 'error'); drupal_goto("node/$info->nid"); } /** * Release a node. * * @param integer $nid * A node id. */ function checkout_release($nid) { db_query('DELETE FROM {checkout} WHERE nid = %d', $nid); } /** * Return a list of all checked out documents. * * @return string $output */ function checkout_admin_overview() { $header = array( array('data' => t('Title'), 'field' => 'n.title', 'sort' => 'asc'), array('data' => t('Username'), 'field' => 'u.name'), array('data' => t('Check-out date'), 'field' => 'c.timestamp'), t('Operations'), ); $rows = array(); $result = pager_query('SELECT c.nid, c.uid, c.timestamp, n.title, u.name FROM {checkout} c INNER JOIN {node} n ON n.nid = c.nid INNER JOIN {users} u ON u.uid = c.uid'. tablesort_sql($header), 50, 0, NULL); while ($data = db_fetch_object($result)) { $row = array(); $row[] = l($data->title, "node/$data->nid"); $row[] = theme('username', user_load(array('uid' => $data->uid))); $row[] = format_date($data->timestamp, 'small'); $row[] = l(t('check-in'), "admin/content/node/checkout/release/$data->nid"); $rows[] = $row; } $output = theme('table', $header, $rows, array('id' => 'checkout')); if (!$rows) { $output .= t('No documents checked out.'); } else if ($pager = theme('pager', array(), 50, 0)) { $output .= $pager; } return $output; } /** * Menu callback; releases a document editing lock. * * @param integer $nid * A node id. * * @return void This function doesn't return. */ function checkout_admin_release($nid) { checkout_release($nid); drupal_set_message(t('The editing lock has been released.')); drupal_goto('admin/content/node/checkout'); } /** * Return a list of a users's checked out documents. * * @param integer $uid * A user id. * * @return string $output */ function checkout_user_overview($uid) { $header = array( array('data' => t('Title'), 'field' => 'n.title', 'sort' => 'asc'), array('data' => t('Check-out date'), 'field' => 'c.timestamp'), t('Operations'), ); $rows = array(); $result = pager_query('SELECT c.nid, c.timestamp, n.title FROM {checkout} c INNER JOIN {node} n ON n.nid = c.nid WHERE c.uid = %d'. tablesort_sql($header), 50, 0, NULL, $uid); while ($data = db_fetch_object($result)) { $row = array(); $row[] = l($data->title, "node/$data->nid"); $row[] = format_date($data->timestamp, 'small'); $row[] = l(t('check-in'), "user/$uid/checkout/release/$data->nid"); $rows[] = $row; } $output = theme('table', $header, $rows, array('id' => 'checkout')); if (!$rows) { $output .= t('No documents checked out.'); } else if ($pager = theme('pager', array(), 50, 0)) { $output .= $pager; } return $output; } /** * Menu callback; releases a document editing lock. * * @param integer $uid * A user id. * @param integer $nid * A node id. * * @return void This function doesn't return. */ function checkout_user_release($uid, $nid) { checkout_release($nid); drupal_set_message(t('The editing lock has been released.')); drupal_goto("user/$uid/checkout"); }