Drupal 101

Or "How I Learned to Stop Worrying And

Love the Framework"

http://sildes.com/lawrencemiller/drupal101

Documentation?

 

  • Glossary:
    • https://www.drupal.org/glossary
  • Key concepts:
    • https://www.drupal.org/node/19828
  • ​Security:
    • https://www.drupal.org/documentation/is-drupal-secure

What is Drupal?

  • A community of many thousands of developers and users
     
  • A PHP Framework for building robust applications
     
  • A Content Management System

What is Drupal Made Of?

  • Content: in the database
  • Code: under version control
  • Media: on the file system

Content

  • "Nodes"
  • Entities and Entity Types
  • Views

Nodes

Nodes are the basic unit of content in Drupal. They can be extended with different types of fields (the default is just two fields).  Each recipe for extension is a "Content Type".

 

"Node" is not used in the mathematical sense.

Entity Types

An entity type is a useful abstraction to group together fields. Entity types are used to store and display data, which can be nodes (content), comments, taxonomy terms, user profiles, or something custom developed.

Views

Content displayed according to dynamic database queries.

Code: Drupal Core

Consists of lots and lots of php files, mainly. The Core Drupal maintainers release minor versions for security fixes and, very occasionally, new features. It is generally best practice to be on the current minor release, but each new version can be reviewed separately.

Drupal Core

Yeah, just leave it alone.

Code: Contributed

Drupal's main usefulness comes from the vast array of extensions, also written in php, that are available to it. Each such module is maintained separately from Drupal Core, and releases its own security updates via the Drupal Security list, which we monitor. In some cases, we heavily modify a contributed module, or build our own module.

Contrib

Third-party modules that extend Drupal's usefulness

<?php

/**
 * @file queue_ui.module
 */

define('QUEUE_UI_BASE', 'admin/config/system/queue-ui');

/**
 * Implements hook_permission().
 */
