date_ical_plugin_row_ical_entity.inc 14.1 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
85
    // 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.'),
    );

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
212
    $start->setTimezone(new dateTimezone($date_field['timezone']));
    $end->setTimezone(new dateTimezone($date_field['timezone']));
    
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
307
    // 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,
    );
    drupal_alter('date_ical_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
    
Robert Rollins's avatar
Robert Rollins committed
334
335
    // Pull the 'changed' date from the entity, so that subscription clients can tell if the event has been updated.
    // According to the iCal standard, LAST-MODIFIED must be UTC. Fortunately, Drupal stores timestamps in the DB
336
    // as UTC, so we just need to specify that UTC be used rather than the server's local timezone.
337
338
339
    if (isset($entity->changed)) {
      $event['last-modified'] = new DateObject($entity->changed, new DateTimeZone('UTC'));
    }
Robert Rollins's avatar
Robert Rollins committed
340
    
341
342
    // Allow other modules to alter the structured event object, before it gets
    // passed to the style plugin to be converted into an iCal VEVENT.
Robert Rollins's avatar
Robert Rollins committed
343
    drupal_alter('date_ical_feed_event_render', $event, $this->view, $context);
344
    
Robert Rollins's avatar
Robert Rollins committed
345
    return $event;
Karen Stevenson's avatar
Karen Stevenson committed
346
347
  }
}