date_ical_plugin_row_ical_entity.inc 15 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
/**
 * @file
 * Contains the iCal row style plugin.
 */

/**
9
 * A Views plugin which builds an iCal VEVENT from a single node.
Karen Stevenson's avatar
Karen Stevenson committed
10
 */
Robert Rollins's avatar
Robert Rollins committed
11
class date_ical_plugin_row_ical_entity extends views_plugin_row {
12
  
Karen Stevenson's avatar
Karen Stevenson committed
13
  // Basic properties that let the row style follow relationships.
14 15
  protected $base_table = 'node';
  protected $base_field = 'nid';
16
  
Karen Stevenson's avatar
Karen Stevenson committed
17
  // Stores the nodes loaded with pre_render.
18
  protected $entities = array();
19
  
20 21 22 23
  /**
   * Initialize the row plugin.
   */
  public function init(&$view, &$display, $options = NULL) {
Karen Stevenson's avatar
Karen Stevenson committed
24 25 26 27
    parent::init($view, $display, $options);
    $this->base_table = $view->base_table;
    $this->base_field = $view->base_field;
  }
28
  
29 30 31 32
  /**
   * Set up the options for the row plugin.
   */
  public function option_definition() {
Karen Stevenson's avatar
Karen Stevenson committed
33 34
    $options = parent::option_definition();
    $options['date_field'] = array('default' => array());
35
    $options['summary_field'] = array('default' => array());
36
    $options['location_field'] = array('default' => array());
Karen Stevenson's avatar
Karen Stevenson committed
37 38
    return $options;
  }
39
  
Karen Stevenson's avatar
Karen Stevenson committed
40
  /**
41
   * Build the form for setting the row plugin's options.
Karen Stevenson's avatar
Karen Stevenson committed
42
   */
43
  public function options_form(&$form, &$form_state) {
Karen Stevenson's avatar
Karen Stevenson committed
44
    parent::options_form($form, $form_state);
45
    
46 47
    // Build the select dropdown for the Date field that the user wants to use
    // to populate the date properties in VEVENTs.
Karen Stevenson's avatar
Karen Stevenson committed
48 49 50
    $data = date_views_fields($this->base_table);
    $options = array();
    foreach ($data['name'] as $item => $value) {
51 52
      // We only want to see one value for each field, so we need to
      // skip '_value2' and other columns.
Karen Stevenson's avatar
Karen Stevenson committed
53 54 55 56 57 58 59 60 61
      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
62 63
      '#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
64
      '#required' => TRUE,
Robert Rollins's avatar
Robert Rollins committed
65 66
    );
    $form['instructions'] = array(
67
      // The surrounding <div> ensures that the settings dialog expands.
Robert Rollins's avatar
Robert Rollins committed
68 69 70 71 72
      '#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
73
    );
74
    
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
    // 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.'),
    );
91
    
92 93 94
    // 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
95
    $location_options = array('none' => t('- None -'));
96 97 98 99 100 101 102 103 104 105 106 107
    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
108
  }
109
  
110 111 112
  /**
   * Preload the list of entities which will appear in the view.
   *
Robert Rollins's avatar
Robert Rollins committed
113 114
   * TODO: When the date is coming in through a relationship, the nid
   * of the view is not the right node to use: we need the related node.
115 116 117
   * Need to sort out how that should be handled.
   */
  public function pre_render($values) {
Karen Stevenson's avatar
Karen Stevenson committed
118 119 120 121 122 123
    // 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) {
124
      // Use the $id as the key so we create only value per entity.
Karen Stevenson's avatar
Karen Stevenson committed
125
      $id = $row->{$this->field_alias};
126
      
Karen Stevenson's avatar
Karen Stevenson committed
127 128 129 130 131 132 133 134 135 136
      // 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;
      }
    }
137
    
Karen Stevenson's avatar
Karen Stevenson committed
138 139 140 141 142
    $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);
    }
143
    
Karen Stevenson's avatar
Karen Stevenson committed
144 145 146 147 148 149 150
    // 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];
    }
  }
151
  
