Newer
Older
* @file
* Allows users to submit questions to a queue that may be answered by site administrators
Alastair Aitchison
committed
*
Alastair Aitchison
committed
* @todo
* - Create upgrade path in hook_install_N
/************************************************
* *
* MENU ITEMS, PERMISSIONS, AND ACCESS CONTROL *
* *
***********************************************/
* Implements hook_permission().
* Defines the permissions that users can have related to this module
function question_permission() {
return array(
'ask questions' => array(
'title' => t('Ask questions'),
'description' => t('Submit a question to the site question queue.'),
),
'view questions' => array(
'title' => t('View questions'),
'description' => t('View all questions in the queue.'),
),
'answer questions' => array(
'title' => t('Answer questions'),
'description' => t('Respond to questions in the queue.'),
),
'delete questions' => array(
'title' => t('Delete questions'),
'description' => t('Delete questions from the queue.'),
Alastair Aitchison
committed
),
'edit own answers' => array(
'title' => t('Edit own answers'),
'description' => t('Edit own answers.'),
),
'edit any answers' => array(
'title' => t('Edit any answers'),
'description' => t('Edit any answers.'),
),
'delete own answers' => array(
'title' => t('Delete own answers'),
'description' => t('Delete own answers.'),
),
'delete any answers' => array(
'title' => t('Delete any answers'),
'description' => t('Delete any answers.'),
),
);
* Implements hook_node_access().
Alastair Aitchison
committed
function question_node_access($node, $op, $account) {
// Get the type of node being accessed
$type = is_string($node) ? $node : $node->type;
Alastair Aitchison
committed
// Only affect question nodes
if ($type != 'question') {
return NODE_ACCESS_IGNORE;
}
if ($op == 'create' && user_access('answer questions', $account)) {
return NODE_ACCESS_ALLOW;
}
if ($op == 'update' && user_access('edit any answers', $account)) {
return NODE_ACCESS_ALLOW;
}
if ($op == 'update' && ($account->uid == $node->uid) && user_access('edit own answers', $account)) {
return NODE_ACCESS_ALLOW;
}
if ($op == 'delete' && user_access('delete any answers', $account)) {
return NODE_ACCESS_ALLOW;
}
if ($op == 'delete' && ($account->uid == $node->uid) && user_access('delete own answers', $account)) {
return NODE_ACCESS_ALLOW;
}
return NODE_ACCESS_IGNORE;
}
* Implements hook_menu().
* Adds all the necessary menu items, and controls access
* to those items based on user permissions.
function question_menu() {
// The page for users to ask a question
Alastair Aitchison
committed
$items['question'] = array(
'title' => 'Ask a question', // note don't wrap in t() function from 6.x
'description' => 'Post a question to be answered.',
'page callback' => 'drupal_get_form',
'page arguments' => array('question_ask_form'),
Alastair Aitchison
committed
'access callback' => 'user_access',
'access arguments' => array('ask questions'),
);
// The queue of unanswered questions
$items['admin/content/question'] = array(
'title' => 'Questions',
'description' => 'Manage the sites question queue.',
'page callback' => 'drupal_get_form',
Alastair Aitchison
committed
'page arguments' => array('question_queue_admin'),
'access callback' => 'user_access',
'access arguments' => array('view questions'),
'type' => MENU_LOCAL_TASK,
);
// The settings page
$items['admin/config/content/question'] = array(
'title' => 'Question',
'description' => 'Edit Question settings.',
'page callback' => 'drupal_get_form',
'page arguments' => array('question_settings'),
'access arguments' => array('administer site configuration'),
'type' => MENU_NORMAL_ITEM,
);
Alastair Aitchison
committed
// Callback to delete a question
$items['question/%/delete'] = array(
'title' => 'Delete',
'page callback' => 'drupal_get_form',
'page arguments' => array('question_delete_confirm', 1),
'access callback' => 'user_access',
'access arguments' => array('delete questions'),
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_INLINE,
);
* Implements hook_menu_alter().
Alastair Aitchison
committed
* Removes the default link to create a question/answer page from the
* "Add new content" menu at node/add.
* Instead, we only want users to create answers by promoting questions from the queue.
function question_menu_alter(&$items) {
$items['node/add/question']['type'] = MENU_CALLBACK;
/************************************************
* *
* SETTINGS, HELP, AND INFORMATION *
* *
***********************************************/
* Implements hook_help().
function question_help($page, $arg) {
switch ($page) {
case 'node/add#question': // This description shows up when users click "create content."
return t('A question/answer node.');
case 'admin/content/questions':
return t('Here is a list of the questions that have been submitted to your site. Each question can be answered by selecting "promote". Once submitted, this question becomes a node. Choose "delete" to delete this question without answering.');
case 'admin/structure/trigger/question':
$explanation = t(
"A trigger is a system event. For the question module, you can choose to
associate actions when a question is submitted to the queue, or when a
question is answered.");
return "<p>$explanation</p>";
}
}
function question_settings() {
// Redirect path following question submit
$form['question_ask_form_redirect'] = array(
'#title' => t('Path to redirect node'),
'#default_value' => variable_get('question_ask_form_redirect', FALSE),
'#description' => t('This is where users will end up after they submit the question form. Example: "node/454".<br/>Leave blank and the user will be returned to the front page of the site.'),
);
Alastair Aitchison
committed
// Message to display after question submitted
$form['question_submission_message'] = array(
'#type' => 'textarea',
'#weight' => -2,
'#title' => t('Message to display'),
'#default_value' => variable_get('question_submission_message', FALSE),
'#size' => 40,
'#description' => t('This message will be shown to the user after submitting a question.<br/>Leave blank to display no message.'),
Alastair Aitchison
committed
// Instructions on the question ask form
$form['question_instructions'] = array(
'#type' => 'textarea',
'#weight' => -1,
'#title' => t('Instructions for the user'),
'#default_value' => variable_get('question_instructions', FALSE),
'#size' => 40,
'#description' => t('This message will appear above the question form to provide the user instructions.'),
);
/************************************************
* *
* USER QUESTION SUBMISSION *
* *
************************************************/
/**
* Form for users to ask a question
*/
function question_ask_form($node, &$form_state) {
// Display the instructions (if applicable)
$instructions = variable_get('question_instructions', FALSE);
if ($instructions) {
$form['instructions'] = array(
'#type' => 'markup',
'#markup' => check_plain($instructions),
);
Alastair Aitchison
committed
$form['question'] = array(
'#type' => 'textarea',
'#title' => t('Question'),
'#cols' => 50,
'#rows' => 5,
'#description' => NULL,
'#attributes' => NULL,
'#default_value' => isset($form_state['storage']['question']) ? $form_state['storage']['question'] : '[please enter a value]',
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Submit Question'),
);
$form['#method'] = 'post';
$form['#theme'] = 'question_ask_form';
Alastair Aitchison
committed
return $form;
}
/**
* Implements _form_validate.
*/
Alastair Aitchison
committed
function question_ask_form_validate(&$form, &$form_state) {
if ($form_state['values']['question'] == '') {
form_set_error('question', t('Please enter a question.'));
/**
* Implements _form_submit.
*/
function question_ask_form_submit($form, &$form_state) {
// Make an array containing all the values to insert
$row = array(
'question' => $form_state['values']['question'],
);
// Write the question to the database
drupal_write_record('question_queue', $row);
// Retrieve the question id assigned by the database to be used in future form processing
$form_state['storage']['qid'] = $row['qid'];
Alastair Aitchison
committed
// Determine where the browser should now go to
$path = variable_get('question_ask_form_redirect', '');
if (!empty($path)) {
// Redirect to the specified path
$form_state['redirect'] = $path;
}
else {
// Return to the site frontpage
$form_state['redirect'] = '<front>';
}
Alastair Aitchison
committed
// Display message to user
$message = variable_get('question_submission_message', '');
if (!empty($message)) {
drupal_set_message(check_plain($message));
}
Alastair Aitchison
committed
// Add an entry to the watchdog log
Alastair Aitchison
committed
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
watchdog('Question', 'Question submitted: %question.', array('%question' => $form_state['values']['question']), WATCHDOG_NOTICE);
}
/**
* Themes the question form
*/
function theme_question_ask_form($variables) {
$form = $variables['form'];
$output = '';
$output .= drupal_render($form['instructions']);
$output .= drupal_render($form['questioner']);
$output .= drupal_render($form['age']);
$output .= drupal_render($form['sex']);
$output .= drupal_render($form['question']);
$output .= drupal_render_children($form);
return $output;
}
/**
* Implements hook_theme().
*/
function question_theme() {
return array(
'question_ask_form' => array('render element' => 'form'),
);
/************************************************
* *
Alastair Aitchison
committed
* QUESTION QUEUE MANAGEMENT *
* *
************************************************ /
Alastair Aitchison
committed
* Provide an array of the bulk operations that can be
* perfomed on items in the question queue.
Alastair Aitchison
committed
* @see hook_node_operations()
function question_question_queue_operations() {
$operations = array();
if (user_access('delete questions')) {
$operations = array(
'delete' => array(
'label' => t('Delete selected questions'),
'callback' => NULL,
),
);
return $operations;
* Admin form for questions in the queue.
Alastair Aitchison
committed
* @see node_admin_content()
function question_queue_admin($form, &$form_state) {
if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') {
return question_queue_multiple_delete_confirm($form, $form_state, array_filter($form_state['values']['questions']));
}
$form['admin'] = question_queue_admin_form();
return $form;
* Return an array of fields to be included in the question queue.
* This is implemented as a separate hook so that it can be extended
* by other modules that add additional fields to the question queue.
function question_question_queue_fields() {
return array(
Alastair Aitchison
committed
// Defines the columns of data in the table
'header' => array(
'question' => array('data' => t('Question'), 'field' => 'question', 'sort' => 'desc'),
'operations' => array('data' => t('Operations')),
),
Alastair Aitchison
committed
// Defines any functions that should be performed on items of data in each column
'options' => array(
'question' => array('#markup' => 'check_plain'),
)
);
Alastair Aitchison
committed
* Lists all questions currently in the queue, and allows
* bulk operations to be performed against those questions.
Alastair Aitchison
committed
* @see node_admin_nodes()
function question_queue_admin_form() {
Alastair Aitchison
committed
$form['instructions'] = array(
'#type' => 'markup',
'#markup' => t('To respond to a question, click on the title.'),
);
// Build the 'Update options' dropdown.
$form['options'] = array(
'#type' => 'fieldset',
'#title' => t('Update options'),
'#prefix' => '<div class="container-inline">',
'#suffix' => '</div>',
);
$options = array();
$operations = array();
Alastair Aitchison
committed
// Invoke all modules that define operations that can be perfomed on the question queue
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
foreach (module_invoke_all('question_queue_operations') as $operation => $array) {
$options[$operation] = $array['label'];
}
$form['options']['operation'] = array(
'#type' => 'select',
'#options' => $options,
'#default_value' => 'delete',
);
$form['options']['submit'] = array(
'#type' => 'submit',
'#value' => t('Update'),
'#submit' => array('question_queue_admin_form_submit'),
);
// Invoke all modules that define fields that should be included
// in the question queue page
$question_queue_fields = module_invoke_all('question_queue_fields');
// Build the table header.
$header = $question_queue_fields['header'];
// Retrieve all questions currently in the queue
$query = db_select('question_queue', 'q')->extend('PagerDefault')->extend('TableSort');
$query->fields('q'); // Retrieve every field from the queue table
$query->addTag('question_queue'); // Add a tag so that other modules can alter this query
$query->orderByHeader($header);
$result = $query->execute();
Alastair Aitchison
committed
$destination = drupal_get_destination();
$options = array();
// Loop through all the questions
foreach ($result as $question) {
// Loop through every field on information about this question
foreach ($question as $column => $value) {
// Determine how the value in this field should be displayed
$markup = "";
Alastair Aitchison
committed
if (isset($question_queue_fields['options'][$column]['#markup'])) {
$markup = call_user_func($question_queue_fields['options'][$column]['#markup'], $value);
}
else {
$markup = check_plain($value);
}
// Add this column to the options array
$options[$question->qid][$column] = array('data' => array('#markup' => $markup));
}
Alastair Aitchison
committed
// Build a list of all the accessible operations for the current question.
$operations = array();
if (user_access('answer questions')) {
Alastair Aitchison
committed
$operations['respond'] = array(
'title' => t('respond'),
'href' => 'node/add/question/' . $question->qid,
'query' => $destination,
);
}
if (user_access('delete questions')) {
$operations['delete'] = array(
'title' => t('delete'),
'href' => 'question/' . $question->qid . '/delete',
'query' => $destination,
);
Alastair Aitchison
committed
$options[$question->qid]['operations'] = array();
if (count($operations) > 1) {
// Render an unordered list of operations links.
$options[$question->qid]['operations'] = array(
'data' => array(
'#theme' => 'links__node_operations',
'#links' => $operations,
'#attributes' => array('class' => array('links', 'inline')),
),
);
}
elseif (!empty($operations)) {
// Render the first and only operation as a link.
$link = reset($operations);
$options[$question->qid]['operations'] = array(
'data' => array(
'#type' => 'link',
'#title' => $link['title'],
'#href' => $link['href'],
'#options' => array('query' => $link['query']),
),
);
}
}
// Create the table
$form['questions'] = array(
'#type' => 'tableselect',
'#header' => $header,
'#options' => $options,
Alastair Aitchison
committed
'#empty' => t('No questions available.'),
Alastair Aitchison
committed
Alastair Aitchison
committed
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
/**
* Callback to delete a single question
*/
function question_delete_confirm($form, &$form_state, $qid) {
$form['qid'] = array('#type' => 'value', '#value' => $qid);
$question = db_query('SELECT question FROM {question_queue} WHERE qid = :qid', array(':qid' => $qid))->fetchField();
return confirm_form($form,
t('Are you sure you want to delete %question?', array('%question' => $question)),
'admin/content/question',
t('This action cannot be undone.'),
t('Delete'),
t('Cancel')
);
}
/**
* Execute node deletion
*/
function question_delete_confirm_submit($form, &$form_state) {
if ($form_state['values']['confirm']) {
$qid = $form_state['values']['qid'];
$num_deleted = db_delete('question_queue')
->condition('qid', $qid)
->execute();
watchdog('question', 'Deleted question %qid.', array('%qid' => $qid));
drupal_set_message(t('The question has been deleted.'));
}
}
* Perform updates against multiple question queue items.
* @see node_mass_update
function question_queue_mass_update($questions, $updates) {
foreach ($questions as $qid) {
_question_queue_mass_update_helper($qid, $updates);
drupal_set_message(t('The update has been performed.'));
/**
* Question Queue Mass Update - helper function.
*
Alastair Aitchison
committed
* @see _node_mass_update_helper()
*/
function _question_queue_mass_update_helper($qid, $updates) {
foreach ($updates as $name => $value) {
$query = db_update('question_queue')
->fields(array(
$name => $value,
))
->condition('qid', $qid, '=')
->execute();
/**
* Validate the question queue bulk update form.
*
* @see node_admin_nodes_validate
*/
function question_queue_admin_form_validate($form, &$form_state) {
$questions = array_filter($form_state['values']['questions']);
if (count($questions) == 0) {
form_set_error('', t('No questions selected.'));
/**
* Submit the question queue bulk update form.
*
Alastair Aitchison
committed
* @see node_admin_nodes_submit()
*/
function question_queue_admin_form_submit($form, &$form_state) {
$operations = module_invoke_all('question_queue_operations');
$operation = $operations[$form_state['values']['operation']];
// Filter out unchecked questions
$questions = array_filter($form_state['values']['questions']);
if ($function = $operation['callback']) {
// Add in callback arguments if present.
if (isset($operation['callback arguments'])) {
$args = array_merge(array($questions), $operation['callback arguments']);
}
else {
$args = array($questions);
}
call_user_func_array($function, $args);
cache_clear_all();
// We need to rebuild the form to go to a second step. For example, to
// show the confirmation form for the deletion of questions.
$form_state['rebuild'] = TRUE;
/**
* Confirm multiple deletion from the question queue bulk update form.
*/
function question_queue_multiple_delete_confirm($form, &$form_state, $questions) {
$form['questions'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
// array_filter returns only elements with TRUE values
foreach ($questions as $qid => $value) {
$title = db_query('SELECT question FROM {question_queue} WHERE qid = :qid', array(':qid' => $qid))->fetchField();
$form['questions'][$qid] = array(
'#type' => 'hidden',
'#value' => $qid,
'#prefix' => '<li>',
'#suffix' => check_plain($title) . "</li>\n",
);
$form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
$form['#submit'][] = 'question_queue_multiple_delete_confirm_submit';
$confirm_question = format_plural(count($questions),
'Are you sure you want to delete this question?',
'Are you sure you want to delete these questions?');
return confirm_form($form,
$confirm_question,
Alastair Aitchison
committed
'admin/content/question', t('This action cannot be undone.'),
t('Delete'), t('Cancel'));
/**
* Submit confirmation of multiple deletion from the question queue bulk update form.
*/
function question_queue_multiple_delete_confirm_submit($form, &$form_state) {
if ($form_state['values']['confirm']) {
foreach ($form_state['values']['questions'] as $qid => $value) {
$num_deleted = db_delete('question_queue')
->condition('qid', $qid)
->execute();
$count = count($form_state['values']['questions']);
watchdog('content', 'Deleted @count questions.', array('@count' => $count));
drupal_set_message(format_plural($count, 'Deleted 1 question', 'Deleted @count questions.', array('@count' => $count)));
/************************************************
* *
* RESPOND TO QUESTIONS *
* *
************************************************/
/**
* Implements hook_form().
*
* This is the node form called when a question has been promoted from the queue.
* The $qid is passed as arg(3) in the url (node/add/question/3).
* This is used to prepopulate the node form with information from the question queue.
*/
function question_form_question_node_form_alter(&$form, &$form_state) {
// If we've arrived here from the question queue, retrieve and pre-populate the information we already know
if (arg(2)=='question' && is_numeric(arg(3))) {
$result = db_query('SELECT * FROM {question_queue} WHERE qid = :qid', array(':qid' => arg(3)))->fetchObject();
$form['qid'] = array(
'#type' => 'hidden',
'#value' => arg(3),
);
$form['question_questioner']['und']['0']['value']['#default_value'] = isset($result->questioner) ? $result->questioner : '';
$form['question_question']['und']['0']['value']['#default_value'] = isset($result->question) ? $result->question : '';
$form['title']['#default_value'] = isset($result->question) ? truncate_utf8($result->question, 64, $wordsafe = TRUE, $dots = TRUE) : '';
$form['#validate'][] = 'question_form_validate'; //add additional validation handler for our new fields
$form['#submit'][] = 'question_form_submit'; //add additional validation handler for our new fields
}
/**
* Implements _form_validate().
*
* Validation functions called when a new response is submitted
*/
function question_form_validate(&$elements, &$form_state, $form_id = NULL) {
if ($elements['question_question']['und'][0]['value']['#value'] == '') {
form_set_error('question_question', t('Please enter a question.'));
}
if ($elements['title']['#value'] == '') {
form_set_error('title', t('Please enter a short title for the question.'));
Alastair Aitchison
committed
}
if ($elements['question_answer']['und'][0]['value']['#value'] == '') {
form_set_error('question_answer', t('Please enter the answer.'));
Alastair Aitchison
committed
}
}
* Implements _form_submit().
* Submit functions called when a new response is submitted
function question_form_submit(&$elements, &$form_state, $form_id = NULL) {
// If this node came from the queue, delete the queue item...
if (isset($elements['qid'])) {
$num_deleted = db_delete('question_queue')
->condition('qid', $elements['qid']['#value'])
->execute();