You are here

course.outline.inc in Course 7.2

Same filename and directory in other branches
  1. 6 includes/course.outline.inc
  2. 7 includes/course.outline.inc

course_outline.inc

Functions to show and edit graphical course outline.

File

includes/course.outline.inc
View source
<?php

/**
 * @file course_outline.inc
 *
 * Functions to show and edit graphical course outline.
 */

/**
 * Form constructor for course outline form.
 *
 * @see course_menu()
 * @see _course_outline_object_form()
 * @see theme_course_outline_overview_form()
 */
function course_outline_overview_form($form, &$form_state) {
  global $user;
  $form = array();

  // Determine the default value of the 'usage' select. When nothing is stored
  // in $form_state['storage'] yet, it's the form hasn't been submitted yet,
  // thus it's the first time the form is being displayed.
  // Load the modal library and add the modal javascript.
  ctools_include('ajax');
  ctools_include('modal');
  ctools_include('object-cache');
  ctools_modal_add_js();

  // Wrapper for objects and more button.
  $form['#tree'] = TRUE;
  $form['#prefix'] = '<div class="clear-block" id="course-outline-wrapper">';
  $form['#suffix'] = '</div>';

  // Shortcut.
  $cform =& $form['course_outline'];
  if (isset($form_state['values']['nid'])) {
    $node = node_load($form_state['values']['nid']);
  }
  else {
    $node = node_load(arg(1));
  }
  $course = course_get_course($node);

  // Check if "Add object" was clicked.
  if (isset($form_state['values']['op']) && $form_state['values']['op'] == t('Add object') && !empty($form_state['values']['more']['object_type'])) {

    // Ensure that we cached the course.
    course_editing_start($course);

    // Create a new course object in the session, and let the rest of the form
    // builder handle it.
    $obj_uniqid = uniqid('course_object_');
    $_SESSION['course'][$node->nid]['editing'][$obj_uniqid] = array();

    // Populate temporary course object, save it in the session.
    $new = array();
    $new['weight'] = 0;

    // Get highest weight and add to it.
    if (isset($form_state['values']['course_outline']['objects'])) {
      foreach ($form_state['values']['course_outline']['objects'] as $key => $object) {
        if ($object['weight'] >= $new['weight']) {
          $new['weight'] = $object['weight'] + 1;
        }
      }
    }
    $new['nid'] = $node->nid;
    $new['coid'] = $obj_uniqid;
    list($new['module'], $new['object_type']) = explode('-', $form_state['values']['more']['object_type']);
    $_SESSION['course'][$node->nid]['editing'][$obj_uniqid] = $new;
    if (!empty($_COOKIE['has_js']) && module_exists('overlay') && ((!isset($user->data['overlay']) || $user->data['overlay']) && user_access('access overlay'))) {

      // Pop up the object editing window in an overlay.
      $outline_path = "node/{$node->nid}/course-outline";
      $settings_path = ltrim(url("node/{$node->nid}/course-object/nojs/{$obj_uniqid}/options"), '/');
      $path = urlencode("{$settings_path}?destination={$outline_path}");
      $opts = [
        'fragment' => "overlay={$path}",
      ];

      // Redirect to object settings form upon adding an object.
      $commands[] = ctools_ajax_command_redirect($outline_path, 0, $opts);
      print ajax_render($commands);
      exit;
    }
  }
  $form['nid']['#type'] = 'hidden';
  $form['nid']['#value'] = $node->nid;

  // Grab initial list of objects from DB or session.
  if (!empty($_SESSION['course'][$node->nid]['editing'])) {
    $objects = $_SESSION['course'][$node->nid]['editing'];
  }
  else {
    if ($objects = $course
      ->getObjects()) {

      // Great.
    }
    else {
      $objects = array();
    }
  }

  // Sort list of objects we pulled from session or DB by weight for proper
  // display.
  uasort($objects, '_course_outline_overview_form_cmp_function');
  $cform['#title'] = t('Course objects');
  $form['#theme'] = 'course_outline_overview_form';
  if (!empty($_SESSION['course'][$node->nid]['editing'])) {
    drupal_set_message('Changes to this course have not yet been saved.', 'warning', FALSE);
  }
  $handlers = course_get_handlers('object');

  // Wrapper for just the objects.
  $cform['objects']['#tree'] = TRUE;
  $object_counts = array();
  if (count($objects)) {
    foreach (array_keys($objects) as $uniqid) {
      if ($courseObject = course_get_course_object_by_id($uniqid)) {
        $rform = _course_outline_object_form($courseObject);

        // Keep track of how many of each course object we have.
        // @kludge probably some simpler way to do this effectively
        if (!isset($object_counts[$courseObject
          ->getModule()][$courseObject
          ->getComponent()])) {
          $object_counts[$courseObject
            ->getModule()][$courseObject
            ->getComponent()] = 1;
        }
        else {
          $object_counts[$courseObject
            ->getModule()][$courseObject
            ->getComponent()]++;
        }

        // Don't allow user to change type of object.
        $rform['object_type'] = array(
          '#type' => 'hidden',
          '#value' => $courseObject
            ->getOption('object_type'),
        );
        if (empty($handlers[$courseObject
          ->getOption('module')][$courseObject
          ->getOption('object_type')])) {
          $show_object_name = t('Missing CourseObject handler for <br/><i>@m/@t</i>', array(
            '@m' => $courseObject
              ->getOption('module'),
            '@t' => $courseObject
              ->getOption('object_type'),
          ));
        }
        else {
          $show_object_name = $handlers[$courseObject
            ->getOption('module')][$courseObject
            ->getOption('object_type')]['name'] . '<br/><small><i>' . ucwords(str_replace('_', ' ', $courseObject
            ->getOption('module'))) . '</i></small>';
        }
        $rform['object_type_show'] = array(
          '#type' => 'markup',
          '#markup' => filter_xss_admin($show_object_name),
        );
        $cform['objects'][$uniqid] = $rform;
      }
    }
  }

  // Add object button and select box for new objects.
  $object_types = array(
    '' => '- ' . t('select object') . ' -',
  );
  foreach ($handlers as $module => $object_definitions) {
    if ($object_definitions) {
      foreach ($object_definitions as $key => $object_info) {
        $class = $object_info['class'];
        $max_object_count = call_user_func(array(
          $class,
          'getMaxOccurences',
        ));
        $under_limit = !$max_object_count || !(isset($object_counts[$module][$key]) && $object_counts[$module][$key] >= $max_object_count);
        if ($under_limit && empty($object_info['legacy'])) {
          $object_types[$module . '-' . $key] = $object_info['name'];
        }
      }
    }
  }
  $form['more'] = array(
    '#type' => 'markup',
    '#prefix' => '<div class="container-inline add-objects-wrapper">',
    '#suffix' => '</div>',
  );
  $form['more']['add_another'] = array(
    '#type' => 'button',
    '#value' => t('Add object'),
    '#ajax' => array(
      'method' => 'replace',
      'wrapper' => 'course-outline-wrapper',
      'callback' => 'course_outline_overview_form_rebuild',
    ),
    '#weight' => 20,
  );

  // Sort course object types, case insensitively.
  asort($object_types, SORT_STRING | SORT_FLAG_CASE);
  $form['more']['object_type'] = array(
    '#title' => t('Object type'),
    '#title_display' => 'invisible',
    '#type' => 'select',
    '#options' => $object_types,
    '#weight' => 10,
  );
  $form['actions']['#type'] = 'actions';

  // Submit and reset buttons.
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save outline'),
    '#submit' => array(
      'course_outline_overview_form_submit',
    ),
    '#attributes' => empty($_SESSION['course'][$node->nid]['editing']) ? array() : array(
      'class' => array(
        'highlight',
      ),
    ),
  );
  if (!empty($_SESSION['course'][$node->nid]['editing'])) {
    $form['actions']['reset'] = array(
      '#type' => 'submit',
      '#value' => t('Revert'),
      '#submit' => array(
        'course_outline_overview_form_reset',
      ),
      '#attributes' => array(
        'class' => array(
          'revert',
        ),
      ),
    );
  }

  // Add an additional set of action btns if there are more objects.
  if (count($objects) > variable_get('course_outline_show_btns_count', 4)) {
    $form['actions_additional'] = $form['actions'];
  }
  $cform['objects']['#element_validate'] = array(
    '_course_outline_overview_validate_objects',
  );

  //return for form on tab
  return $form;
}

