array( 'title' => t('Provide an answer to makemeeting forms'), 'description' => t('Give the ability to make a choice in makemeeting answer forms.'), ), 'edit any makemeeting answer' => array( 'title' => t('Edit any makemeeting answers'), 'description' => t('Edit answers given by other users.'), ), 'delete any makemeeting answer' => array( 'title' => t('Delete any makemeeting answers'), 'description' => t('Delete answers given by other users.'), ), ); } /** * Implements hook_menu(). */ function makemeeting_menu() { $items['makemeeting/delete-answer/%makemeeting_answer'] = array( 'title' => 'Remove an answer', 'page callback' => 'makemeeting_delete_answer', 'page arguments' => array(2), 'access callback' => 'makemeeting_delete_answer_access', 'access arguments' => array(2), 'type' => MENU_CALLBACK, ); $items['makemeeting/edit-answer/%makemeeting_answer'] = array( 'title' => 'Edit an answer', 'page callback' => 'makemeeting_edit_answer', 'page arguments' => array(2), 'access callback' => 'makemeeting_edit_answer_access', 'access arguments' => array(2), 'type' => MENU_CALLBACK, ); return $items; } /** * Load an answer * * @param $answer_id * @return mixed */ function makemeeting_answer_load($answer_id) { $answer = db_select('makemeeting_answers', 'm') ->fields('m') ->condition('answer_id', $answer_id) ->execute() ->fetchAssoc(); $answer['value'] = unserialize($answer['value']); return $answer ? (object) $answer : FALSE; } /** * Access callback: delete an answer * * @param $answer * @return bool */ function makemeeting_delete_answer_access($answer) { global $user; if (user_access('delete any makemeeting answer') || $answer->uid == $user->uid) { return TRUE; } return FALSE; } /** * Access callback: edit an answer * * @param $answer * @return bool */ function makemeeting_edit_answer_access($answer) { global $user; if (user_access('edit any makemeeting answer') || $answer->uid == $user->uid) { return TRUE; } return FALSE; } /** * Menu callback: delete an answer * * @param $answer_id */ function makemeeting_delete_answer($answer, $redirect = TRUE) { db_delete('makemeeting_answers')->condition('answer_id', $answer->answer_id)->execute(); if ($redirect) { drupal_goto($_GET['destination']); } } /** * Menu callback: edit an answer * * @param $answer The loaded answer object * @param string $type Ajax or nojs * @return array|void The entire form or just the row depending on ajax */ function makemeeting_edit_answer($answer, $type = 'ajax') { $entities = entity_load($answer->entity_type, array($answer->entity_id)); if (empty($entities)) { return drupal_not_found(); } $entity = $entities[$answer->entity_id]; $field = $entity->{$answer->field_name}[$answer->language][$answer->delta]; $form = drupal_get_form('makemeeting_answers_form', $field, (array) $answer, $answer); if ($type == 'ajax') { $form['#theme'] = 'makemeeting_answer_row'; // Replace 'ajax' word in form action $form['#action'] = str_replace('/ajax/', '/nojs/', $form['#action']); $output = '' . drupal_render($form) . ''; $commands = array(); // See ajax_example_advanced.inc for more details on the available commands // and how to use them. $commands[] = ajax_command_replace('#answer-' . $answer->answer_id, $output); $page = array('#type' => 'ajax', '#commands' => $commands); ajax_deliver($page); } else { return $form; } } /** * Implements hook_field_info(). */ function makemeeting_field_info() { return array( 'makemeeting' => array( 'label' => t('Makemeeting form'), 'description' => t('This field stores user choices about some dates in the database.'), 'default_widget' => 'makemeeting_choices', 'default_formatter' => 'makemeeting_answers', ), ); } /** * Implements hook_field_widget_info(). */ function makemeeting_field_widget_info() { return array( 'makemeeting_choices' => array( 'label' => t('Makemeeting choices'), 'field types' => array('makemeeting'), ), ); } /** * Implements hook_field_widget_form(). */ function makemeeting_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { $element += array( '#type' => 'fieldset', ); $item =& $items[$delta]; // Simple options form elements about the poll $element['hidden'] = array( '#title' => t('Hidden poll'), '#description' => t('Confidential participation: only you and administrators can see the answers.'), '#type' => 'checkbox', '#default_value' => isset($item['hidden']) ? $item['hidden'] : '', ); $element['one_option'] = array( '#title' => t('Participant can only choose one option'), '#description' => t('By default all options are selectable. This setting limits the choice to one option per participant.'), '#type' => 'checkbox', '#default_value' => isset($item['one_option']) ? $item['one_option'] : '', ); $element['limit'] = array( '#title' => t('Limit the number of participants per option'), '#description' => t('Poll as registration form: As soon as the indicated limit has been reached, the respective option is no longer available. 0 for unlimited'), '#type' => 'textfield', '#default_value' => isset($item['limit']) ? $item['limit'] : 0, '#element_validate' => array('element_validate_integer'), ); $element['yesnomaybe'] = array( '#title' => t('Provide a "Maybe" option'), '#description' => t('Provide a third "Maybe" option in case users may be available.'), '#type' => 'checkbox', '#default_value' => isset($item['yesnomaybe']) ? $item['yesnomaybe'] : '', // Hide if one_option is checked '#states' => array( 'visible' => array( ':input[name$="[one_option]"]' => array('checked' => FALSE), ), ), ); $element['closed'] = array( '#title' => t('Closed'), '#description' => t('Check this when you want to close the poll.'), '#type' => 'checkbox', '#default_value' => isset($item['closed']) ? $item['closed'] : '', ); // Now let's take care of the choices part // $process is a callback that we will attach to the wrapper in order // to add parents to the form elements. This is so we can store // nested values in the DB table with field storage API. $fieldset_info = element_info('fieldset'); $process = array_merge($fieldset_info['#process'], array('_makemeeting_rebuild_parents')); $element['choices_wrapper'] = array( '#tree' => FALSE, '#prefix' => '
', '#suffix' => '
', '#process' => $process, ); // Container just for the poll choices. $element['choices_wrapper']['choices'] = array( '#prefix' => '
', '#suffix' => '
', '#theme' => 'makemeeting_choices', ); // Fill the item values with previously submitted values if (!empty($form_state['values'])) { $submitted_values = drupal_array_get_nested_value($form_state['values'], $element['#field_parents']); $submitted_values = $submitted_values[$element['#field_name']][$element['#language']][$element['#delta']]; if (!empty($submitted_values)) { $item['choices'] = $submitted_values['choices']; } } // Calculate the choice and suggestion counts $form_state['choice_count'] = isset($form_state['choice_count']) ? $form_state['choice_count'] : 1; if (empty($form_state['suggestion_count'])) { $form_state['suggestion_count'] = 1; } if (isset($item['choices'])) { foreach ($item['choices'] as $choice) { if ($form_state['suggestion_count'] < count($choice['chsuggestions'])) { $form_state['suggestion_count'] = count($choice['chsuggestions']); } } } // Add the current choices to the form. $delta = 0; if (isset($item['choices'])) { // Sort choices by their dates asort($item['choices']); $delta = count($item['choices']); foreach ($item['choices'] as $key => $choice) { $element['choices_wrapper']['choices'][$key] = _makemeeting_choice_form($key, $choice['chdate'], $choice['chsuggestions'], $form_state['suggestion_count']); } } // Add initial or additional choices. $existing_delta = $delta; for (; $delta < $form_state['choice_count']; $delta++) { $key = 'new:' . ($delta - $existing_delta); $element['choices_wrapper']['choices'][$key] = _makemeeting_choice_form($key, '', NULL, $form_state['suggestion_count']); } // We prefix our buttons with 'makemeeting' to avoid conflicts // with other modules using Ajax-enabled buttons with the id 'more'. $default_submit = array( '#type' => 'submit', '#limit_validation_errors' => array(array('choices')), '#submit' => array('makemeeting_choices_submit'), '#ajax' => array( 'callback' => 'makemeeting_choice_js', 'wrapper' => 'makemeeting-choices', 'effect' => 'fade', ), ); $element['choices_wrapper']['makemeeting_more_choices'] = $default_submit + array( '#value' => t('More choices'), '#attributes' => array( 'title' => t("If the amount of rows above isn't enough, click here to add more choices."), ), '#weight' => 1, ); $element['choices_wrapper']['makemeeting_more_suggestions'] = $default_submit + array( '#value' => t('More suggestions'), '#attributes' => array( 'title' => t("If the amount of boxes above isn't enough, click here to add more suggestions."), ), '#weight' => 2, ); $element['choices_wrapper']['makemeeting_copy_suggestions'] = $default_submit + array( '#value' => t('Copy and paste first row'), '#attributes' => array( 'title' => t("Copy the suggestions from the first to the other rows."), ), '#weight' => 3, ); return $element; } /** * Implements hook_field_validate(). */ function makemeeting_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { foreach ($items as $delta => $item) { if (isset($item['choices'])) { // Verify that we do not have duplicate dates $dates = array(); foreach ($item['choices'] as $chid => $chvalue) { $date = implode('', $chvalue['chdate']); if (in_array($date, $dates)) { // Register an error with the choice id $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'duplicate_dates', 'message' => t('%name: you can\'t have duplicate dates.', array('%name' => $instance['label'])), 'chid' => $chid, ); } else { $dates[] = $date; } } } } } /** * Implements hook_field_widget_error(). */ function makemeeting_field_widget_error($element, $error) { // Show our error concerning duplicate dates here $choices = $element['choices_wrapper']['choices']; form_error($choices[$error['chid']]['chdate'], $error['message']); } /** * Submit handler to add more choices or suggestions to a poll form, * or to copy the first row of suggestions to the others * * This handler is run regardless of whether JS is enabled or not. It makes * changes to the form state. If the button was clicked with JS disabled, then * the page is reloaded with the complete rebuilt form. If the button was * clicked with JS enabled, then ajax_form_callback() calls makemeeting_choice_js() to * return just the changed part of the form. */ function makemeeting_choices_submit($form, &$form_state) { $clicked_button = $form_state['clicked_button']; // Get the element $element =& drupal_array_get_nested_value($form_state['values'], $clicked_button['#parents']); // If clicked button is 'Remove', unset corresponding choice if ($clicked_button['#value'] == t('Remove')) { unset($element['choices'][$clicked_button['#name']]); $form_state['choice_count'] = count($element['choices']); } // Affect timestamp as key to each choice $choices = array(); foreach ($element['choices'] as $choice) { $choices['choices'][_makemeeting_date_timestamp($choice['chdate'])] = $choice; } // Handle other operations if (!empty($form_state['values']['op'])) { switch ($form_state['values']['op']) { // Add 1 more choice to the form. case t('More choices'): // Increment choice count $form_state['choice_count'] = count($element['choices']) + 1; break; // Add 1 more suggestion to the form. case t('More suggestions'): $form_state['suggestion_count']++; break; // Copy first row suggestions to the other rows. case t('Copy and paste first row'); $first = reset($choices['choices']); foreach (array_keys($choices['choices']) as $key) { $choices['choices'][$key]['chsuggestions'] = $first['chsuggestions']; } break; } } // Replace submitted values by our own work drupal_array_set_nested_value($form_state['input'], $clicked_button['#parents'], $choices); drupal_array_set_nested_value($form_state['values'], $clicked_button['#parents'], $choices); // Require the widget form to rebuild the choices // with the values in $form_state $form_state['rebuild'] = TRUE; } /** * Ajax callback in response to new choices being added to the form. * * This returns the new page content to replace the page content made obsolete * by the form submission. */ function makemeeting_choice_js($form, $form_state) { // Get the element from the parents of the clicked button list($field_name, $language, $delta) = $form_state['clicked_button']['#parents']; $element =& $form[$field_name][$language][$delta]; return $element['choices_wrapper']['choices']; } /** * Ajax callback in response to an answer that has been edited. * * This returns the new form content to replace the form content made obsolete * by the form submission. */ function makemeeting_answer_js($form, $form_state) { $values = $form_state['values']; $answer = $values['answer_edited']; $entities = entity_load($answer->entity_type, array($answer->entity_id)); if (empty($entities)) { return drupal_not_found(); } $entity = $entities[$answer->entity_id]; $field = $entity->{$answer->field_name}[$answer->language][$answer->delta]; // We need to empty POST data to prevent the form // from being processed as submitted $_POST = array(); return drupal_get_form('makemeeting_answers_form', $field, $values); } /** * Implements hook_field_is_empty(). * * This field cannot be empty as it will always * have a default choice of today's date */ function makemeeting_field_is_empty() { return FALSE; } /** * Implements hook_field_formatter_info(). */ function makemeeting_field_formatter_info() { return array( 'makemeeting_answers' => array( 'label' => t('Makemeeting answers'), 'field types' => array('makemeeting'), ), ); } /** * Implements hook_field_formatter_view(). * * Build a renderable array for a field value. * * @param $entity_type * The type of $entity. * @param $entity * The entity being displayed. * @param $field * The field structure. * @param $instance * The field instance. * @param $langcode * The language associated with $items. * @param $items * Array of values for this field. * @param $display * The display settings to use, as found in the 'display' entry of instance * definitions. The array notably contains the following keys and values; * - type: The name of the formatter to use. * - settings: The array of formatter settings. * * @return * A renderable array for the $items, as an array of child elements keyed * by numeric indexes starting from 0. * * We only have a display type so no need to test $display['type'] */ function makemeeting_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { $content = array(); foreach ($items as $delta => $item) { $instance += array( 'entity_id' => reset(entity_extract_ids($entity_type, $entity)), 'language' => $langcode, 'delta' => $delta ); $content[] = drupal_get_form('makemeeting_answers_form', $item, $instance); } return $content; } /** * Form callback: enables users to answer a makemeeting poll */ function makemeeting_answers_form($form, $form_state, $item, $instance, $answer = NULL) { global $user; $form = array(); $form['#item'] = $item; $form['#theme'] = 'makemeeting_answers'; // Force the id of the form as it might get overriden in AJAX-loaded forms $form['#id'] = 'makemeeting-answers-form'; // Pass entity-related values in the form foreach (array('field_name', 'entity_type', 'deleted', 'entity_id', 'language', 'delta') as $info) { $form[$info] = array( '#type' => 'value', '#value' => $instance[$info], ); } // Pass answer being edited if ($answer) { $form['answer_edited'] = array( '#type' => 'value', '#value' => $answer, ); } // Include the name of the current user $form['name'] = array( '#type' => 'textfield', '#size' => 22, ); if (!user_is_logged_in()) { // This is an HTML5 attribute $form['name']['#attributes'] = array('placeholder' => t('Your name (required)')); $form['name']['#required'] = TRUE; } else { $form['name']['#default_value'] = format_username($user); $form['name']['#disabled'] = TRUE; } if (!empty($answer)) { if ($answer->uid > 0) { $account = user_load($answer->uid); $form['name']['#default_value'] = format_username($account); } else { $form['name']['#default_value'] = $answer->name; } } // If the form is limited, fetch already submitted answers $answers = array(); if ($item['limit'] > 0) { $select = db_select('makemeeting_answers', 'ma')->fields('ma', array('value')); foreach (array('field_name', 'entity_type', 'deleted', 'entity_id', 'language', 'delta') as $info) { $select->condition($info, $instance[$info]); } // Filter out answer being edited if ($answer) { $select->condition('answer_id', $answer->answer_id, '!='); } $results = $select->execute(); // And add each answer to our results array for futher use foreach ($results as $result) { $answer = unserialize($result->value); if (is_array($answer)) { foreach ($answer as $key => $value) { if ($value) { $answers[$key] = empty($answers[$key]) ? 1 : $answers[$key] + 1; } } } elseif (is_string($answer) && $answer) { $answers[$answer] = empty($answers[$answer]) ? 1 : $answers[$answer] + 1; } } } // Possible answers $form['answers'] = array(); foreach ($item['choices'] as $choice) { $chdate = _makemeeting_date_timestamp($choice['chdate']); $count = 0; foreach ($choice['chsuggestions'] as $id => $text) { // Add a form element only if there's a suggestion label // or it is the first suggestion for this date if ($text || (!$text && !$count)) { _makemeeting_answer_element($form, $item, $id, $chdate, $text, $answers, $answer); } $count++; } } $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), ); if (!empty($answer)) { // Modify form submit to include ajax behavior $form['submit']['#ajax'] = array( 'callback' => 'makemeeting_answer_js', 'wrapper' => 'makemeeting-answers-form', 'effect' => 'fade', ); } return $form; } function makemeeting_answers_form_validate($form, &$form_state) { // The required attribute won't work, so we display a single message if (!$form_state['values']['name']) { drupal_set_message(t('You must enter your name.'), 'error'); } // Check is the user has already voted if (user_is_logged_in() && empty($form_state['values']['answer_edited'])) { global $user; $select = db_select('makemeeting_answers', 'ma'); foreach (array('field_name', 'entity_type', 'deleted', 'entity_id', 'language', 'delta') as $info) { $select->condition($info, $form_state['values'][$info]); } $result = $select->condition('uid', $user->uid)->countQuery()->execute()->fetchField(); if ($result) { form_error($form, t('You already voted on this poll.')); } } } function makemeeting_answers_form_submit($form, $form_state) { global $user; if (!empty($form_state['values']['answer_edited'])) { db_update('makemeeting_answers') ->fields(array( 'value' => serialize($form_state['values']['answers']), )) ->condition('answer_id', $form_state['values']['answer_edited']->answer_id) ->execute(); } else { $fields = array(); foreach (array('field_name', 'entity_type', 'deleted', 'entity_id', 'language', 'delta', 'name') as $field) { $fields[$field] = $form_state['values'][$field]; } db_insert('makemeeting_answers')->fields($fields + array( 'value' => serialize($form_state['values']['answers']), 'uid' => $user->uid, ))->execute(); } } /** * Function used to render answers' table */ function theme_makemeeting_answers($vars) { global $user; $form = $vars['form']; $item = $form['#item']; // Add appropriate CSS. drupal_add_css(drupal_get_path('module', 'makemeeting') . '/makemeeting.css'); // Initialize variables $header = $day_cell = $row_form = $tree = $rows = array(); $tz = drupal_get_user_timezone(); // First construct tree of suggestions for headers foreach ($item['choices'] as $choice) { $chdate = _makemeeting_date_timestamp($choice['chdate']); $month = format_date($chdate, 'custom', 'F Y', $tz); $day = format_date($chdate, 'custom', 'D. j', $tz); $count = 0; foreach ($choice['chsuggestions'] as $text) { if (!$text && isset($tree[$month][$day]) && $count) { continue; } elseif (!$text) { $text = "-"; } $tree[$month][$day][] = $text; $count++; } } // Now construct headers based on tree $headers = $row_days = $row_suggestions = array(''); // Add month-related classes $months_total = count($tree); $months_count = 0; foreach ($tree as $month => $days) { $header['colspan'] = 0; $header['data'] = $month; $header['class'] = array('date-month'); _makemeeting_define_class_suffix($header, $months_count, $months_total, 'date-month'); $months_count++; // Add day-related classes $days_total = count($days); $days_count = 0; foreach ($days as $day => $suggestions) { $day_cell['colspan'] = 0; $day_cell['data'] = $day; $day_cell['class'] = array('date'); _makemeeting_define_class_suffix($day_cell, $days_count, $days_total, 'date'); $days_count++; // Add suggestion-related classes $suggestions_total = count($suggestions); $suggestion_count = 0; foreach ($suggestions as $text) { $suggestion = array('data' => $text, 'class' => array('suggestion')); _makemeeting_define_class_suffix($suggestion, $suggestion_count, $suggestions_total, 'suggestion'); $suggestion_count++; $row_suggestions[] = $suggestion; // Set colspan $header['colspan']++; $day_cell['colspan']++; } $row_days[] = $day_cell; } $headers[] = $header; } array_push($rows, $row_days, $row_suggestions); // Initialize totals labels $totals = array(); $options = _makemeeting_options($item['yesnomaybe']); foreach ($options as $key => $label) { $totals[$key]['table'] = array(array('data' => t('Totals ' . $label), 'class' => 'total-title')); foreach ($item['choices'] as $choice) { $count = 0; foreach ($choice['chsuggestions'] as $text) { if ($text || (!$text && !$count)) { // Initialize totals count $totals[$key]['count'][] = 0; } $count++; } } } // Display already submitted answers $select = db_select('makemeeting_answers', 'ma')->fields('ma'); foreach (array('field_name', 'entity_type', 'deleted', 'entity_id', 'language', 'delta') as $info) { $select->condition($info, $form[$info]['#value']); } // Filter out answer being edited if (!empty($form['answer_edited'])) { $select->condition('answer_id', $form['answer_edited']['#value']->answer_id, '!='); } $results = $select->execute()->fetchAllAssoc('uid'); foreach ($results as $result) { $row = array(); // Display name of the person that has answered $row[] = _makemeeting_render_cell_name($result->uid, $result->name, $result->answer_id); // Display his/her answers $answers = unserialize($result->value); foreach (element_children($form['answers']) as $answer_key) { $row_count = count($row); // Determine choice based on choice type if ($item['one_option']) { $key = $answers == $answer_key ? MAKEMEETING_YES : MAKEMEETING_NO; } else { $key = isset($answers[$answer_key]) ? intval($answers[$answer_key]) : FALSE; } $label = isset($options[$key]) ? $options[$key] : FALSE; if ($key !== FALSE && $label !== FALSE) { // Add corresponding classes for the answer $classes = array('answer', 'answer-' . $key); foreach (array('suggestion-first', 'suggestion-medium', 'suggestion-last') as $class) { if (in_array($class, $row_suggestions[$row_count]['class'])) { $classes[] = str_replace('suggestion', 'answer', $class); } } // Store answer (will an background image according to class) $row[] = array('data' => ' ', 'class' => $classes); // Increment key answer counter $totals[$key]['count'][$row_count - 1]++; } else { $row[] = t('No answer'); } } $rows[] = array('data' => $row, 'id' => 'answer-' . $result->answer_id); } // Add participant count $rows[1][0] = format_plural(count($results), '@count participant', '@count participants'); // Display answer form if ... not limited, $limited = $item['limit'] > 0 && $item['limit'] * count($row_suggestions) - 1 <= count($results); // ... not closed, $closed = $item['closed'] || $limited; $submitted = user_is_logged_in() && in_array($user->uid, array_keys($results)); // ... user has access and has not already submitted $show_form = user_access('answer makemeeting form') && !$closed && !$submitted; if ($show_form) { $row_form[] = drupal_render($form['name']); foreach (element_children($form['answers']) as $answer_key) { $row_form[] = array( 'data' => drupal_render($form['answers'][$answer_key]), 'class' => $form['answers'][$answer_key]['#type'], ); } $rows[] = array( 'data' => $row_form, 'class' => array('answer-form-row'), ); } // Get total maxima $totals_max = array(); foreach ($totals as $key => &$table) { if (empty($table['count'])) { $table['count'][] = 0; } $totals_max[$key] = max($table['count']); } // Display totals foreach ($options as $key => $label) { foreach ($totals[$key]['count'] as $choice => $count) { $classes = array('total-count', 'total-' . $key); if ($count) { // Indicate first / last suggestion item foreach (array('suggestion-first', 'suggestion-medium', 'suggestion-last') as $class) { if (in_array($class, $row_suggestions[$choice + 1]['class'])) { $classes[] = str_replace('suggestion', 'total', $class); } } } // Indicate max count if (!empty($totals_max[$key]) && $count == $totals_max[$key]) { $classes[] = 'total-max'; } $totals[$key]['table'][] = array('data' => $count, 'class' => $classes); } $classes = array('total-row'); // Change labels for two choices if (!$item['yesnomaybe']) { $totals[$key]['table'][0]['data'] = t('Totals'); } // Do not display other totals for two choices if ($item['yesnomaybe'] || !$item['yesnomaybe'] && $key == MAKEMEETING_YES) { $classes[] = 'total-row-' . ($key ? ($key == MAKEMEETING_YES ? 'last' : 'medium') : 'first'); $rows[] = array('data' => $totals[$key]['table'], 'class' => $classes); } } // Apply classes to the two first rows $rows[0] = array('data' => $rows[0], 'class' => array('dates')); $rows[1] = array('data' => $rows[1], 'class' => array('suggestions')); $suggestion = array( '#theme' => 'table', '#header' => $headers, '#rows' => $rows, '#attributes' => array('class' => array('makemeeting-table')), ); if ($show_form) { return drupal_render($suggestion) . drupal_render_children($form); } return drupal_render($suggestion); } /** * Function used to render a single answer row as a form */ function theme_makemeeting_answer_row($vars) { $form = $vars['form']; $element = array( 'first_cell' => array(), 'other_cells' => array('#markup' => '') ); // Render suggestions foreach (element_children($form['answers']) as $answer_key) { $element['other_cells']['#markup'] .= _theme_table_cell(array( 'data' => drupal_render($form['answers'][$answer_key]), 'class' => $form['answers'][$answer_key]['#type'], )); } // Render name, submit and the rest of hidden children $first_cell = drupal_render($form['name']) . drupal_render($form['submit']); $first_cell .= drupal_render_children($form); $element['first_cell']['#markup'] = _theme_table_cell($first_cell); return drupal_render($element); } /** * Render the cell containing the submitter's name with * a possible option to delete the answer */ function _makemeeting_render_cell_name($uid, $name, $answer_id) { global $user; if ($uid) { $account = user_load($uid); $username = theme('username', array('account' => user_load($account->uid))); } else { $username = $name; } if (!user_access('delete any makemeeting answer') && $uid != $user->uid) { return $username; } $images_path = drupal_get_path('module', 'makemeeting') . '/images/'; $cell[] = array( '#prefix' => '
', 'delete-link' => array( '#type' => 'link', '#title' => theme_image(array('path' => $images_path . 'trash.png', 'attributes' => array('title' => t('Delete answer')))), '#href' => 'makemeeting/delete-answer/' . $answer_id, '#options' => array( 'query' => drupal_get_destination(), 'html' => TRUE, ), ), 'edit-link' => array( '#type' => 'link', '#title' => theme_image(array('path' => $images_path . 'pen.png', 'attributes' => array('title' => t('Edit answer')))), // Note the /nojs portion of the href - if javascript is enabled, // this part will be stripped from the path before it is called. '#href' => 'makemeeting/edit-answer/' . $answer_id . '/nojs/', '#options' => array( 'query' => drupal_get_destination(), 'html' => TRUE, ), '#ajax' => array( 'wrapper' => 'answer-' . $answer_id, 'method' => 'html', ), ), '#suffix' => '
', ); $cell[] = array('#markup' => $username); return '
' . drupal_render($cell) . '
'; } /** * Implements hook_field_presave(). */ function makemeeting_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { if ($field['type'] == 'makemeeting') { foreach ($items as $delta => $item) { if (isset($item['choices'])) { // Sort the choices by date $choices = array_values($item['choices']); usort($choices, '_makemeeting_sort_choices'); // Affect timestamp as keys $timestamped_choices = array(); foreach ($choices as $choice) { $timestamped_choices[_makemeeting_date_timestamp($choice['chdate'])] = $choice; } // Serialize the whole array $items[$delta]['choices'] = serialize($timestamped_choices); } } } } /** * Implements hook_field_load(). */ function makemeeting_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) { // Unserialize array values foreach ($entities as $id => $entity) { foreach ($items[$id] as $delta => $item) { if ($field['type'] == 'makemeeting' && is_string($items[$id][$delta]['choices'])) { $items[$id][$delta]['choices'] = unserialize($items[$id][$delta]['choices']); } } } } /** * Implements hook_theme(). */ function makemeeting_theme($existing, $type, $theme, $path) { return array( 'makemeeting_choices' => array( 'render element' => 'form', ), 'makemeeting_answers' => array( 'render element' => 'form', ), 'makemeeting_answer_row' => array( 'render element' => 'form', ), ); } /** * Returns HTML for an admin poll form for choices. */ function theme_makemeeting_choices($variables) { $form = $variables['form']; // Add appropriate CSS. drupal_add_css(drupal_get_path('module', 'makemeeting') . '/makemeeting.css'); $rows = array(); $headers = array(t('Date')); $headers_added = FALSE; foreach (element_children($form) as $key) { // Build the table rows. $row = array(); $row[] = array( 'data' => drupal_render($form[$key]['chdate']) . drupal_render($form[$key]['chremove']), 'class' => 'makemeeting-choice-date', ); $delta = 0; foreach (element_children($form[$key]['chsuggestions']) as $key2) { $delta++; // Add sugesstion headers for the first line if (!$headers_added) { $headers[] = t('Suggestion %num', array('%num' => $delta)); } $row[] = drupal_render($form[$key]['chsuggestions'][$key2]); } $headers_added = TRUE; $rows[] = $row; } $output = theme('table', array( 'header' => $headers, 'rows' => $rows, 'attributes' => array('id' => 'poll-choice-table') )); $output .= drupal_render_children($form); return $output; } /** * Helper function to update the form elements parents. This is required * for the element to be saved properly by the Field API (see #process) * * @param $element * @return mixed */ function _makemeeting_rebuild_parents(&$element) { $parents = array_diff($element['#array_parents'], $element['#parents']); $elements = array('makemeeting_more_choices', 'makemeeting_more_suggestions', 'makemeeting_copy_suggestions'); foreach ($elements as $e) { $element[$e]['#parents'] = $parents; $element[$e]['#limit_validation_errors'][0] = array_merge($parents, $element[$e]['#limit_validation_errors'][0]); } $element['makemeeting_more_suggestions']['#parents'] = $parents; $element['makemeeting_copy_suggestions']['#parents'] = $parents; foreach (element_children($element['choices']) as $key1) { foreach (element_children($element['choices'][$key1]) as $key2) { $keys = element_children($element['choices'][$key1][$key2]); if (empty($keys)) { $element['choices'][$key1][$key2]['#parents'] = array_merge($parents, $element['choices'][$key1][$key2]['#parents']); } else { foreach ($keys as $key3) { $element['choices'][$key1][$key2][$key3]['#parents'] = array_merge($parents, $element['choices'][$key1][$key2][$key3]['#parents']); } } } } return $element; } /** * Helper function to construct a row of the widget form * * @param $key * @param string $value * @param array $suggestions * @param int $size * @return array */ function _makemeeting_choice_form($key, $value = '', $suggestions = array(), $size = 1) { $form = array( '#tree' => TRUE, ); // We'll manually set the #parents property of these fields so that // their values appear in the $form_state['values']['choices'] array // They are to be updated later in #process as we don't know here the // hierarchy of the parents $form['chdate'] = array( '#type' => 'date', '#title' => t('Date'), '#title_display' => 'invisible', '#default_value' => $value, '#parents' => array('choices', $key, 'chdate'), ); $form['chremove'] = array( '#type' => 'submit', '#parents' => array(), '#submit' => array('makemeeting_choices_submit'), '#ajax' => array( 'callback' => 'makemeeting_choice_js', 'wrapper' => 'makemeeting-choices', 'effect' => 'fade', ), '#value' => t('Remove'), // Assigning key as name to make each 'Remove' button unique '#name' => $key, ); $form['chsuggestions'] = array( '#tree' => TRUE, '#parents' => array('choices', $key, 'chsuggestions'), ); for ($i = 0; $i < $size; $i++) { $key2 = 'sugg:' . ($i); $form['chsuggestions'][$key2] = array( '#type' => 'textfield', '#title_display' => 'invisible', '#default_value' => isset($suggestions[$key2]) ? $suggestions[$key2] : '', '#size' => 5, '#maxlength' => 255, '#parents' => array('choices', $key, 'chsuggestions', $key2), ); } return $form; } /** * Helper function to sort choice dates */ function _makemeeting_sort_choices($a, $b) { if ($a['chdate']['year'] == $b['chdate']['year']) { if ($a['chdate']['month'] == $b['chdate']['month']) { if ($a['chdate']['day'] == $b['chdate']['day']) { return 0; } else { return (int) $a['chdate']['day'] < (int) $b['chdate']['day'] ? -1 : 1; } } else { return (int) $a['chdate']['month'] < (int) $b['chdate']['month'] ? -1 : 1; } } else { return (int) $a['chdate']['year'] < (int) $b['chdate']['year'] ? -1 : 1; } } /** * Helper function to convert date field value into an Unix timestamp * * @param $array array Date field value (month, day and year) * @return int Unix timestamp */ function _makemeeting_date_timestamp($array) { date_default_timezone_set('UTC'); $timestamp = mktime(0, 0, 0, $array['month'], $array['day'], $array['year']); date_default_timezone_set(drupal_get_user_timezone()); return $timestamp; } /** * Helper function for selecting choices * * @param bool $three_choices If should be returning three choices * @return array An array of options for radios */ function _makemeeting_options($three_choices = FALSE) { $options = array( MAKEMEETING_NO => t('No'), MAKEMEETING_MAYBE => t('Maybe'), MAKEMEETING_YES => t('Yes'), ); if (!$three_choices) { unset($options[MAKEMEETING_MAYBE]); } return $options; } /** * Helper function to provide an answer form element * * @param $form * Form to be modified * @param $item * Item providing settings for the answer form * @param $id * Suggestion id * @param $chdate * Suggestion date * @param $text * Suggestion text * @param $answers * Already submitted answers */ function _makemeeting_answer_element(&$form, $item, $id, $chdate, $text, $answers = array(), $answer = NULL) { $key = $chdate . ':' . $id; // If the limit is reached for this option, display a markup text if ($item['limit'] > 0 && isset($answers[$key]) && $answers[$key] >= $item['limit']) { $form['answers'][$key] = array( '#markup' => t('Unavailable'), ); } // Else add a form element else { $tz = drupal_get_user_timezone(); $title = format_date($chdate, 'custom', 'l j F Y', $tz) . ' ' . $text; $form['answers'][$key] = array( '#type' => $item['one_option'] ? 'radio' : ($item['yesnomaybe'] ? 'radios' : 'checkbox'), '#attributes' => array('title' => check_plain($title)), '#parents' => array('answers', $key), ); if ($item['one_option']) { $form['answers'][$key]['#parents'] = array('answers'); $form['answers'][$key]['#return_value'] = $key; } else { $form['answers'][$key]['#options'] = _makemeeting_options($item['yesnomaybe']); } if ($item['yesnomaybe']) { $form['answers'][$key]['#default_value'] = MAKEMEETING_NO; } // Display previous choice if answer is being edited if ($answer && !empty($answer->value[$key])) { $form['answers'][$key]['#default_value'] = $answer->value[$key]; } } } /** * Helper function to define classes based on a count and a total * * @param $element * @param $count * @param $total */ function _makemeeting_define_class_suffix(&$element, $count, $total, $prefix = '') { if ($prefix) { $prefix .= '-'; } if ($count == 0) { $element['class'][] = $prefix . 'first'; } elseif ($count == ($total - 1)) { $element['class'][] = $prefix . 'last'; } else { $element['class'][] = $prefix . 'medium'; } }