152 153 154 155
  /**
   * Renders the entities returned by the view into event arrays.
   */
  public function render($row) {
Karen Stevenson's avatar
Karen Stevenson committed
156 157
    $id = $row->{$this->field_alias};
    if (!is_numeric($id)) {
158
      return NULL;
Karen Stevenson's avatar
Karen Stevenson committed
159
    }
160
    
Karen Stevenson's avatar
Karen Stevenson committed
161 162 163
    // Load the specified entity:
    $entity = $this->entities[$id];
    if (empty($entity)) {
164 165
      // This can happen when an RRULE is involved.
      return NULL;
Karen Stevenson's avatar
Karen Stevenson committed
166
    }
167 168 169 170 171 172 173
    
    $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']);
    $delta_field = $date_info['delta_field'];
    $is_field    = $date_info['is_field'];
    
174
    // Sometimes the timestamp is actually the revision timestamp.
Karen Stevenson's avatar
Karen Stevenson committed
175 176 177
    if ($this->view->base_table == 'node_revision' && $field_name == 'timestamp') {
      $field_name = 'revision_timestamp';
    }
178
    
179 180
    if (!isset($entity->$field_name)) {
      // This entity doesn't have the date property that the user configured
181
      // our view to use. We can't do anything with it.
182
      return NULL;
183 184 185
    }
    $date_field = $entity->$field_name;
    
186
    // Pull the date value from the specified field of the entity.
Karen Stevenson's avatar
Karen Stevenson committed
187 188 189
    $entity->date_id = array();
    $start = NULL;
    $end   = NULL;
190
    $delta = isset($row->$delta_field) ? $row->$delta_field : 0;
Karen Stevenson's avatar
Karen Stevenson committed
191
    if ($is_field) {
192
      $items = field_get_items($this->entity_type, $entity, $field_name);
193
      if (!$items) {
194 195
        // 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.
196 197
        return;
      }
198 199 200 201
      $date_field = $items[$delta];
      $domain = check_plain($_SERVER['SERVER_NAME']);
      $entity->date_id[] = "calendar.$id.$field_name.$delta@$domain";
      
202 203
      if (!empty($date_field['value'])) {
        $start = new DateObject($date_field['value'], $date_field['timezone_db']);
204
        if (!empty($date_field['value2'])) {
205
          $end = new DateObject($date_field['value2'], $date_field['timezone_db']);
Karen Stevenson's avatar
Karen Stevenson committed
206 207 208 209 210 211
        }
        else {
          $end = clone $start;
        }
      }
    }
212 213 214
    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
215
    }
216
    
217
    // Set the item date to the proper display timezone.
218 219
    $start->setTimezone(new DateTimeZone($date_field['timezone']));
    $end->setTimezone(new DateTimeZone($date_field['timezone']));
220
    
221 222 223 224 225 226
    // 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'])
    );
227
    
Karen Stevenson's avatar
Karen Stevenson committed
228
    if ($all_day) {
229 230 231
      // 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
232 233
      $end->modify("+1 day");
    }
234 235 236 237
    
    // 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') {
238
      $location_fields = date_ical_get_location_fields($this->base_table);
239 240 241 242
      $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.
243 244
      $items = field_get_items($this->entity_type, $entity, $location_field_name);
      if ($items) {
245
        $location_field = $items[0];
246
        if ($location_info['type'] == 'node_reference') {
247 248 249 250
          // Make sure this Node Reference actually references a node.
          if ($location_field['nid']) {
            $node = node_load($location_field['nid']);
            $location = check_plain($node->title);
251 252
          }
        }
253 254
        elseif ($location_info['type'] == 'addressfield') {
          $locations = array();
255
          foreach ($location_field as $key => $loc) {
256 257 258 259 260 261
            if ($loc && !in_array($key, array('first_name', 'last_name'))) {
              $locations[] = $loc;
            }
          }
          $location = implode(', ', array_reverse($locations));
        }
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
        elseif ($location_info['type'] == 'location') {
          $included_fields = array(
            'name',
            'additional',
            'street',
            'city',
            'province_name',
            'postal_code',
            'country_name'
          );
          $location_data = array();
          foreach ($included_fields as $included_field) {
            if (!empty($location_field[$included_field])) {
              $locations_data[] = $location_field[$included_field];
            }
          }
          $location = check_plain(implode(', ', $location_data));
        }
280
        else {
281
          $location = check_plain($location_field['value']);
282 283 284 285
        }
      }
    }
    