/**
 * Rebuild outline overview form.
 */
function course_outline_overview_form_rebuild($form, $form_state) {
  return $form;
}

/**
 * Submit handler for resetting a Course back to stored defaults.
 */
function course_outline_overview_form_reset(&$form, &$form_state) {
  unset($_SESSION['course'][$form['nid']['#value']]['editing']);
}

/**
 * Form constructor for a course object.
 *
 * To be re-used in listing and creating new course objects.
 */
function _course_outline_object_form($courseObject = NULL) {
  $rform['#tree'] = TRUE;
  $uniqid = $courseObject
    ->getId();
  foreach (array(
    'coid',
    'instance',
    'module',
    'object_type',
    'nid',
  ) as $key) {
    $rform[$key] = array(
      '#type' => 'value',
      '#default_value' => $courseObject
        ->getOption($key),
    );
  }

  // Do not use prefix/suffix because markup only renders with a value, and we
  // need the wrapper before the title is saved for ajax population after each
  // settings modal update.
  $title = $courseObject
    ->getTitle();
  $rform['title'] = array(
    '#prefix' => '<div id="title-' . $uniqid . '">',
    '#suffix' => '</div>',
    '#type' => 'markup',
    '#markup' => check_plain($title ? $title : ' '),
  );
  $summary = $courseObject
    ->renderOptionsSummary();
  $rform['summary'] = array(
    '#prefix' => '<div id="summary-' . $uniqid . '">',
    '#suffix' => '</div>',
    '#type' => 'markup',
    '#markup' => filter_xss_admin($summary ? $summary : ' '),
  );

  // Placeholder for the settings link, it gets added after this function runs
  // in course_outline_overview_form(). #value needs a space for the prefix and
  // suffix to render.
  // Settings link for saved objects.
  $text = t('Settings');
  $path = "node/{$courseObject->getCourseNid()}/course-object/nojs/{$uniqid}/options";
  $l_options = array(
    'query' => array(
      'destination' => "node/{$courseObject->getCourseNid()}/course-outline",
    ),
  );
  $rform['options']['#markup'] = l($text, $path, $l_options);
  $rform['weight'] = array(
    '#type' => 'textfield',
    '#size' => 3,
    '#default_value' => $courseObject
      ->getOption('weight'),
    '#attributes' => array(
      'class' => array(
        'course-object-weight',
      ),
    ),
  );
  return $rform;
}