function queue_ui_permission() {
  return array(
    'admin queue_ui' => array(
      'title' => t('Administer queues'),
      'description' => t('View, run, and delete queues'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function queue_ui_menu() {
  $items[QUEUE_UI_BASE] = array(
    'title' => 'Queue manager',
    'description' => 'View and manage queues',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('queue_ui_page'),
    'access arguments' => array('admin queue_ui'),
    'file' => 'queue_ui.pages.inc',
  );

  $items[QUEUE_UI_BASE . '/inspect/%'] = array(
    'title' => 'View Queue: @name',
    'title arguments' => array('@name' => 5),
    'page callback' => 'queue_ui_view_queue',
    'page arguments' => array(5),
    'access arguments' => array('admin queue_ui'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'queue_ui.forms.inc',
  );

  // View item callback for Queue UI.
  $items[QUEUE_UI_BASE . '/%/view/%queue_ui_queue_item'] = array(
    'title' => 'View Queue Item',
    'description' => 'View the details of an individual queue item',
    'page callback' => 'queue_ui_view_queue_item',
    'page arguments' => array(4, 6),
    'access arguments' => array('admin queue_ui'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'queue_ui.forms.inc',
  );

  // Release item callback for Queue UI.
  $items[QUEUE_UI_BASE . '/%/release/%queue_ui_queue_item'] = array(
    'title' => 'Release items',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('queue_ui_release_item_form', 4, 6),
    'access arguments' => array('admin queue_ui'),
    'file' => 'queue_ui.forms.inc',
  );

  // Delete item callback for Queue UI.
  $items[QUEUE_UI_BASE . '/%/delete/%queue_ui_queue_item'] = array(
    'title' => 'Release items',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('queue_ui_delete_item_form', 4, 6),
    'access arguments' => array('admin queue_ui'),
    'file' => 'queue_ui.forms.inc',
  );

  return $items;
}

/**
 * Wildcard loader for queue_ui_queue.
 */
function queue_ui_queue_load($name) {
  return DrupalQueue::get($name);
}

/**
 * Wildcard loader for queue item.
 *
 * @param $queue_item
 *   The item id of the queue item to load.
 */
function queue_ui_queue_item_load($queue_item) {
  // @TODO - add validation of queue_item.
  return $queue_item;
}


// @todo remove before prod
function queue_ui_test() {
  $queue = DrupalQueue::get('queue ui test_me');
  $queue->createQueue();
  $num = mt_rand(0,99);
  for ($i = 0; $i < $num; $i++) {
    $queue->createItem(time());
  }
}

/**
 * Retrieve the QueueUI object for the class a particular queue is implemented as.
 *
 * @param $queue_name
 *  The name of the queue to retrieve the QueueUI class for.
 * @return mixte
 *  The QueueUI object for the relevant queue class, or FALSE if not found.
 */
function _queue_ui_queueclass($queue_name){
  $queue = DrupalQueue::get($queue_name);
  $class = get_class($queue);

  return QueueUI::get('QueueUI' . $class);
}

/**
 * Implements hook_queue_class_info.
 *
 * @return array of class information definitions.
 */
function queue_ui_queue_class_info() {
  // Implement queue class info definitions for core Drupal queue classes.
  return array(
    'SystemQueue' => array(
      'title' => t('System Queue (database Queue)'),
      'inspect' => TRUE,
      'operations' => array(
        'view',
        'release',
        'delete',
        'deleteall'),
    ),
    'MemoryQueue' => array(
      'title' => t('Memory queue (non-reliable)'),
      'inspect' => FALSE,
    ),
  );
}

/**
 * Gets queues class info defined with hook_queue_class_info().
 *
 * @param string $class
 *  The class name to retrieve
 *
 * @return Array of queue classes information.
 */
function queue_ui_get_queue_class_info($class) {
  $classes = &drupal_static(__FUNCTION__);
  if (!isset($classes)) {
    // Invoke hook_queue_class_info().
    $classes = module_invoke_all('queue_class_info');
  }
  return $classes[$class];
}


/**
 * Get the names of queues.
 *
 * @param Array of queue names suitable for DrupalQueue::get();
 */
function queue_ui_queue_names() {
  $result = db_query("SELECT DISTINCT name FROM {queue}");
  return $result->fetchAll();
}

/**
 * Get queues.
 *
 * @return Array of queues indexed by name and containing queue object and number
 * of items.
 */
function queue_ui_queues() {
  $queues = array();
  $queue_names = queue_ui_queue_names();
  if (!empty($queue_names)) {
    // Build array of queues indexed by name with number of items.
    foreach ($queue_names as $name) {
      $queue = DrupalQueue::get($name->name);
      $class = get_class($queue);
      $queues[$class][$name->name] = array(
        'queue' => $queue,
        'items' => $queue->numberOfItems(),
      );
    }
  }
  return $queues;
}

/**
 * Get queues defined with hook_queue_info().
 *
 * @return Array of queues indexed by name and containing
 */
function queue_ui_defined_queues() {
  $queues = &drupal_static(__FUNCTION__);
  if (!isset($queues)) {
    // Invoke hook_queue_info().
    $queues = module_invoke_all('queue_info');
  }
  return $queues;
}

/**
 * Implements hook_cron().
 */
function queue_ui_cron() {
  // Retrieve queues set for cron processing.
  $defs = queue_ui_defined_queues();
  if (!empty($defs)) {
    foreach ($defs as $name => $definition) {
      $queue = DrupalQueue::get($name);
      // A cron callback must be defined and there must be items in the queue.
      if (isset($definition['cron']) && is_object($queue) && $queue->numberOfItems()) {
        $active = variable_get('queue_ui_cron_' . $name, FALSE);
        if ($active) {
          // Pass $queue to cron callback for processing.
          $function = $definition['cron']['callback'];
          // Definitions can define arguments.
          $args = isset($definition['cron']['callback']) ? $definition['cron']['callback'] : NULL;
          $function($queue, $args);
        }
      }
    }
  }
}

// @todo remove before prod
function queue_ui_queue_info() {
  return array(
    'queue ui test_me' => array(
      'title' => t('Test queue'),
      'batch' => array(
        'operations' => array(array('queue_ui_batch_test', array())),
        'finished' => 'queue_ui_batch_finished',
        'title' => t('Processing test queue'),
      ),
      'cron' => array(
        'callback' =>'queue_ui_test_process',
      ),
    ),
  );
}

function queue_ui_test_process($queue) {
  $count = $queue->numberOfItems();
  for ($i = 0; $i < 20 && $count > 0; $i++) {
    $item = $queue->claimItem(20); // Lease time.
    if ($item) {
      // We would do some processing, if this were REAL.
      $queue->deleteItem($item);
      $count--;
    }
  }
}

function queue_ui_batch_test($queue, &$context) {
  if (empty($context['sandbox'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['current'] = 0;
    $context['sandbox']['max'] = $queue->numberOfItems();
  }
  for ($i = 0; $i < 20 && $context['sandbox']['current'] < $context['sandbox']['max']; $i++) {
    $item = $queue->claimItem(20); // Lease time.
    if ($item) {
      // We would do some processing, if this were REAL.
      $queue->deleteItem($item);
    }
    $context['sandbox']['progress']++;
    $context['sandbox']['current']++;
  }
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

function queue_ui_batch_finished($success, $results, $operations) {
  if ($success) {
    $message = 'success';
  }
  else {
    $message = t('An error occured @todo.');
  }
  drupal_set_message($message);
}

Custom

<?php

/*
 * Implementation of some hook or other; we want to run this whenever one of the
 * epp menu pages is accessed, but we don't want to run it on every page load.
 *
 * This function loads the EPP libraries.
 */

function epp_load_epp() {
  require_once('sites/all/libraries/FirePHPCore/FirePHP.class.php');
  if ($path = libraries_get_path('epp_php')) {
    include_once "$path/EppClient.class.php";
//spl_autoload_unregister("drupal_autoload_class");
//spl_autoload_unregister("drupal_autoload_interface");
    spl_autoload_register(array('EppClient', 'eppAutoload'));
    return TRUE;
  }
}

/*
 * implementation of hook_permission
 */

function epp_permission() {

  $perm = array();
  $perm['administer epp'] = array(
    'title' => t('Administer EPP'),
    'description' => t('Perform administrative tasks such as setting EPP server'),
    'restrict access' => TRUE,
  );
  return $perm;
}

/*
 * Implementation of hook_menu
 */

function epp_menu() {
  $items = array();
  $items['admin/config/epp'] = array(
    'title' => t('Epp Settings page'),
    'access arguments' => array('administer epp'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('epp_settings'),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/epp/epp'] = array(
    'title' => t('Epp Settings page'),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -20,
  );
  $items['epp/domain_info/%'] = array(
    'title callback' => 'epp_domain_info_title_callback',
    'title arguments' => array(2),
    'access arguments' => array('administer epp'),
    'page callback' => 'epp_domain_info',
    'page arguments' => array(2),
  );
  return $items;
}

/*
 * epp_domain_info title callback
 */

function epp_domain_info_title_callback($arg) {
  return "Domain Info for " . $arg;
}

/*
 * Issue domainInfo on a domain name, receive an SrsDomain object, return it
 */

function epp_get_domain($domain = NULL) {
  epp_load_epp();
  $eppRegistry = epp_login();
  try {
    $result = $eppRegistry->domainInfo($domain);
    return $result;
  }
  catch (Exception $e) {
    throw $e;
  }
}

/**
 * @param null $contact
 * @return Exception|SrsContact
 *
 * @author Lawrence Miller
 */
function epp_get_contact($contact = NULL) {
  epp_load_epp();
  $eppRegistry = epp_login();
  try {
    $result = $eppRegistry->contactInfo($contact);
    return $result;
  }
  catch (Exception $e) {
    return $e;
  }
}


/*
 * Wrapper for domainCreate; requires an SrsDomain object as an argument
 * $domain: an SrsDomain object
 * $term: number of years the registration is for
 * $pwd: a password (optional)
 */

function epp_domain_create($domain, $term = 1, $pwd = NULL) {
  epp_load_epp();
  $eppRegistry = epp_login();
  $result = $eppRegistry->domainCreate($domain, $term, $pwd);
  return $result;
}

/*
 * Wrapper for domainUpdate: requires an SrsDomain as an object, optionally
 * takes a second one as an original.
 */

function epp_domain_update($domain, $orig = NULL) {
  epp_load_epp();
  $eppRegistry = epp_login();
  $result = $eppRegistry->domainUpdate($domain, $orig);
  return $result;
}

/*
 * function epp_domain_add_status
 * helper function to collect a domain name string and a status string and
 * create a correctly formed epp_domain_update to add the status
 */

function epp_domain_add_status($domainName, $status, $reason_code) {
  epp_load_epp();
  $domain = new SrsDomain();
  $orig = new SrsDomain();
  $stat = new SrsStatus();
  try {
    $stat->setStatus($status);
    $stat->setUpdateAdd();
    $stat->setReasonCode($reason_code);
    $orig->name = $domainName;
    $domain->status = array($stat);
    $domain->name = $domainName;
    $result = epp_domain_update($domain, $orig);
    return $result;
  }
  catch (Exception $e) {
    drupal_set_message(t($domainName . ' | Error: EPP status entered (' . $status . '): '  . $e->getMessage() . '.'), 'error');
    watchdog('EPP', t($domainName . ' | Error: EPP status entered (%status): %error_msg.'), $variables = array('%status' => $status, '%domainName' => $domainName, '%error_msg' => $e->getMessage()), $severity = WATCHDOG_ERROR);
  }
  //dsm($domain);
}

/*
 * function epp_domain_remove_status
 * helper function to collect a domain name string and a status string and
 * create a correctly formed epp_domain_update to remove the status
 */

function epp_domain_remove_status($domainName, $status, $reason_code) {
  //dsm("trying to remove $status from $domainName");
  epp_load_epp();
  $domain = new SrsDomain();
  $orig = new SrsDomain();
  $stat = new SrsStatus();
  try {
    $stat->setStatus($status);
    $stat->setReasonCode($reason_code);
    $stat->setUpdateRemove();
    $orig->name = $domainName;
    $domain->status = array($stat);
    $domain->name = $domainName;
    //dsm($domain);
    $result = epp_domain_update($domain, $orig);
    return $result;
  }
  catch (Exception $e) {
    drupal_set_message(t($domainName . ' | Status entered is not a legitimate EPP status.'), 'error');
  }
}

/*
 * Define default EPP server credentials
 */
define('EPP_SERVER', variable_get('epp_server', 'ote1.dotproregistry.net '));
define('EPP_USER', variable_get('epp_user', 'OTE-BSS'));
define('EPP_PASSWORD', variable_get('epp_password', ''));
define('EPP_PORT', variable_get('epp_port', '700'));
/**
 * @todo refactor this so certificate chains can exist anywhere.
 */
define('EPP_CERT', dirname(__FILE__) . '/certs/' . variable_get('epp_cert', 'newchain.pem'));

/*
 * epp_login
 * instantiates the libraries and attempts to login using the stored credentials
 */

function epp_login() {
  epp_load_epp();
  $eppRegistry = new EppRegistry();

  $eppRegistry->connect(EPP_SERVER, EPP_PORT, EPP_CERT);
  $eppRegistry->login(EPP_USER, EPP_PASSWORD);
  return $eppRegistry;
}

/*
 * Epp Settings Menu
 */

function epp_settings() {
  $form['epp_server'] = array(
    '#type' => 'textfield',
    '#title' => 'EPP Server',
    '#description' => t('Please enter the EPP server'),
    '#default_value' => variable_get('epp_server'),
  );
  $form['epp_port'] = array(
    '#type' => 'textfield',
    '#title' => 'EPP Port',
    '#default_value' => variable_get('epp_port'),
  );
  $form['epp_user'] = array(
    '#type' => 'textfield',
    '#title' => 'EPP User ID',
    '#description' => t('What is the User ID on the EPP server we should use?'),
    '#default_value' => variable_get('epp_user'),
  );
  $form['epp_password'] = array(
    '#type' => 'textfield',
    '#title' => 'EPP Password',
    '#descriptuon' => t('What is the password for the above user on this server?'),
    '#default_value' => variable_get('epp_password'),
  );
  $form['epp_cert'] = array(
    '#type' => 'textfield',
    '#title' => 'EPP Certificate Chain File',
    '#description' => t('What is the filename of the certificate chain file (.pem)?  Must be located in /sites/all/modules/epp/certs'),
    '#default_value' => variable_get('epp_cert'),
  );

  return system_settings_form($form);
}

function epp_action_info() {
  $actions = array(
    'epp_domain_check_test' => array(
      'label' => t('This is my domain check test.'),
      'type' => 'node',
      'configurable' => FALSE,
      'hooks' => array(
        'nodeapi' => array('update')
      )
    )
  );
  return $actions;
}

function epp_domain_check($domain) {
  epp_load_epp();
  $eppRegistry = epp_login();
  // @TODO: implement try/catch
  try {
    $result = $eppRegistry->domainCheck($domain);
    return $result;
  } catch(Exception $e) {
    drupal_set_message(t($domainName . ' is not allowed.'), 'error');
  }
}

Code: Themes

A Theme consists of a set of stylesheets, images, and HTML (well, PHP actually) that describe the look and feel of the site. All of our sites use custom themes. Some parts of themes, such as button images, aren't actually code at all, but should be under version control, so they are treated the same as code; changes require a new build.

 

We prefer to build subthemes, which inherit from their parents but can override them completely.

Injecting content

  1. In your module, hook into the theme to add stuff to the $content variable
<?php
/**
 * Created by JetBrains PhpStorm.
 * User: ldpm
 * To change this template use File | Settings | File Templates.
 */

/**
 * @param $node
 * @param $view_mode
 * @todo combine this with the REST retrieve code to avoid duplication
 */
function uploader_batch_view_node_view($node, $view_mode)
{
  if ($node->type == "batch" && $view_mode == 'full') {
    // count the number of non-new status documents from this batch
    $non_new = db_query("select ds.status_text, count(*) from {documents} d INNER JOIN document_statuses AS ds on (d.status = ds.status_id) WHERE d.batch_id = $node->nid group by ds.status_text")->fetchAllKeyed();
    $node->content['batch_data'] = $non_new;
    return $node;

  }
}

/**
 * @param $batchID
 * @param string $limit
 * @return string
 */
function uploader_batch_view_render_documents($batchID, $limit = '10')
{
  $header_row = array(
    array('data' => 'Document ID', 'field' => 'd.document_id'),
    array('data' => 'Text', 'field' => 'd.document_text'),
    array('data' => 'Status', 'field' => 'ds.status_text'),
  );
  $query = db_select('documents', 'd')
    ->condition('batch_id', $batchID)
    ->fields('d', array('document_id','document_text'))
    ->fields('ds', array('status_text'));
  $query->join('document_statuses', 'ds', 'd.status = ds.status_id');
  $query = $query->extend('PagerDefault')->limit($limit);
  $query = $query->extend('TableSort')->orderByHeader($header_row);
  $result = $query->execute();
  foreach ($result as $row) {
    $table_rows[] = array($row->document_id, $row->document_text, $row->status_text);
  }
  return theme_table(array('header' => $header_row,
    'rows' => $table_rows, 'attributes' => array(), 'empty' => 'Data not found',
  ));
}

Injecting Content

Step 2: In your theme, run a function that you defined in your module

<?php

/**
 * @file
 * Default theme implementation to display a node.
 *
 * Available variables:
 * - $title: the (sanitized) title of the node.
 * - $content: An array of node items. Use render($content) to print them all,
 *   or print a subset such as render($content['field_example']). Use
 *   hide($content['field_example']) to temporarily suppress the printing of a
 *   given element.
 * - $user_picture: The node author's picture from user-picture.tpl.php.
 * - $date: Formatted creation date. Preprocess functions can reformat it by
 *   calling format_date() with the desired parameters on the $created variable.
 * - $name: Themed username of node author output from theme_username().
 * - $node_url: Direct url of the current node.
 * - $display_submitted: whether submission information should be displayed.
 * - $classes: String of classes that can be used to style contextually through
 *   CSS. It can be manipulated through the variable $classes_array from
 *   preprocess functions. The default values can be one or more of the
 *   following:
 *   - node: The current template type, i.e., "theming hook".
 *   - node-[type]: The current node type. For example, if the node is a
 *     "Blog entry" it would result in "node-blog". Note that the machine
 *     name will often be in a short form of the human readable label.
 *   - node-teaser: Nodes in teaser form.
 *   - node-preview: Nodes in preview mode.
 *   The following are controlled through the node publishing options.
 *   - node-promoted: Nodes promoted to the front page.
 *   - node-sticky: Nodes ordered above other non-sticky nodes in teaser
 *     listings.
 *   - node-unpublished: Unpublished nodes visible only to administrators.
 * - $title_prefix (array): An array containing additional output populated by
 *   modules, intended to be displayed in front of the main title tag that
 *   appears in the template.
 * - $title_suffix (array): An array containing additional output populated by
 *   modules, intended to be displayed after the main title tag that appears in
 *   the template.
 *
 * Other variables:
 * - $node: Full node object. Contains data that may not be safe.
 * - $type: Node type, i.e. story, page, blog, etc.
 * - $comment_count: Number of comments attached to the node.
 * - $uid: User ID of the node author.
 * - $created: Time the node was published formatted in Unix timestamp.
 * - $classes_array: Array of html class attribute values. It is flattened
 *   into a string within the variable $classes.
 * - $zebra: Outputs either "even" or "odd". Useful for zebra striping in
 *   teaser listings.
 * - $id: Position of the node. Increments each time it's output.
 *
 * Node status variables:
 * - $view_mode: View mode, e.g. 'full', 'teaser'...
 * - $teaser: Flag for the teaser state (shortcut for $view_mode == 'teaser').
 * - $page: Flag for the full page state.
 * - $promote: Flag for front page promotion state.
 * - $sticky: Flags for sticky post setting.
 * - $status: Flag for published status.
 * - $comment: State of comment settings for the node.
 * - $readmore: Flags true if the teaser content of the node cannot hold the
 *   main body content.
 * - $is_front: Flags true when presented in the front page.
 * - $logged_in: Flags true when the current user is a logged-in member.
 * - $is_admin: Flags true when the current user is an administrator.
 *
 * Field variables: for each field instance attached to the node a corresponding
 * variable is defined, e.g. $node->body becomes $body. When needing to access
 * a field's raw values, developers/themers are strongly encouraged to use these
 * variables. Otherwise they will have to explicitly specify the desired field
 * language, e.g. $node->body['en'], thus overriding any language negotiation
 * rule that was previously applied.
 *
 * @see template_preprocess()
 * @see template_preprocess_node()
 * @see template_process()
 */
?>
<article id="node-<?php print $node->nid; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>

  <?php if ($user_picture || !$page || $display_submitted): ?>
    <header>
      <?php print $user_picture; ?>

      <?php print render($title_prefix); ?>
      <?php if (!$page): ?>
        <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $title; ?></a></h2>
      <?php endif; ?>
      <?php print render($title_suffix); ?>

      <?php if ($display_submitted): ?>

        <p class="submitted">
          <?php print $submitted; ?>
          <time pubdate datetime="<?php print $submitted_pubdate; ?>">
            <?php print $submitted_date; ?>
          </time>
        </p>

      <?php endif; ?>
    </header>
  <?php endif; ?>

  <div class="content"<?php print $content_attributes; ?>>
    <?php
    // We hide the comments, tags and links now so that we can render them later.
    hide($content['comments']);
    hide($content['links']);
    hide($content['field_tags']);

    $slice_colors = array(
      "New" => "blue",
      "Processing" => "yellow",
      "Accepted" => "green",
      "Rejected" => "red",
    );

    $foo = array_values($content['batch_data']);
    //dsm($foo);
    $ints = array();
    foreach ($foo as $f) {
      $ints[] = (int)$f;
    }

    $keys = array_keys($content['batch_data']);
    foreach ($keys as $k) {
      $slices[] = array("color" => $slice_colors[$k]);
    }

    $chart = array(
      '#type' => 'chart',
      '#title' => 'Batch',
      '#chart_type' => 'pie',
      '#slices' => $slices,
    );
    $chart['pie_data'] = array(
      '#type' => 'chart_data',
      '#title' => 'Hello, world!',
      '#labels' => array_keys($content['batch_data']),
      '#data' => $ints,
    );
    print render($chart);
    print render($content);
    print uploader_batch_view_render_documents($node->nid);
    print theme('pager', array('tags' => array()));
    ?>
  </div>
  <!-- /.content -->

  <?php if (!empty($content['field_tags']) || !empty($content['links'])): ?>
    <footer>
      <?php print render($content['field_tags']); ?>
      <?php print render($content['links']); ?>
    </footer>
  <?php endif; ?>

  <?php print render($content['comments']); ?>

</article><!-- /.node -->

Media

"Media" refers to files that are uploaded by users of the site. A very common example would be a pdf file that is uploaded as an "attachment" to a page. These files live on the file system, but are not under subversion control; to prevent them from being overwritten whenever there's a new build, we typically store them in a folder outside the Apache documentroot. Though these files are obviously not in the database, the path to the files (and other metadata) is in the database.

Drupal101

By Lawrence Miller

Drupal101

  • 632