date_ical_plugin_row_ical_entity.inc 14.4 KB
Newer Older
Karen Stevenson's avatar
Karen Stevenson committed
1
<?php
2

Karen Stevenson's avatar
Karen Stevenson committed
3 4 5 6 7 8 9 10 11
/**
 * @file
 * Contains the iCal row style plugin.
 */

/**
 * Plugin which creates a view on the resulting object
 * and formats it as an iCal VEVENT.
 */
Robert Rollins's avatar
Robert Rollins committed
12
class date_ical_plugin_row_ical_entity extends views_plugin_row {
13
  
Karen Stevenson's avatar
Karen Stevenson committed
14 15 16
  // Basic properties that let the row style follow relationships.
  var $base_table = 'node';
  var $base_field = 'nid';
17
  
Karen Stevenson's avatar
Karen Stevenson committed
18 19
  // Stores the nodes loaded with pre_render.
  var $entities = array();
20
  
Karen Stevenson's avatar
Karen Stevenson committed
21 22 23 24 25
  function init(&$view, &$display, $options = NULL) {
    parent::init($view, $display, $options);
    $this->base_table = $view->base_table;
    $this->base_field = $view->base_field;
  }
26
  
Karen Stevenson's avatar
Karen Stevenson committed
27 28 29
  function option_definition() {
    $options = parent::option_definition();
    $options['date_field'] = array('default' => array());
30
    $options['summary_field'] = array('default' => array());
31
    $options['location_field'] = array('default' => array());
Karen Stevenson's avatar
Karen Stevenson committed
32 33
    return $options;
  }
34
  
Karen Stevenson's avatar
Karen Stevenson committed
35 36 37 38 39
  /**
   * Provide a form for setting options.
   */
  function options_form(&$form, &$form_state) {
    parent::options_form($form, $form_state);
40 41 42
    
    // Build the select dropdown for the date field that the user wants to use
    // to populate the date fields in VEVENTs.
Karen Stevenson's avatar
Karen Stevenson committed
43 44 45 46 47 48 49 50 51 52 53 54 55
    $data = date_views_fields($this->base_table);
    $options = array();
    foreach ($data['name'] as $item => $value) {
      // We only want to see one value for each field, skip '_value2', and other columns.
      if ($item == $value['fromto'][0]) {
        $options[$item] = $value['label'];
      }
    }
    $form['date_field'] = array(
      '#type' => 'select',
      '#title' => t('Date field'),
      '#options' => $options,
      '#default_value' => $this->options['date_field'],
Robert Rollins's avatar
Robert Rollins committed
56 57
      '#description' => t('Please identify the field to use as the iCal date for each item in this view.
          Add a Date Filter or a Date Argument to the view to limit results to content in a specified date range.'),
Karen Stevenson's avatar
Karen Stevenson committed
58
      '#required' => TRUE,
Robert Rollins's avatar
Robert Rollins committed
59 60 61 62 63 64 65 66
    );
    $form['instructions'] = array(
      // The surrounding <div> is necessary to ensure that the settings dialog expands to show everything.
      '#prefix' => '<div style="font-size: 90%">',
      '#suffix' => '</div>',
      '#markup' => t("Each item's Title and iCal view mode will be included as the SUMMARY and DESCRIPTION elements (respectively) in the VEVENTs output by this View.
        <br>To change the iCal view mode, configure it on the 'Manage Display' page for each Content Type.
        Please note that HTML will be stripped from the output (link URLs will become footnotes), to comply with iCal standards."),
Karen Stevenson's avatar
Karen Stevenson committed
67
    );
68
    
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
    // Build the select dropdown for the text/node_reference field that the user
    // wants to use to (optionally) populate the SUMMARY.
    $summary_fields = date_ical_get_summary_fields($this->base_table);
    $summary_options = array('default_title' => t('- Default Title -'));
    foreach ($summary_fields['name'] as $item => $value) {
      $summary_options[$item] = $value['label'];
    }
    $form['summary_field'] = array(
      '#type' => 'select',
      '#title' => t('SUMMARY field'),
      '#options' => $summary_options,
      '#default_value' => $this->options['summary_field'],
      '#description' => t('You may optionally change the SUMMARY component for each event in the iCal output.
        Choose which text, taxonomy term reference or Node Reference field you would like to be output as the SUMMARY.
        If using a Node Reference, the Title of the referenced node will be used.'),
    );
85
    
86 87 88
    // Build the select dropdown for the text/node_reference field that the user
    // wants to use to (optionally) populate the LOCATION.
    $location_fields = date_ical_get_location_fields($this->base_table);
Robert Rollins's avatar
Robert Rollins committed
89
    $location_options = array('none' => t('- None -'));
90 91 92 93 94 95 96 97 98 99 100 101
    foreach ($location_fields['name'] as $item => $value) {
      $location_options[$item] = $value['label'];
    }
    $form['location_field'] = array(
      '#type' => 'select',
      '#title' => t('LOCATION field'),
      '#options' => $location_options,
      '#default_value' => $this->options['location_field'],
      '#description' => t('You may optionally include a LOCATION component for each event in the iCal output.
        Choose which text or Node Reference field you would like to be output as the LOCATION.
        If using a Node Reference, the Title of the referenced node will be used.'),
    );
Karen Stevenson's avatar
Karen Stevenson committed
102
  }
103
  
Karen Stevenson's avatar
Karen Stevenson committed
104 105 106 107
  function pre_render($values) {
    // @TODO When the date is coming in through a relationship, the nid
    // of the view is not the right node to use, then we need the related node.
    // Need to sort out how that should be handled.
108
    
Karen Stevenson's avatar
Karen Stevenson committed
109 110 111 112 113 114 115 116
    // Preload each entity used in this view from the cache.
    // Provides all the entity values relatively cheaply, and we don't
    // need to do it repeatedly for the same entity if there are
    // multiple results for one entity.
    $ids = array();
    foreach ($values as $row) {
      // Use the $id as the key so we don't create more than one value per entity.
      $id = $row->{$this->field_alias};
117
      
Karen Stevenson's avatar
Karen Stevenson committed
118 119 120 121 122 123 124 125 126 127
      // Node revisions need special loading.
      if ($this->view->base_table == 'node_revision') {
        $this->entities[$id] = node_load(NULL, $id);
      }
      // For other entities we just create an array of ids to pass
      // to entity_load().
      else {
        $ids[$id] = $id;
      }
    }
128
    
Karen Stevenson's avatar
Karen Stevenson committed
129 130 131 132 133
    $base_tables = date_views_base_tables();
    $this->entity_type = $base_tables[$this->view->base_table];
    if (!empty($ids)) {
      $this->entities = entity_load($this->entity_type, $ids);
    }
134
    
Karen Stevenson's avatar
Karen Stevenson committed
135 136 137 138 139 140 141
    // Get the language for this view.
    $this->language = $this->display->handler->get_option('field_language');
    $substitutions = views_views_query_substitutions($this->view);
    if (array_key_exists($this->language, $substitutions)) {
      $this->language = $substitutions[$this->language];
    }
  }
142
  
Karen Stevenson's avatar
Karen Stevenson committed
143 144 145 146
  function render($row) {
    global $base_url;
    $id = $row->{$this->field_alias};
    if (!is_numeric($id)) {
147
      return NULL;
Karen Stevenson's avatar
Karen Stevenson committed
148
    }
149
    
Karen Stevenson's avatar
Karen Stevenson committed
150 151 152
    // Load the specified entity:
    $entity = $this->entities[$id];
    if (empty($entity)) {
153 154
      // This can happen when an RRULE is involved.
      return NULL;
Karen Stevenson's avatar
Karen Stevenson committed
155
    }
156 157 158 159 160 161 162 163
    
    $date_fields = date_views_fields($this->base_table);
    $date_info = $date_fields['name'][$this->options['date_field']];
    $field_name  = str_replace(array('_value', '_value2'), '', $date_info['real_field_name']);
    $table_name  = $date_info['table_name'];
    $delta_field = $date_info['delta_field'];
    $is_field    = $date_info['is_field'];
    
Karen Stevenson's avatar
Karen Stevenson committed
164 165 166 167 168 169
    // This is ugly and hacky but I can't figure out any generic way to
    // recognize that the node module is going to give some the revision timestamp
    // a different field name on the entity than the actual column name in the database.
    if ($this->view->base_table == 'node_revision' && $field_name == 'timestamp') {
      $field_name = 'revision_timestamp';
    }
170
    
171 172 173
    if (!isset($entity->$field_name)) {
      // This entity doesn't have the date property that the user configured
      // our view to use. We can't do anything with it
174
      return NULL;
175 176 177
    }
    $date_field = $entity->$field_name;
    
178
    // Pull the date value from the specified field of the entity.
Karen Stevenson's avatar
Karen Stevenson committed
179 180 181
    $entity->date_id = array();
    $start = NULL;
    $end   = NULL;
182
    $delta = isset($row->$delta_field) ? $row->$delta_field : 0;
Karen Stevenson's avatar
Karen Stevenson committed
183
    if ($is_field) {
184
      $items = field_get_items($this->entity_type, $entity, $field_name);
185
      if (!$items) {
186 187
        // This entity doesn't have data in the date field that the user
        // configured our view to use. We can't do anything with it.
188 189
        return;
      }
190 191 192 193
      $date_field = $items[$delta];
      $domain = check_plain($_SERVER['SERVER_NAME']);
      $entity->date_id[] = "calendar.$id.$field_name.$delta@$domain";
      
194 195 196 197
      if (!empty($date_field['value'])) {
        $start = new DateObject($date_field['value'], $date_field['timezone_db']);
        if (array_key_exists('value2', $date_field)) {
          $end = new DateObject($date_field['value2'], $date_field['timezone_db']);
Karen Stevenson's avatar
Karen Stevenson committed
198 199 200 201 202 203
        }
        else {
          $end = clone $start;
        }
      }
    }
204 205 206
    elseif (!$is_field && !empty($date_field)) {
      $start = new DateObject($date_field, $date_field['timezone_db']);
      $end   = new DateObject($date_field, $date_field['timezone_db']);
Karen Stevenson's avatar
Karen Stevenson committed
207
    }
208
    
209
    // Set the item date to the proper display timezone.
210 211
    $start->setTimezone(new DateTimeZone($date_field['timezone']));
    $end->setTimezone(new DateTimeZone($date_field['timezone']));
212
    
213 214 215 216 217 218
    // Check if the start and end dates indicate that this is an All Day event.
    $all_day = date_is_all_day(
      date_format($start, DATE_FORMAT_DATETIME),
      date_format($end, DATE_FORMAT_DATETIME),
      date_granularity_precision($date_info['granularity'])
    );
219
    
Karen Stevenson's avatar
Karen Stevenson committed
220
    if ($all_day) {
221 222 223
      // According to RFC 2445 (clarified in RFC 5545) the DTEND value is
      // non-inclusive. When dealing with All Day values, they're DATEs rather
      // than DATETIMEs, so we need to add a day to conform to RFC.
Karen Stevenson's avatar
Karen Stevenson committed
224 225
      $end->modify("+1 day");
    }
226 227 228 229
    
    // If the user specified a LOCATION field, pull that data from the entity.
    $location = '';
    if (!empty($this->options['location_field']) && $this->options['location_field'] != 'none') {
230
      $location_fields = date_ical_get_location_fields($this->base_table);
231 232 233 234
      $location_info = $location_fields['name'][$this->options['location_field']];
      $location_field_name = $location_info['real_field_name'];
      
      // Only attempt this is the entity actually has this field.
235 236 237
      $items = field_get_items($this->entity_type, $entity, $location_field_name);
      if ($items) {
        $location_field = $items[$delta];
238
        if ($location_info['type'] == 'node_reference') {
239 240 241 242
          // Make sure this Node Reference actually references a node.
          if ($location_field['nid']) {
            $node = node_load($location_field['nid']);
            $location = check_plain($node->title);
243 244
          }
        }
245 246 247 248 249 250 251 252 253
        elseif ($location_info['type'] == 'addressfield') {
          $locations = array();
          foreach($location_field as $key => $loc) {
            if ($loc && !in_array($key, array('first_name', 'last_name'))) {
              $locations[] = $loc;
            }
          }
          $location = implode(', ', array_reverse($locations));
        }
254
        else {
255
          $location = check_plain($location_field['value']);
256 257 258 259
        }
      }
    }
    
Karen Stevenson's avatar
Karen Stevenson committed
260 261
    // Create the rendered display using the display settings from the 'iCal' view mode.
    $rendered_array = entity_view($this->entity_type, array($entity), 'ical', $this->language, TRUE);
262 263 264 265
    $data = array(
      'description' => drupal_render($rendered_array),
      'summary' => entity_label($this->entity_type, $entity)
    );
266 267 268 269 270 271 272 273 274 275 276 277 278
    if (!empty($this->options['summary_field']) && $this->options['summary_field'] != 'default_title') {
      $summary_fields = date_ical_get_summary_fields();
      $summary_info = $summary_fields['name'][$this->options['summary_field']];
      $summary_field_name = $summary_info['real_field_name'];
      // Only attempt this is the entity actually has this field.
      $items = field_get_items($this->entity_type, $entity, $summary_field_name);
      $summary = '';
      if ($items) {
        $summary_field = $items[$delta];
        if ($summary_info['type'] == 'node_reference') {
          // Make sure this Node Reference actually references a node.
          if ($summary_field['nid']) {
            $node = node_load($summary_field['nid']);
279
            $summary = $node->title;
280 281 282 283 284 285
          }
        }
        elseif ($summary_info['type'] == 'taxonomy_term_reference') {
          $terms = taxonomy_term_load_multiple($items);
          // Make sure that there are terms that were loaded
          if ($terms) {
286
            $term_names = array();
287
            foreach ($terms as $term) {
288
              $term_names[] = $term->name;
289
            }
290
            $summary = implode(', ', $term_names);
291 292 293
          }
        }
        else {
294
          $summary = trim($summary_field['value']);
295
        }
296
      $data['summary'] = $summary ? $summary : $data['summary'];
297 298
      }
    }
299 300 301 302 303 304 305 306
    // Allow other modules to alter the HTML of the Summary and Description,
    // before it gets converted to iCal-compliant plaintext. This allows users
    // to set up a newline between fields, for instance.
    $context = array(
      'entity' => $entity,
      'entity_type' => $this->entity_type,
      'language' => $this->language,
    );
307
    drupal_alter('date_ical_export_html', $data, $this->view, $context);
Robert Rollins's avatar
Robert Rollins committed
308
    
Karen Stevenson's avatar
Karen Stevenson committed
309
    $event = array();
310 311
    $event['summary'] = date_ical_sanitize_text($data['summary']);
    $event['description'] = date_ical_sanitize_text($data['description']);
Karen Stevenson's avatar
Karen Stevenson committed
312
    $event['all_day'] = $all_day;
Robert Rollins's avatar
Robert Rollins committed
313 314
    $event['start'] = $start;
    $event['end'] = $end;
315 316 317
    $uri = entity_uri($this->entity_type, $entity);
    $uri['options']['absolute'] = TRUE;
    $event['url'] = url($uri['path'], $uri['options']);
318
    $event['rrule'] = $is_field && array_key_exists('rrule', $date_field) ? $date_field['rrule'] : '';
319 320 321
    if ($location) {
      $event['location'] = $location;
    }
Robert Rollins's avatar
Robert Rollins committed
322
    
323 324 325
    // For this event's UID, use either the date_id generated by the Date module, or the
    // event page's URL, if the date_id isn't available.
    $event['uid'] = !empty($entity->date_id) ? $entity->date_id[0] : $event['url'];
326
    
327 328 329
    // If we are using a repeat rule (and not just multi-day events) we
    // remove the item from the entities list so that its VEVENT won't be
    // re-created.
330 331 332
    if ($event['rrule']) {
      $this->entities[$id] = NULL;
    }
333
    
334 335
    // According to the iCal standard, CREATED and LAST-MODIFIED must be UTC.
    // Fortunately, Drupal stores timestamps in the DB as UTC, so we just need
336
    // to tell Date to treat the timestamp as UTC from the start.
337
    if (isset($entity->created)) {
338
      $event['created'] = new DateObject($entity->created, 'UTC');
339 340 341
    }
    // Pull the 'changed' date from the entity (if available), so that
    // subscription clients can tell if the event has been updated.
342
    if (isset($entity->changed)) {
343
      $event['last-modified'] = new DateObject($entity->changed, 'UTC');
344
    }
345
    else if (isset($entity->created)) {
346
      // If changed is unset, but created is, use that for last-modified.
347
      $event['last-modified'] = new DateObject($entity->created, 'UTC');
348
    }
Robert Rollins's avatar
Robert Rollins committed
349
    
350
    // Allow other modules to alter the structured event object, before it gets
351 352
    // passed to the style plugin to be converted into an iCalcreator vevent.
    drupal_alter('date_ical_export_raw_event', $event, $this->view, $context);
353
    
Robert Rollins's avatar
Robert Rollins committed
354
    return $event;
Karen Stevenson's avatar
Karen Stevenson committed
355 356
  }
}