/**
 * Theme the course outline overview form as a table.
 *
 * @see course_outline_overview_form()
 *
 * @ingroup themeable
 */
function theme_course_outline_overview_form(&$variables) {
  $form =& $variables['form'];
  $objects =& $form['course_outline']['objects'];
  drupal_add_tabledrag('course-objects', 'order', 'sibling', 'course-object-weight');
  drupal_add_css(drupal_get_path('module', 'course') . '/css/admin.css');
  $rows = array();
  foreach (element_children($objects) as $key) {
    $object =& $objects[$key];
    unset($objects[$key]['title']['#title']);
    unset($objects[$key]['object_type']['#title']);
    $rows[$key]['data'][] = array(
      'data' => NULL,
      'width' => 1,
    );
    $rows[$key]['data'][] = drupal_render($objects[$key]['title']) . drupal_render($objects[$key]['summary']);
    $rows[$key]['data'][] = drupal_render($objects[$key]['object_type']) . drupal_render($objects[$key]['object_type_show']);
    $rows[$key]['data'][] = drupal_render($objects[$key]['options']);

    // Add draggable settings to row.
    $rows[$key]['class'] = array(
      'draggable',
    );
    $rows[$key]['data'][] = drupal_render($objects[$key]['weight']);

    // Add id to row for per-row ajax handling.
    $rows[$key]['id'] = "row-{$key}";

    // Deletion handling.
    $nid = $object['nid']['#value'];

    // @kludge should we unify this, so we always use $courseObject->getOptions()
    // instead of pulling from the session?
    if (!empty($_SESSION['course'][$nid]['editing'][$key]['delete'])) {
      $rows[$key]['class'] = array(
        'deleted',
      );
    }

    // Mark everything else as printed.
    drupal_render($objects[$key]);
  }
  $actions_additional = !empty($form['actions_additional']) ? drupal_render($form['actions_additional']) : '';
  $out = drupal_render_children($form);
  $out .= theme('table', array(
    'header' => [],
    'rows' => $rows,
    'attributes' => array(
      'id' => 'course-objects',
    ),
  ));
  $out .= $actions_additional;
  return $out;
}

/**
 * Validation callback.
 */
function _course_outline_overview_validate_objects(&$form, &$form_state) {
}

/**
 * Comparator function for course outline weights.
 */
