date_ical_plugin_row_ical_entity.inc 14.5 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
113
114
115
116
117
  /**
   * Preload the list of entities which will appear in the view.
   *
   * @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.
   */
  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
245
      $items = field_get_items($this->entity_type, $entity, $location_field_name);
      if ($items) {
        $location_field = $items[$delta];
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
        else {
263
          $location = check_plain($location_field['value']);
264
265
266
267
        }
      }
    }
    
268
269
    // Create the rendered event using the display settings from the
    // iCal view mode.
Karen Stevenson's avatar
Karen Stevenson committed
270
    $rendered_array = entity_view($this->entity_type, array($entity), 'ical', $this->language, TRUE);
271
272
    $data = array(
      'description' => drupal_render($rendered_array),
273
      'summary' => entity_label($this->entity_type, $entity),
274
    );
275
276
277
278
279
280
281
282
283
284
285
286
287
    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']);
288
            $summary = $node->title;
289
290
291
292
          }
        }
        elseif ($summary_info['type'] == 'taxonomy_term_reference') {
          $terms = taxonomy_term_load_multiple($items);
293
          // Make sure that there are terms that were loaded.
294
          if ($terms) {
295
            $term_names = array();
296
            foreach ($terms as $term) {
297
              $term_names[] = $term->name;
298
            }
299
            $summary = implode(', ', $term_names);
300
301
302
          }
        }
        else {
303
          $summary = trim($summary_field['value']);
304
        }
305
        $data['summary'] = $summary ? $summary : $data['summary'];
306
307
      }
    }
308
309
310
311
312
313
314
315
    // 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,
    );
316
    drupal_alter('date_ical_export_html', $data, $this->view, $context);
Robert Rollins's avatar
Robert Rollins committed
317
    
Karen Stevenson's avatar
Karen Stevenson committed
318
    $event = array();
319
320
    $event['summary'] = date_ical_sanitize_text($data['summary']);
    $event['description'] = date_ical_sanitize_text($data['description']);
Karen Stevenson's avatar
Karen Stevenson committed
321
    $event['all_day'] = $all_day;
Robert Rollins's avatar
Robert Rollins committed
322
323
    $event['start'] = $start;
    $event['end'] = $end;
324
325
326
    $uri = entity_uri($this->entity_type, $entity);
    $uri['options']['absolute'] = TRUE;
    $event['url'] = url($uri['path'], $uri['options']);
327
    $event['rrule'] = $is_field && array_key_exists('rrule', $date_field) ? $date_field['rrule'] : '';
328
329
330
    if ($location) {
      $event['location'] = $location;
    }
Robert Rollins's avatar
Robert Rollins committed
331
    
332
333
    // 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.
334
    $event['uid'] = !empty($entity->date_id) ? $entity->date_id[0] : $event['url'];
335
    
336
337
338
    // 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.
339
340
341
    if ($event['rrule']) {
      $this->entities[$id] = NULL;
    }
342
    
343
344
    // 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
345
    // to tell DateObject to treat the timestamp as UTC from the start.
346
    if (isset($entity->created)) {
347
      $event['created'] = new DateObject($entity->created, 'UTC');
348
349
350
    }
    // Pull the 'changed' date from the entity (if available), so that
    // subscription clients can tell if the event has been updated.
351
    if (isset($entity->changed)) {
352
      $event['last-modified'] = new DateObject($entity->changed, 'UTC');
353
    }
354
    elseif (isset($entity->created)) {
355
      // If changed is unset, but created is, use that for last-modified.
356
      $event['last-modified'] = new DateObject($entity->created, 'UTC');
357
    }
Robert Rollins's avatar
Robert Rollins committed
358
    
359
    // Allow other modules to alter the structured event object, before it gets
360
361
    // passed to the style plugin to be converted into an iCalcreator vevent.
    drupal_alter('date_ical_export_raw_event', $event, $this->view, $context);
362
    
Robert Rollins's avatar
Robert Rollins committed
363
    return $event;
Karen Stevenson's avatar
Karen Stevenson committed
364
365
  }
}