286 287
    // Create the rendered event using the display settings from the
    // iCal view mode.
Karen Stevenson's avatar
Karen Stevenson committed
288
    $rendered_array = entity_view($this->entity_type, array($entity), 'ical', $this->language, TRUE);
289 290
    $data = array(
      'description' => drupal_render($rendered_array),
291
      'summary' => entity_label($this->entity_type, $entity),
292
    );
293 294 295 296 297 298 299 300
    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) {
301
        $summary_field = $items[0];
302 303 304 305
        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']);
306
            $summary = $node->title;
307 308 309 310
          }
        }
        elseif ($summary_info['type'] == 'taxonomy_term_reference') {
          $terms = taxonomy_term_load_multiple($items);
311
          // Make sure that there are terms that were loaded.
312
          if ($terms) {
313
            $term_names = array();
314
            foreach ($terms as $term) {
315
              $term_names[] = $term->name;
316
            }
317
            $summary = implode(', ', $term_names);
318 319 320
          }
        }
        else {
321
          $summary = trim($summary_field['value']);
322
        }
323
        $data['summary'] = $summary ? $summary : $data['summary'];
324 325
      }
    }
326 327 328 329 330 331 332 333
    // 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,
    );
334
    drupal_alter('date_ical_export_html', $data, $this->view, $context);
Robert Rollins's avatar
Robert Rollins committed
335
    
Karen Stevenson's avatar
Karen Stevenson committed
336
    $event = array();
337 338
    $event['summary'] = date_ical_sanitize_text($data['summary']);
    $event['description'] = date_ical_sanitize_text($data['description']);
Karen Stevenson's avatar
Karen Stevenson committed
339
    $event['all_day'] = $all_day;
Robert Rollins's avatar
Robert Rollins committed
340 341
    $event['start'] = $start;
    $event['end'] = $end;
342 343 344
    $uri = entity_uri($this->entity_type, $entity);
    $uri['options']['absolute'] = TRUE;
    $event['url'] = url($uri['path'], $uri['options']);
345
    $event['rrule'] = $is_field && array_key_exists('rrule', $date_field) ? $date_field['rrule'] : '';
346 347 348
    if ($location) {
      $event['location'] = $location;
    }
Robert Rollins's avatar
Robert Rollins committed
349
    
350 351
    // 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.
352
    $event['uid'] = !empty($entity->date_id) ? $entity->date_id[0] : $event['url'];
353
    
354 355 356
    // 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.
357 358 359
    if ($event['rrule']) {
      $this->entities[$id] = NULL;
    }
360
    
361 362
    // 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
363
    // to tell DateObject to treat the timestamp as UTC from the start.
364
    if (isset($entity->created)) {
365
      $event['created'] = new DateObject($entity->created, 'UTC');
366 367 368
    }
    // Pull the 'changed' date from the entity (if available), so that
    // subscription clients can tell if the event has been updated.
369
    if (isset($entity->changed)) {
370
      $event['last-modified'] = new DateObject($entity->changed, 'UTC');
371
    }
372
    elseif (isset($entity->created)) {
373
      // If changed is unset, but created is, use that for last-modified.
374
      $event['last-modified'] = new DateObject($entity->created, 'UTC');
375
    }
Robert Rollins's avatar
Robert Rollins committed
376
    
377
    // Allow other modules to alter the structured event object, before it gets
378 379
    // passed to the style plugin to be converted into an iCalcreator vevent.
    drupal_alter('date_ical_export_raw_event', $event, $this->view, $context);
380
    
Robert Rollins's avatar
Robert Rollins committed
381
    return $event;
Karen Stevenson's avatar
Karen Stevenson committed
382 383
  }
}