function _course_outline_overview_form_cmp_function($a, $b) {
  if (is_object($a)) {
    if (isset($a->weight) && isset($b->weight)) {
      return $a->weight - $b->weight;
    }
  }
  else {
    if (isset($a['weight']) && isset($b['weight'])) {
      return $a['weight'] - $b['weight'];
    }
  }
}

/**
 * Submit handler.
 */
function course_outline_overview_form_submit(&$form, &$form_state) {
  if (empty($form_state['values']['course_outline']['objects'])) {

    // Empty form submitted.
    return;
  }
  $node = node_load($form['nid']['#value']);

  // Get form state values for object elements on the course outline overview:
  // - An associative array of course objects, keyed by ID. The ID for already
  //   saved objects is {course_outline}.coid, but for AHAH created objects the
  //   key is a generated unique ID until save.
  //   - coid: The key loaded from the database. If empty, the object is new.
  //   - module: The implementing module name (course_quiz etc).
  //   - object_type: The course object key as defined by
  //     hook_course_handlers().
  $objects = $form_state['values']['course_outline']['objects'];

  // Sort by weight so we can renumber.
  uasort($objects, '_course_outline_overview_form_cmp_function');
  foreach ($objects as $object_key => $object) {

    // Get each course object settings saved form values, which are not on the
    // outline overview form.
    if (!($courseObject = course_get_course_object_by_id($object_key))) {

      // For non-JS, no settings have been saved. We have to construct a new
      // course object.
      $courseObject = course_get_course_object($object);
    }
    $options = $courseObject
      ->getOptions();
    foreach ($options as $option => $value) {
      $courseObject
        ->setOption($option, $value);
    }

    // Renumber weights to the way draggable table would do it in case of no JS.
    $courseObject
      ->setOption('weight', $object['weight']);

    // We only save overview objects with a selected component, whether loaded
    // from the database or AHAH created.
    if (isset($object['object_type'])) {

      // Delete database loaded objects that are on the chopping block.
      $is_loaded = $object['coid'];
      if ($options['delete'] && $is_loaded) {
        if ($options['delete_instance']) {

          // Also delete related object instance(s) if specified.
          $courseObject
            ->delete();
        }
        course_outline_delete_object($object);
      }
      elseif (!$options['delete']) {

        // If we get this far, save the object.
        $courseObject
          ->save();
      }
    }
  }

  // Clear the editing session.
  unset($_SESSION['course'][$node->nid]['editing']);

  // Save the node to clear any caches.
  entity_get_controller('node')
    ->resetCache(array(
    $node->nid,
  ));
  drupal_set_message('Updated course.');
  $form_state['redirect'] = "node/{$node->nid}/course-outline";
}

/**
 * Generate HTML of the course outline.
 *
 * @param object $node
 *
 * @return course outline list.
 */
function course_outline_list($node) {
  global $user;
  $course = course_get_course($node);

  // Iterate over objects.
  $workflow = array();
  $img = NULL;
  foreach ($course
    ->getObjects() as $key => $courseObject) {
    if ($courseObject
      ->access('see', $user)) {

      // The item will be in the list only if the user can see it. If they can
      // take it, entity_view() will output a link instead of text.
      $entity = entity_load_single('course_object', $courseObject
        ->getId());
      $view = entity_view('course_object', array(
        $entity,
      ));
      $data = drupal_render($view);
      $item = array(
        'data' => $data,
        'id' => $courseObject
          ->getId(),
      );
      if ($courseObject
        ->access('take')) {

        // User can take this course object.
        $item['class'][] = 'accessible';

        // Step is complete.
        if ($courseObject
          ->getFulfillment($user)
          ->isComplete()) {
          $item['class'][] = 'completed';
        }
        elseif ($courseObject
          ->getFulfillment($user)
          ->getId()) {
          $item['class'][] = 'in-progress';
        }
        if ($course
          ->getActive() === $courseObject) {
          $item['class'][] = 'active';
        }
      }

      // Allow other modules to modify this list item.
      $courseObject
        ->overrideOutlineListItem($item);

      // Add this item to the list.
      $workflow[] = $item;
    }
  }
  if ($course
    ->getTracker($user)
    ->getOption('complete')) {
    $img = theme('image', array(
      'path' => 'misc/watchdog-ok.png',
      'alt' => t('Complete'),
    ));
    $workflow[] = array(
      'data' => $img . l(t('Complete'), "node/{$node->nid}/course-complete", array(
        'html' => TRUE,
      )),
      'id' => 'complete',
    );
  }
  $output = '';
  if ($workflow) {
    return theme('course_outline', array(
      'node' => $node,
      'items' => $workflow,
    ));
  }
  return $output;
}

/**
 * Render a landing page for course completion.
 *
 * @param stdClass $course_node A course node.
 *
 * @return render array for the completion landing page.
 */
function course_completion_page($course_node) {
  global $user;
  $account = $user;

  // User's course record.
  $report = course_report_load($course_node, $account);

  // Render array.
  $page = array();

  // Links.
  $links = array();
  $links['course'] = array(
    t('Return to course'),
    "node/{$course_node->nid}/takecourse",
    t('Return to the course to view course details and material.'),
  );
  if ($report
    ->getOption('complete')) {

    // Allow modules to add links to the course completion landing page, such as
    // post-course actions.
    drupal_alter('course_outline_completion_links', $links, $course_node, $account);
  }
  else {
    drupal_alter('course_outline_incomplete_links', $links, $course_node, $account);
  }
  drupal_set_title($report
    ->getOption('complete') ? t('Course complete') : t('Remaining requirements'));
  $course = course_get_course($course_node);
  $objects = $course
    ->getObjects();
  $items = array();
  foreach ($objects as $courseObject) {
    if ($courseObject
      ->access('see')) {

      // Find required course objects the user has not yet completed.
      $req = $courseObject
        ->getFulfillment($account);
      $status_css = $req
        ->isComplete() ? 'complete' : 'incomplete';
      $status_img = $req
        ->isComplete() ? 'ok' : ($req
        ->getCourseObject()
        ->isRequired() ? 'error' : 'warning');
      $status_class = 'course-complete-item-' . $status_img;
      $status_optional = ' (' . (!$req
        ->getCourseObject()
        ->isRequired() ? t('optional') : t('required')) . ')';
      if ($courseObject
        ->access('take')) {
        $link = l($req
          ->getCourseObject()
          ->getTitle(), $courseObject
          ->getUrl());
      }
      else {
        $link = $req
          ->getCourseObject()
          ->getTitle();
      }
      $items[] = array(
        'data' => array(
          array(
            'data' => theme('image', array(
              'path' => "misc/watchdog-{$status_img}.png",
              'width' => '',
              'height' => '',
              'alt' => $status_css,
            )),
            'width' => 20,
            'class' => array(
              'course-complete-item-status',
            ),
          ),
          array(
            'data' => $link . $status_optional . '<br/>' . $req
              ->getCourseObject()
              ->getStatus(),
            'class' => array(
              'course-complete-item-title',
            ),
          ),
        ),
        'class' => array(
          $status_class,
        ),
      );
    }
  }
  if ($report
    ->getOption('complete')) {
    $message = t('You have completed the course. Use the links below to review the course content.');
  }
  else {
    $message = t('This course is not complete. Use the links below to access the remaining course content.');
  }
  $page['course_header'] = array(
    '#type' => 'item',
    '#title' => t('Thank you for participating in this course.'),
    '#description' => $message,
    '#weight' => 1,
  );
  $page['course_completion_requirements'] = array(
    '#theme' => 'table',
    '#header' => NULL,
    '#rows' => $items,
    '#weight' => 3,
    '#attributes' => array(
      'class' => array(
        'course-complete-items',
      ),
    ),
  );
  foreach ($links as $key => $link) {
    $element = array(
      '#title' => l($link[0], $link[1], array(
        'html' => TRUE,
      )),
      '#description' => $link[2],
      '#type' => 'item',
    );
    $page['course_links'][$key] = $element;
  }
  $page['course_links']['#weight'] = 2;
  return $page;
}

Functions

Namesort descending Description
course_completion_page Render a landing page for course completion.
course_outline_list Generate HTML of the course outline.
course_outline_overview_form Form constructor for course outline form.
course_outline_overview_form_rebuild Rebuild outline overview form.
course_outline_overview_form_reset Submit handler for resetting a Course back to stored defaults.
course_outline_overview_form_submit Submit handler.
theme_course_outline_overview_form Theme the course outline overview form as a table.
_course_outline_object_form Form constructor for a course object.
_course_outline_overview_form_cmp_function Comparator function for course outline weights.
_course_outline_overview_validate_objects Validation callback.