diff --git a/core/modules/aggregator/aggregator.views.inc b/core/modules/aggregator/aggregator.views.inc new file mode 100644 index 0000000..2e12893 --- /dev/null +++ b/core/modules/aggregator/aggregator.views.inc @@ -0,0 +1,363 @@ + 'iid', + 'title' => t('Aggregator item'), + 'help' => t("Aggregator items are imported from external RSS and Atom news feeds."), + ); + + // ---------------------------------------------------------------- + // Fields + + // iid + $data['aggregator_item']['iid'] = array( + 'title' => t('Item ID'), + 'help' => t('The unique ID of the aggregator item.'), // The help that appears on the UI, + // Information for displaying the iid + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + // Information for accepting a iid as an argument + 'argument' => array( + 'id' => 'aggregator_iid', + 'name field' => 'title', // the field to display in the summary. + 'numeric' => TRUE, + ), + // Information for accepting a nid as a filter + 'filter' => array( + 'id' => 'numeric', + ), + // Information for sorting on a nid. + 'sort' => array( + 'id' => 'standard', + ), + ); + + // title + $data['aggregator_item']['title'] = array( + 'title' => t('Title'), // The item it appears as on the UI, + 'help' => t('The title of the aggregator item.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'aggregator_title_link', + 'extra' => array('link'), + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'string', + ), + ); + + // link + $data['aggregator_item']['link'] = array( + 'title' => t('Link'), // The item it appears as on the UI, + 'help' => t('The link to the original source URL of the item.'), + 'field' => array( + 'id' => 'url', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'string', + ), + ); + + // author + $data['aggregator_item']['author'] = array( + 'title' => t('Author'), // The item it appears as on the UI, + 'help' => t('The author of the original imported item.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'aggregator_xss', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // guid + $data['aggregator_item']['guid'] = array( + 'title' => t('GUID'), // The item it appears as on the UI, + 'help' => t('The guid of the original imported item.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'xss', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // feed body + $data['aggregator_item']['description'] = array( + 'title' => t('Body'), // The item it appears as on the UI, + 'help' => t('The actual content of the imported item.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'aggregator_xss', + 'click sortable' => FALSE, + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'string', + ), + ); + + // item timestamp + $data['aggregator_item']['timestamp'] = array( + 'title' => t('Timestamp'), // The item it appears as on the UI, + 'help' => t('The date the original feed item was posted. (With some feeds, this will be the date it was imported.)'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'date', + ), + 'argument' => array( + 'id' => 'date', + ), + ); + + // ---------------------------------------------------------------------- + // Aggregator feed table + + $data['aggregator_feed']['table']['group'] = t('Aggregator feed'); + + // Explain how this table joins to others. + $data['aggregator_feed']['table']['join'] = array( + 'aggregator_item' => array( + 'left_field' => 'fid', + 'field' => 'fid', + ), + ); + + // fid + $data['aggregator_feed']['fid'] = array( + 'title' => t('Feed ID'), + 'help' => t('The unique ID of the aggregator feed.'), // The help that appears on the UI, + // Information for displaying the fid + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + // Information for accepting a fid as an argument + 'argument' => array( + 'id' => 'aggregator_fid', + 'name field' => 'title', // the field to display in the summary. + 'numeric' => TRUE, + ), + // Information for accepting a nid as a filter + 'filter' => array( + 'id' => 'numeric', + ), + // Information for sorting on a fid. + 'sort' => array( + 'id' => 'standard', + ), + ); + + // title + $data['aggregator_feed']['title'] = array( + 'title' => t('Title'), // The item it appears as on the UI, + 'help' => t('The title of the aggregator feed.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'aggregator_title_link', + 'extra' => array('link'), + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // link + $data['aggregator_feed']['link'] = array( + 'title' => t('Link'), // The item it appears as on the UI, + 'help' => t('The link to the source URL of the feed.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'url', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + // feed last updated + $data['aggregator_feed']['checked'] = array( + 'title' => t('Last checked'), // The item it appears as on the UI, + 'help' => t('The date the feed was last checked for new content.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date', + ), + 'filter' => array( + 'id' => 'date', + ), + 'argument' => array( + 'id' => 'date', + ), + ); + + // feed description + $data['aggregator_feed']['description'] = array( + 'title' => t('Description'), // The item it appears as on the UI, + 'help' => t('The description of the aggregator feed.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'xss', + 'click sortable' => FALSE, + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + // feed last updated + $data['aggregator_feed']['modified'] = array( + 'title' => t('Last modified'), // The item it appears as on the UI, + 'help' => t('The date of the most recent new content on the feed.'), + // Information for displaying a title as a field + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'date', + ), + 'argument' => array( + 'id' => 'date', + ), + ); + + // ---------------------------------------------------------------------- + // Aggregator category feed table + + $data['aggregator_category_feed']['table']['join'] = array( + 'aggregator_item' => array( + 'left_field' => 'fid', + 'field' => 'fid', + ), + ); + + // ---------------------------------------------------------------------- + // Aggregator category table + + $data['aggregator_category']['table']['group'] = t('Aggregator category'); + + $data['aggregator_category']['table']['join'] = array( + 'aggregator_item' => array( + 'left_table' => 'aggregator_category_feed', + 'left_field' => 'cid', + 'field' => 'cid', + ), + ); + + // cid + $data['aggregator_category']['cid'] = array( + 'title' => t('Category ID'), + 'help' => t('The unique ID of the aggregator category.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'aggregator_category_cid', + 'name field' => 'title', + 'numeric' => TRUE, + ), + 'filter' => array( + 'id' => 'aggregator_category_cid', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // title + $data['aggregator_category']['title'] = array( + 'title' => t('Category'), + 'help' => t('The title of the aggregator category.'), + 'field' => array( + 'id' => 'aggregator_category', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + return $data; +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/CategoryCid.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/CategoryCid.php new file mode 100644 index 0000000..8454377 --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/CategoryCid.php @@ -0,0 +1,41 @@ +addField('c', 'title'); + $query->condition('c.cid', $this->value); + $result = $query->execute(); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } + +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/Fid.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/Fid.php new file mode 100644 index 0000000..2c4aaf4 --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/Fid.php @@ -0,0 +1,42 @@ + $this->value)); + $query = db_select('aggregator_feed', 'f'); + $query->addField('f', 'title'); + $query->condition('f.fid', $this->value); + $result = $query->execute(); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } + +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/Iid.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/Iid.php new file mode 100644 index 0000000..b557e1d --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/argument/Iid.php @@ -0,0 +1,42 @@ +value), '%d')); + + $result = db_select('aggregator_item') + ->condition('iid', $this->value, 'IN') + ->fields(array('title')) + ->execute(); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } + +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/Category.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/Category.php new file mode 100644 index 0000000..9cd4b50 --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/Category.php @@ -0,0 +1,74 @@ +additional_fields['cid'] = 'cid'; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_to_category'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to category option + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_category'] = array( + '#title' => t('Link this field to its aggregator category page'), + '#description' => t('This will override any other link you have set.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_category']), + ); + parent::buildOptionsForm($form, $form_state); + } + + /** + * Render whatever the data is as a link to the category. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + $cid = $this->get_value($values, 'cid'); + if (!empty($this->options['link_to_category']) && !empty($cid) && $data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "aggregator/category/$cid"; + } + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/TitleLink.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/TitleLink.php new file mode 100644 index 0000000..94a336d --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/TitleLink.php @@ -0,0 +1,72 @@ +additional_fields['link'] = 'link'; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['display_as_link'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide link to the page being visited. + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['display_as_link'] = array( + '#title' => t('Display as link'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['display_as_link']), + ); + parent::buildOptionsForm($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + + function render_link($data, $values) { + $link = $this->get_value($values, 'link'); + if (!empty($this->options['display_as_link'])) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = $link; + $this->options['alter']['html'] = TRUE; + } + + return $data; + } + +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/Xss.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/Xss.php new file mode 100644 index 0000000..017d77f --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/field/Xss.php @@ -0,0 +1,30 @@ +get_value($values); + return aggregator_filter_xss($value); + } + +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/filter/CategoryCid.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/filter/CategoryCid.php new file mode 100644 index 0000000..c256ed9 --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/filter/CategoryCid.php @@ -0,0 +1,39 @@ +value_options)) { + return; + } + + $this->value_options = array(); + // Uses db_query() rather than db_select() because the query is static and + // does not include any variables. + $result = db_query('SELECT * FROM {aggregator_category} ORDER BY title'); + foreach ($result as $category) { + $this->value_options[$category->cid] = $category->title; + } + } + +} diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/row/Rss.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/row/Rss.php new file mode 100644 index 0000000..bb46b33 --- /dev/null +++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/views/row/Rss.php @@ -0,0 +1,92 @@ + 'default'); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['item_length'] = array( + '#type' => 'select', + '#title' => t('Display type'), + '#options' => array( + 'fulltext' => t('Full text'), + 'teaser' => t('Title plus teaser'), + 'title' => t('Title only'), + 'default' => t('Use default RSS settings'), + ), + '#default_value' => $this->options['item_length'], + ); + } + + function render($row) { + $iid = $row->{$this->field_alias}; + $query = db_select('aggregator_item', 'ai'); + $query->leftJoin('aggregator_feed', 'af', 'ai.fid = af.fid'); + $query->fields('ai'); + $query->addExpression('af.title', 'feed_title'); + $query->addExpression('ai.link', 'feed_LINK'); + $query->condition('iid', $iid); + $result = $query->execute(); + + $item->elements = array( + array( + 'key' => 'pubDate', + 'value' => gmdate('r', $item->timestamp), + ), + array( + 'key' => 'dc:creator', + 'value' => $item->author, + ), + array( + 'key' => 'guid', + 'value' => $item->guid, + 'attributes' => array('isPermaLink' => 'false') + ), + ); + + foreach ($item->elements as $element) { + if (isset($element['namespace'])) { + $this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $element['namespace']); + } + } + + return theme($this->themeFunctions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $item + )); + } + +} diff --git a/core/modules/block/lib/Drupal/block/Plugin/views/display/Block.php b/core/modules/block/lib/Drupal/block/Plugin/views/display/Block.php new file mode 100644 index 0000000..cb82081 --- /dev/null +++ b/core/modules/block/lib/Drupal/block/Plugin/views/display/Block.php @@ -0,0 +1,270 @@ + '', 'translatable' => TRUE); + $options['block_caching'] = array('default' => DRUPAL_NO_CACHE); + + return $options; + } + + /** + * The default block handler doesn't support configurable items, + * but extended block handlers might be able to do interesting + * stuff with it. + */ + public function executeHookBlockList($delta = 0, $edit = array()) { + $delta = $this->view->storage->name . '-' . $this->display['id']; + $desc = $this->getOption('block_description'); + + if (empty($desc)) { + if ($this->display['display_title'] == $this->definition['title']) { + $desc = t('View: !view', array('!view' => $this->view->storage->getHumanName())); + } + else { + $desc = t('View: !view: !display', array('!view' => $this->view->storage->getHumanName(), '!display' => $this->display['display_title'])); + } + } + return array( + $delta => array( + 'info' => $desc, + 'cache' => $this->getCacheType() + ), + ); + } + + /** + * The display block handler returns the structure necessary for a block. + */ + public function execute() { + // Prior to this being called, the $view should already be set to this + // display, and arguments should be set on the view. + $info['content'] = $this->view->render(); + $info['subject'] = filter_xss_admin($this->view->getTitle()); + if (!empty($this->view->result) || $this->getOption('empty') || !empty($this->view->style_plugin->definition['even empty'])) { + return $info; + } + } + + /** + * Provide the summary for page options in the views UI. + * + * This output is returned as an array. + */ + public function optionsSummary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::optionsSummary($categories, $options); + + $categories['block'] = array( + 'title' => t('Block settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + $block_description = strip_tags($this->getOption('block_description')); + if (empty($block_description)) { + $block_description = t('None'); + } + + $options['block_description'] = array( + 'category' => 'block', + 'title' => t('Block name'), + 'value' => views_ui_truncate($block_description, 24), + ); + + $types = $this->blockCachingModes(); + $options['block_caching'] = array( + 'category' => 'other', + 'title' => t('Block caching'), + 'value' => $types[$this->getCacheType()], + ); + } + + /** + * Provide a list of core's block caching modes. + */ + protected function blockCachingModes() { + return array( + DRUPAL_NO_CACHE => t('Do not cache'), + DRUPAL_CACHE_GLOBAL => t('Cache once for everything (global)'), + DRUPAL_CACHE_PER_PAGE => t('Per page'), + DRUPAL_CACHE_PER_ROLE => t('Per role'), + DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE => t('Per role per page'), + DRUPAL_CACHE_PER_USER => t('Per user'), + DRUPAL_CACHE_PER_USER | DRUPAL_CACHE_PER_PAGE => t('Per user per page'), + ); + } + + /** + * Provide a single method to figure caching type, keeping a sensible default + * for when it's unset. + */ + protected function getCacheType() { + $cache_type = $this->getOption('block_caching'); + if (empty($cache_type)) { + $cache_type = DRUPAL_NO_CACHE; + } + return $cache_type; + } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::buildOptionsForm($form, $form_state); + + switch ($form_state['section']) { + case 'block_description': + $form['#title'] .= t('Block admin description'); + $form['block_description'] = array( + '#type' => 'textfield', + '#description' => t('This will appear as the name of this block in administer >> structure >> blocks.'), + '#default_value' => $this->getOption('block_description'), + ); + break; + case 'block_caching': + $form['#title'] .= t('Block caching type'); + + $form['block_caching'] = array( + '#type' => 'radios', + '#description' => t("This sets the default status for Drupal's built-in block caching method; this requires that caching be turned on in block administration, and be careful because you have little control over when this cache is flushed."), + '#options' => $this->blockCachingModes(), + '#default_value' => $this->getCacheType(), + ); + break; + case 'exposed_form_options': + $this->view->initHandlers(); + if (!$this->usesExposed() && parent::usesExposed()) { + $form['exposed_form_options']['warning'] = array( + '#weight' => -10, + '#markup' => '
' . t('Exposed filters in block displays require "Use AJAX" to be set to work correctly.') . '
', + ); + } + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::submitOptionsForm($form, $form_state); + switch ($form_state['section']) { + case 'display_id': + $this->updateBlockBid($form_state['view']->storage->name, $this->display['id'], $this->display['new_id']); + break; + case 'block_description': + $this->setOption('block_description', $form_state['values']['block_description']); + break; + case 'block_caching': + $this->setOption('block_caching', $form_state['values']['block_caching']); + $this->saveBlockCache($form_state['view']->storage->name . '-'. $form_state['display_id'], $form_state['values']['block_caching']); + break; + } + } + + /** + * Block views use exposed widgets only if AJAX is set. + */ + public function usesExposed() { + if ($this->isAJAXEnabled()) { + return parent::usesExposed(); + } + return FALSE; + } + + /** + * Update the block delta when you change the machine readable name of the display. + */ + protected function updateBlockBid($name, $old_delta, $delta) { + $old_hashes = $hashes = variable_get('views_block_hashes', array()); + + $old_delta = $name . '-' . $old_delta; + $delta = $name . '-' . $delta; + if (strlen($old_delta) >= 32) { + $old_delta = md5($old_delta); + unset($hashes[$old_delta]); + } + if (strlen($delta) >= 32) { + $md5_delta = md5($delta); + $hashes[$md5_delta] = $delta; + $delta = $md5_delta; + } + + // Maybe people don't have block module installed, so let's skip this. + if (db_table_exists('block')) { + db_update('block') + ->fields(array('delta' => $delta)) + ->condition('delta', $old_delta) + ->execute(); + } + + // Update the hashes if needed. + if ($hashes != $old_hashes) { + variable_set('views_block_hashes', $hashes); + } + } + + /** + * Save the block cache setting in the blocks table if this block allready + * exists in the blocks table. Dirty fix untill http://drupal.org/node/235673 gets in. + */ + protected function saveBlockCache($delta, $cache_setting) { + if (strlen($delta) >= 32) { + $delta = md5($delta); + } + if (db_table_exists('block') && $bid = db_query("SELECT bid FROM {block} WHERE module = 'views' AND delta = :delta", array( + ':delta' => $delta))->fetchField()) { + db_update('block') + ->fields(array( + 'cache' => $cache_setting, + )) + ->condition('module', 'views') + ->condition('delta', $delta) + ->execute(); + } + } + +} diff --git a/core/modules/book/book.views.inc b/core/modules/book/book.views.inc new file mode 100644 index 0000000..731a495 --- /dev/null +++ b/core/modules/book/book.views.inc @@ -0,0 +1,116 @@ + array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + $data['book']['bid'] = array( + 'title' => t('Top level book'), + 'help' => t('The book the node is in.'), + 'relationship' => array( + 'base' => 'node', + 'id' => 'standard', + 'label' => t('Book'), + ), + // There is no argument here; if you need an argument, add the relationship + // and use the node: nid argument. + ); + + // ---------------------------------------------------------------------- + // menu_links table -- this is aliased so we can get just book relations + + // Book hierarchy and weight data are now in {menu_links}. + $data['book_menu_links']['table']['group'] = t('Book'); + $data['book_menu_links']['table']['join'] = array( + 'node' => array( + 'table' => 'menu_links', + 'left_table' => 'book', + 'left_field' => 'mlid', + 'field' => 'mlid', + ), + ); + + $data['book_menu_links']['weight'] = array( + 'title' => t('Weight'), + 'help' => t('The weight of the book page.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + $data['book_menu_links']['depth'] = array( + 'title' => t('Depth'), + 'help' => t('The depth of the book page in the hierarchy; top level books have a depth of 1.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'argument' => array( + 'id' => 'standard', + ), + ); + + $data['book_menu_links']['p'] = array( + 'title' => t('Hierarchy'), + 'help' => t('The order of pages in the book hierarchy.'), + 'sort' => array( + 'id' => 'menu_hierarchy', + ), + ); + + // ---------------------------------------------------------------------- + // book_parent table -- this is an alias of the book table which + // represents the parent book. + + // The {book} record for the parent node. + $data['book_parent']['table']['group'] = t('Book'); + $data['book_parent']['table']['join'] = array( + 'node' => array( + 'table' => 'book', + 'left_table' => 'book_menu_links', + 'left_field' => 'plid', + 'field' => 'mlid', + ), + ); + + $data['book_parent']['nid'] = array( + 'title' => t('Parent'), + 'help' => t('The parent book node.'), + 'relationship' => array( + 'base' => 'node', + 'base field' => 'nid', + 'id' => 'standard', + 'label' => t('Book parent'), + ), + ); + + return $data; +} diff --git a/core/modules/book/lib/Drupal/book/Plugin/views/argument_default/Root.php b/core/modules/book/lib/Drupal/book/Plugin/views/argument_default/Root.php new file mode 100644 index 0000000..d99f91a --- /dev/null +++ b/core/modules/book/lib/Drupal/book/Plugin/views/argument_default/Root.php @@ -0,0 +1,36 @@ +book['bid'])) { + return $node->book['bid']; + } + } + } + +} diff --git a/core/modules/comment/comment.views.inc b/core/modules/comment/comment.views.inc new file mode 100644 index 0000000..acf1d63 --- /dev/null +++ b/core/modules/comment/comment.views.inc @@ -0,0 +1,608 @@ + 'cid', + 'title' => t('Comment'), + 'help' => t("Comments are responses to node content."), + 'access query tag' => 'comment_access', + ); + $data['comment']['table']['entity type'] = 'comment'; + + // Fields + + // subject + $data['comment']['subject'] = array( + 'title' => t('Title'), + 'help' => t('The title of the comment.'), + 'field' => array( + 'id' => 'comment', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // cid + $data['comment']['cid'] = array( + 'title' => t('ID'), + 'help' => t('The comment ID of the field'), + 'field' => array( + 'id' => 'comment', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'numeric', + ), + ); + + // name (of comment author) + $data['comment']['name'] = array( + 'title' => t('Author'), + 'help' => t("The name of the comment's author. Can be rendered as a link to the author's homepage."), + 'field' => array( + 'id' => 'comment_username', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // homepage + $data['comment']['homepage'] = array( + 'title' => t("Author's website"), + 'help' => t("The website address of the comment's author. Can be rendered as a link. Will be empty if the author is a registered user."), + 'field' => array( + 'id' => 'url', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // hostname + $data['comment']['hostname'] = array( + 'title' => t('Hostname'), + 'help' => t('Hostname of user that posted the comment.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // mail + $data['comment']['mail'] = array( + 'title' => t('Mail'), + 'help' => t('Email of user that posted the comment. Will be empty if the author is a registered user.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // created (when comment was posted) + $data['comment']['created'] = array( + 'title' => t('Post date'), + 'help' => t('Date and time of when the comment was created.'), + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date', + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + // Langcode field + if (module_exists('language')) { + $data['comment']['language']['moved to'] = array('comment', 'langcode'); + $data['comment']['langcode'] = array( + 'title' => t('Language'), + 'help' => t('The language the comment is in.'), + 'field' => array( + 'id' => 'language', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'language', + ), + 'argument' => array( + 'id' => 'language', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + } + + // changed (when comment was last updated) + $data['comment']['changed'] = array( + 'title' => t('Updated date'), + 'help' => t('Date and time of when the comment was last updated.'), + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date', + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + $data['comment']['changed_fulldata'] = array( + 'title' => t('Created date'), + 'help' => t('Date in the form of CCYYMMDD.'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_fulldate', + ), + ); + + $data['comment']['changed_year_month'] = array( + 'title' => t('Created year + month'), + 'help' => t('Date in the form of YYYYMM.'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_year_month', + ), + ); + + $data['comment']['changed_year'] = array( + 'title' => t('Created year'), + 'help' => t('Date in the form of YYYY.'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_year', + ), + ); + + $data['comment']['changed_month'] = array( + 'title' => t('Created month'), + 'help' => t('Date in the form of MM (01 - 12).'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_month', + ), + ); + + $data['comment']['changed_day'] = array( + 'title' => t('Created day'), + 'help' => t('Date in the form of DD (01 - 31).'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_day', + ), + ); + + $data['comment']['changed_week'] = array( + 'title' => t('Created week'), + 'help' => t('Date in the form of WW (01 - 53).'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_week', + ), + ); + + // status (approved or not) + $data['comment']['status'] = array( + 'title' => t('Approved'), + 'help' => t('Whether the comment is approved (or still in the moderation queue).'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'approved-not-approved' => array(t('Approved'), t('Not Approved')), + ), + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Approved comment'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // link to view comment + $data['comment']['view_comment'] = array( + 'field' => array( + 'title' => t('View link'), + 'help' => t('Provide a simple link to view the comment.'), + 'id' => 'comment_link', + ), + ); + + // link to edit comment + $data['comment']['edit_comment'] = array( + 'field' => array( + 'title' => t('Edit link'), + 'help' => t('Provide a simple link to edit the comment.'), + 'id' => 'comment_link_edit', + ), + ); + + // link to delete comment + $data['comment']['delete_comment'] = array( + 'field' => array( + 'title' => t('Delete link'), + 'help' => t('Provide a simple link to delete the comment.'), + 'id' => 'comment_link_delete', + ), + ); + + // link to approve comment + $data['comment']['approve_comment'] = array( + 'field' => array( + 'title' => t('Approve link'), + 'help' => t('Provide a simple link to approve the comment.'), + 'id' => 'comment_link_approve', + ), + ); + + // link to reply to comment + $data['comment']['replyto_comment'] = array( + 'field' => array( + 'title' => t('Reply-to link'), + 'help' => t('Provide a simple link to reply to the comment.'), + 'id' => 'comment_link_reply', + ), + ); + + $data['comment']['thread'] = array( + 'field' => array( + 'title' => t('Depth'), + 'help' => t('Display the depth of the comment if it is threaded.'), + 'id' => 'comment_depth', + ), + 'sort' => array( + 'title' => t('Thread'), + 'help' => t('Sort by the threaded order. This will keep child comments together with their parents.'), + 'id' => 'comment_thread', + ), + ); + + $data['comment']['nid'] = array( + 'title' => t('Nid'), + 'help' => t('The node ID to which the comment is a reply to.'), + 'relationship' => array( + 'title' => t('Content'), + 'help' => t('The content to which the comment is a reply to.'), + 'base' => 'node', + 'base field' => 'nid', + 'id' => 'standard', + 'label' => t('Content'), + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'field' => array( + 'id' => 'numeric', + ), + ); + + $data['comment']['uid'] = array( + 'title' => t('Author uid'), + 'help' => t('If you need more fields than the uid add the comment: author relationship'), + 'relationship' => array( + 'title' => t('Author'), + 'help' => t("The User ID of the comment's author."), + 'base' => 'users', + 'base field' => 'uid', + 'id' => 'standard', + 'label' => t('author'), + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'field' => array( + 'id' => 'user', + ), + ); + + $data['comment']['pid'] = array( + 'title' => t('Parent CID'), + 'help' => t('The Comment ID of the parent comment.'), + 'field' => array( + 'id' => 'standard', + ), + 'relationship' => array( + 'title' => t('Parent comment'), + 'help' => t('The parent comment.'), + 'base' => 'comment', + 'base field' => 'cid', + 'id' => 'standard', + 'label' => t('Parent comment'), + ), + ); + + // ---------------------------------------------------------------------- + // node_comment_statistics table + + // define the group + $data['node_comment_statistics']['table']['group'] = t('Content'); + + // joins + $data['node_comment_statistics']['table']['join'] = array( + //...to the node table + 'node' => array( + 'type' => 'INNER', + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + // last_comment_timestamp + $data['node_comment_statistics']['last_comment_timestamp'] = array( + 'title' => t('Last comment time'), + 'help' => t('Date and time of when the last comment was posted.'), + 'field' => array( + 'id' => 'comment_last_timestamp', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date', + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + // last_comment_name (author's name) + $data['node_comment_statistics']['last_comment_name'] = array( + 'title' => t("Last comment author"), + 'help' => t('The name of the author of the last posted comment.'), + 'field' => array( + 'id' => 'comment_ncs_last_comment_name', + 'click sortable' => TRUE, + 'no group by' => TRUE, + ), + 'sort' => array( + 'id' => 'comment_ncs_last_comment_name', + 'no group by' => TRUE, + ), + ); + + // comment_count + $data['node_comment_statistics']['comment_count'] = array( + 'title' => t('Comment count'), + 'help' => t('The number of comments a node has.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'standard', + ), + ); + + // last_comment_timestamp + $data['node_comment_statistics']['last_updated'] = array( + 'title' => t('Updated/commented date'), + 'help' => t('The most recent of last comment posted or node updated time.'), + 'field' => array( + 'id' => 'comment_ncs_last_updated', + 'click sortable' => TRUE, + 'no group by' => TRUE, + ), + 'sort' => array( + 'id' => 'comment_ncs_last_updated', + 'no group by' => TRUE, + ), + 'filter' => array( + 'id' => 'comment_ncs_last_updated', + ), + ); + + $data['node_comment_statistics']['cid'] = array( + 'title' => t('Last comment CID'), + 'help' => t('Display the last comment of a node'), + 'relationship' => array( + 'title' => t('Last Comment'), + 'help' => t('The last comment of a node.'), + 'group' => t('Comment'), + 'base' => 'comment', + 'base field' => 'cid', + 'id' => 'standard', + 'label' => t('Last Comment'), + ), + ); + + // last_comment_uid + $data['node_comment_statistics']['last_comment_uid'] = array( + 'title' => t('Last comment uid'), + 'help' => t('The User ID of the author of the last comment of a node.'), + 'relationship' => array( + 'title' => t('Last comment author'), + 'base' => 'users', + 'base field' => 'uid', + 'id' => 'standard', + 'label' => t('Last comment author'), + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'field' => array( + 'id' => 'numeric', + ), + ); + + return $data; +} + +/** + * Use views_data_alter to add items to the node table that are + * relevant to comments. + */ +function comment_views_data_alter(&$data) { + // new comments + $data['node']['new_comments'] = array( + 'title' => t('New comments'), + 'help' => t('The number of new comments on the node.'), + 'field' => array( + 'id' => 'node_new_comments', + 'no group by' => TRUE, + ), + ); + + $data['node']['comments_link'] = array( + 'field' => array( + 'title' => t('Add comment link'), + 'help' => t('Display the standard add comment link used on regular nodes, which will only display if the viewing user has access to add a comment.'), + 'id' => 'comment_node_link', + ), + ); + + // Comment status of the node + $data['node']['comment'] = array( + 'title' => t('Comment status'), + 'help' => t('Whether comments are enabled or disabled on the node.'), + 'field' => array( + 'id' => 'node_comment', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'node_comment', + ), + ); + + $data['node']['uid_touch'] = array( + 'title' => t('User posted or commented'), + 'help' => t('Display nodes only if a user posted the node or commented on the node.'), + 'argument' => array( + 'field' => 'uid', + 'name table' => 'users', + 'name field' => 'name', + 'id' => 'argument_comment_user_uid', + 'no group by' => TRUE, + ), + 'filter' => array( + 'field' => 'uid', + 'name table' => 'users', + 'name field' => 'name', + 'id' => 'comment_user_uid', + ), + ); + + $data['node']['cid'] = array( + 'title' => t('Comments of the node'), + 'help' => t('Relate all comments on the node. This will create 1 duplicate record for every comment. Usually if you need this it is better to create a comment view.'), + 'relationship' => array( + 'group' => t('Comment'), + 'label' => t('Comments'), + 'base' => 'comment', + 'base field' => 'nid', + 'relationship field' => 'nid', + 'id' => 'standard', + ), + ); + +} + +/** + * Template helper for theme_views_view_row_comment + */ +function template_preprocess_views_view_row_comment(&$vars) { + $options = $vars['options']; + $view = &$vars['view']; + $plugin = &$view->style_plugin->row_plugin; + $comment = $plugin->comments[$vars['row']->{$vars['field_alias']}]; + $node = $plugin->nodes[$comment->nid]; + // Put the view on the node so we can retrieve it in the preprocess. + $node->view = &$view; + + $build = comment_view_multiple(array($comment->id() => $comment), $node, $plugin->options['view_mode']); + // If we're displaying the comments without links, remove them from the + // renderable array. There is no way to avoid building them in the first + // place (see comment_build_content()). + if (empty($options['links'])) { + foreach ($build as $cid => &$comment_build) { + if (isset($comment_build['links'])) { + unset($comment_build['links']); + } + } + } + $vars['comment'] = drupal_render($build); +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/argument/UserUid.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/argument/UserUid.php new file mode 100644 index 0000000..521e45a --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/argument/UserUid.php @@ -0,0 +1,76 @@ +argument) { + $title = config('user.settings')->get('anonymous'); + } + else { + $query = db_select('users', 'u'); + $query->addField('u', 'name'); + $query->condition('u.uid', $this->argument); + $title = $query->execute()->fetchField(); + } + if (empty($title)) { + return t('No user'); + } + + return check_plain($title); + } + + function default_actions($which = NULL) { + // Disallow summary views on this argument. + if (!$which) { + $actions = parent::default_actions(); + unset($actions['summary asc']); + unset($actions['summary desc']); + return $actions; + } + + if ($which != 'summary asc' && $which != 'summary desc') { + return parent::default_actions($which); + } + } + + public function query($group_by = FALSE) { + $this->ensureMyTable(); + + $subselect = db_select('comment', 'c'); + $subselect->addField('c', 'cid'); + $subselect->condition('c.uid', $this->argument); + $subselect->where("c.nid = $this->tableAlias.nid"); + + $condition = db_or() + ->condition("$this->tableAlias.uid", $this->argument, '=') + ->exists($subselect); + + $this->query->add_where(0, $condition); + } + + function get_sort_name() { + return t('Numerical', array(), array('context' => 'Sort order')); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Comment.php new file mode 100644 index 0000000..eadc0b5 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Comment.php @@ -0,0 +1,86 @@ +options['link_to_comment'])) { + $this->additional_fields['cid'] = 'cid'; + $this->additional_fields['nid'] = 'nid'; + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_to_comment'] = array('default' => TRUE, 'bool' => TRUE); + $options['link_to_node'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide link-to-comment option + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_comment'] = array( + '#title' => t('Link this field to its comment'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_comment'], + ); + $form['link_to_node'] = array( + '#title' => t('Link field to the node if there is no comment.'), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_node'], + ); + parent::buildOptionsForm($form, $form_state); + } + + function render_link($data, $values) { + if (!empty($this->options['link_to_comment'])) { + $this->options['alter']['make_link'] = TRUE; + $nid = $this->get_value($values, 'nid'); + $cid = $this->get_value($values, 'cid'); + if (!empty($cid)) { + $this->options['alter']['path'] = "comment/" . $cid; + $this->options['alter']['fragment'] = "comment-" . $cid; + } + // If there is no comment link to the node. + elseif ($this->options['link_to_node']) { + $this->options['alter']['path'] = "node/" . $nid; + } + } + + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Depth.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Depth.php new file mode 100644 index 0000000..1713bd7 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Depth.php @@ -0,0 +1,33 @@ +get_value($values); + return count(explode('.', $comment_thread)) - 1; + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LastTimestamp.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LastTimestamp.php new file mode 100644 index 0000000..1f14803 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LastTimestamp.php @@ -0,0 +1,45 @@ +additional_fields['comment_count'] = 'comment_count'; + } + + function render($values) { + $comment_count = $this->get_value($values, 'comment_count'); + if (empty($this->options['empty_zero']) || $comment_count) { + return parent::render($values); + } + else { + return NULL; + } + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Link.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Link.php new file mode 100644 index 0000000..d11e7a5 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Link.php @@ -0,0 +1,74 @@ + '', 'translatable' => TRUE); + $options['link_to_node'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + $form['link_to_node'] = array( + '#title' => t('Link field to the node if there is no comment.'), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_node'], + ); + parent::buildOptionsForm($form, $form_state); + } + + public function query() {} + + function render($values) { + $comment = $this->get_entity($values); + return $this->render_link($comment, $values); + } + + function render_link($data, $values) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('view'); + $comment = $data; + $nid = $comment->nid; + $cid = $comment->id(); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['html'] = TRUE; + + if (!empty($cid)) { + $this->options['alter']['path'] = "comment/" . $cid; + $this->options['alter']['fragment'] = "comment-" . $cid; + } + // If there is no comment link to the node. + elseif ($this->options['link_to_node']) { + $this->options['alter']['path'] = "node/" . $nid; + } + + return $text; + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkApprove.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkApprove.php new file mode 100644 index 0000000..5eb2195 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkApprove.php @@ -0,0 +1,47 @@ +get_value($values, 'status'); + + // Don't show an approve link on published nodes. + if ($status == COMMENT_PUBLISHED) { + return; + } + + $text = !empty($this->options['text']) ? $this->options['text'] : t('approve'); + $cid = $this->get_value($values, 'cid'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "comment/" . $cid . "/approve"; + $this->options['alter']['query'] = drupal_get_destination() + array('token' => drupal_get_token("comment/$cid/approve")); + + return $text; + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkDelete.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkDelete.php new file mode 100644 index 0000000..fbe0815 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkDelete.php @@ -0,0 +1,40 @@ +options['text']) ? $this->options['text'] : t('delete'); + $cid = $this->get_value($values, 'cid'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "comment/" . $cid . "/delete"; + $this->options['alter']['query'] = drupal_get_destination(); + + return $text; + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkEdit.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkEdit.php new file mode 100644 index 0000000..4a17fe7 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkEdit.php @@ -0,0 +1,63 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['destination'] = array( + '#type' => 'checkbox', + '#title' => t('Use destination'), + '#description' => t('Add destination to the link'), + '#default_value' => $this->options['destination'], + '#fieldset' => 'more', + ); + } + + function render_link($data, $values) { + parent::render_link($data, $values); + // ensure user has access to edit this comment. + $comment = $this->get_value($values); + if (!comment_access('edit', $comment)) { + return; + } + + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + unset($this->options['alter']['fragment']); + + if (!empty($this->options['destination'])) { + $this->options['alter']['query'] = drupal_get_destination(); + } + + $this->options['alter']['path'] = "comment/" . $comment->id() . "/edit"; + + return $text; + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkReply.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkReply.php new file mode 100644 index 0000000..bee231c --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/LinkReply.php @@ -0,0 +1,40 @@ +options['text']) ? $this->options['text'] : t('reply'); + $nid = $this->get_value($values, 'nid'); + $cid = $this->get_value($values, 'cid'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "comment/reply/" . $nid . '/' . $cid; + + return $text; + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NcsLastCommentName.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NcsLastCommentName.php new file mode 100644 index 0000000..e52caa4 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NcsLastCommentName.php @@ -0,0 +1,77 @@ +ensureMyTable(); + // join 'users' to this table via vid + $definition = array( + 'table' => 'users', + 'field' => 'uid', + 'left_table' => $this->tableAlias, + 'left_field' => 'last_comment_uid', + 'extra' => array( + array( + 'field' => 'uid', + 'operator' => '!=', + 'value' => '0' + ) + ) + ); + $join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $definition); + + // ncs_user alias so this can work with the sort handler, below. +// $this->user_table = $this->query->add_relationship(NULL, $join, 'users', $this->relationship); + $this->user_table = $this->query->ensure_table('ncs_users', $this->relationship, $join); + + $this->field_alias = $this->query->add_field(NULL, "COALESCE($this->user_table.name, $this->tableAlias.$this->field)", $this->tableAlias . '_' . $this->field); + + $this->user_field = $this->query->add_field($this->user_table, 'name'); + $this->uid = $this->query->add_field($this->tableAlias, 'last_comment_uid'); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['link_to_user'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + function render($values) { + if (!empty($this->options['link_to_user'])) { + $account = entity_create('user', array()); + $account->name = $this->get_value($values); + $account->uid = $values->{$this->uid}; + return theme('username', array( + 'account' => $account + )); + } + else { + return $this->sanitizeValue($this->get_value($values)); + } + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NcsLastUpdated.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NcsLastUpdated.php new file mode 100644 index 0000000..da21adf --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NcsLastUpdated.php @@ -0,0 +1,31 @@ +ensureMyTable(); + $this->node_table = $this->query->ensure_table('node', $this->relationship); + $this->field_alias = $this->query->add_field(NULL, "GREATEST(" . $this->node_table . ".changed, " . $this->tableAlias . ".last_comment_timestamp)", $this->tableAlias . '_' . $this->field); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeComment.php new file mode 100644 index 0000000..e0b3f90 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeComment.php @@ -0,0 +1,38 @@ +get_value($values); + switch ($value) { + case COMMENT_NODE_HIDDEN: + default: + return t('Hidden'); + case COMMENT_NODE_CLOSED: + return t('Closed'); + case COMMENT_NODE_OPEN: + return t('Open'); + } + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeLink.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeLink.php new file mode 100644 index 0000000..21e0102 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeLink.php @@ -0,0 +1,57 @@ + FALSE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['teaser'] = array( + '#type' => 'checkbox', + '#title' => t('Show teaser-style link'), + '#default_value' => $this->options['teaser'], + '#description' => t('Show the comment link in the form used on standard node teasers, rather than the full node form.'), + ); + + parent::buildOptionsForm($form, $form_state); + } + + public function query() {} + + function render($values) { + $node = $this->get_entity($values); + + // Call comment.module's hook_link: comment_link($type, $node = NULL, $teaser = FALSE) + // Call node by reference so that something is changed here + comment_node_view($node, $this->options['teaser'] ? 'teaser' : 'full'); + // question: should we run these through: drupal_alter('link', $links, $node); + // might this have unexpected consequences if these hooks expect items in $node that we don't have? + + // Only render the links, if they are defined. + return !empty($node->content['links']['comment']) ? drupal_render($node->content['links']['comment']) : ''; + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeNewComments.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeNewComments.php new file mode 100644 index 0000000..fe24f52 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/NodeNewComments.php @@ -0,0 +1,124 @@ +additional_fields['nid'] = 'nid'; + $this->additional_fields['type'] = 'type'; + $this->additional_fields['comment_count'] = array('table' => 'node_comment_statistics', 'field' => 'comment_count'); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['link_to_comment'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_comment'] = array( + '#title' => t('Link this field to new comments'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_comment'], + ); + + parent::buildOptionsForm($form, $form_state); + } + + public function query() { + $this->ensureMyTable(); + $this->add_additional_fields(); + $this->field_alias = $this->table . '_' . $this->field; + } + + function pre_render(&$values) { + global $user; + if (!$user->uid || empty($values)) { + return; + } + + $nids = array(); + $ids = array(); + foreach ($values as $id => $result) { + $nids[] = $result->{$this->aliases['nid']}; + $values[$id]->{$this->field_alias} = 0; + // Create a reference so we can find this record in the values again. + if (empty($ids[$result->{$this->aliases['nid']}])) { + $ids[$result->{$this->aliases['nid']}] = array(); + } + $ids[$result->{$this->aliases['nid']}][] = $id; + } + + if ($nids) { + $query = db_select('node', 'n'); + $query->addField('n', 'nid'); + $query->innerJoin('comment', 'c', 'n.nid = c.nid'); + $query->addExpression('COUNT(c.cid)', 'num_comments'); + $query->leftJoin('history', 'h', 'h.nid = n.nid'); + $query->condition('n.nid', $nids); + $query->where('c.changed > GREATEST(COALESCE(h.timestamp, :timestamp), :timestamp)', array(':timestamp' => NODE_NEW_LIMIT)); + $query->condition('c.status', COMMENT_PUBLISHED); + $query->groupBy('n.nid'); + $result = $query->execute(); + foreach ($result as $node) { + foreach ($ids[$node->nid] as $id) { + $values[$id]->{$this->field_alias} = $node->num_comments; + } + } + } + } + + function render_link($data, $values) { + if (!empty($this->options['link_to_comment']) && $data !== NULL && $data !== '') { + $node = entity_create('node', array( + 'nid' => $this->get_value($values, 'nid'), + 'type' => $this->get_value($values, 'type'), + )); + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'node/' . $node->nid; + $this->options['alter']['query'] = comment_new_page_count($this->get_value($values, 'comment_count'), $this->get_value($values), $node); + $this->options['alter']['fragment'] = 'new'; + } + + return $data; + } + + function render($values) { + $value = $this->get_value($values); + if (!empty($value)) { + return $this->render_link(parent::render($values), $values); + } + else { + $this->options['alter']['make_link'] = FALSE; + } + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Username.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Username.php new file mode 100644 index 0000000..42a7590 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/field/Username.php @@ -0,0 +1,71 @@ +additional_fields['uid'] = 'uid'; + $this->additional_fields['homepage'] = 'homepage'; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_to_user'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_user'] = array( + '#title' => t("Link this field to its user or an author's homepage"), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_user'], + ); + parent::buildOptionsForm($form, $form_state); + } + + function render_link($data, $values) { + if (!empty($this->options['link_to_user'])) { + $account = entity_create('user', array()); + $account->uid = $this->get_value($values, 'uid'); + $account->name = $this->get_value($values); + $account->homepage = $this->get_value($values, 'homepage'); + + return theme('username', array( + 'account' => $account + )); + } + else { + return $data; + } + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/NcsLastUpdated.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/NcsLastUpdated.php new file mode 100644 index 0000000..9b96a6a --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/NcsLastUpdated.php @@ -0,0 +1,37 @@ +ensureMyTable(); + $this->node_table = $this->query->ensure_table('node', $this->relationship); + + $field = "GREATEST(" . $this->node_table . ".changed, " . $this->tableAlias . ".last_comment_timestamp)"; + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/NodeComment.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/NodeComment.php new file mode 100644 index 0000000..1eae8c9 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/NodeComment.php @@ -0,0 +1,33 @@ +value_options = array( + COMMENT_NODE_HIDDEN => t('Hidden'), + COMMENT_NODE_CLOSED => t('Closed'), + COMMENT_NODE_OPEN => t('Open'), + ); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/UserUid.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/UserUid.php new file mode 100644 index 0000000..32eae9c --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/filter/UserUid.php @@ -0,0 +1,41 @@ +ensureMyTable(); + + $subselect = db_select('comment', 'c'); + $subselect->addField('c', 'cid'); + $subselect->condition('c.uid', $this->value, $this->operator); + $subselect->where("c.nid = $this->tableAlias.nid"); + + $condition = db_or() + ->condition("$this->tableAlias.uid", $this->value, $this->operator) + ->exists($subselect); + + $this->query->add_where($this->options['group'], $condition); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/row/Rss.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/row/Rss.php new file mode 100644 index 0000000..d140963 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/row/Rss.php @@ -0,0 +1,168 @@ + 'default'); + $options['links'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['item_length'] = array( + '#type' => 'select', + '#title' => t('Display type'), + '#options' => $this->options_form_summary_options(), + '#default_value' => $this->options['item_length'], + ); + $form['links'] = array( + '#type' => 'checkbox', + '#title' => t('Display links'), + '#default_value' => $this->options['links'], + ); + } + + function pre_render($result) { + $cids = array(); + $nids = array(); + + foreach ($result as $row) { + $cids[] = $row->cid; + } + + $this->comments = comment_load_multiple($cids); + foreach ($this->comments as &$comment) { + $comment->depth = count(explode('.', $comment->thread)) - 1; + $nids[] = $comment->nid; + } + + $this->nodes = node_load_multiple($nids); + } + + /** + * Return the main options, which are shown in the summary title + * + * @see views_plugin_row_node_rss::options_form_summary_options() + * @todo: Maybe provide a views_plugin_row_rss_entity and reuse this method + * in views_plugin_row_comment|node_rss.inc + */ + function options_form_summary_options() { + $entity_info = entity_get_info('node'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + $options['title'] = t('Title only'); + $options['default'] = t('Use site default RSS settings'); + return $options; + } + + function render($row) { + global $base_url; + + $cid = $row->{$this->field_alias}; + if (!is_numeric($cid)) { + return; + } + + $item_length = $this->options['item_length']; + if ($item_length == 'default') { + $item_length = config('system.rss')->get('items.view_mode'); + } + + // Load the specified comment and its associated node: + $comment = $this->comments[$cid]; + if (empty($comment) || empty($this->nodes[$comment->nid])) { + return; + } + + $item_text = ''; + + $uri = $comment->uri(); + $comment->link = url($uri['path'], $uri['options'] + array('absolute' => TRUE)); + $comment->rss_namespaces = array(); + $comment->rss_elements = array( + array( + 'key' => 'pubDate', + 'value' => gmdate('r', $comment->created), + ), + array( + 'key' => 'dc:creator', + 'value' => $comment->name, + ), + array( + 'key' => 'guid', + 'value' => 'comment ' . $comment->id() . ' at ' . $base_url, + 'attributes' => array('isPermaLink' => 'false'), + ), + ); + + // The comment gets built and modules add to or modify + // $comment->rss_elements and $comment->rss_namespaces. + $build = comment_view($comment, $this->nodes[$comment->nid], 'rss'); + unset($build['#theme']); + + if (!empty($comment->rss_namespaces)) { + $this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $comment->rss_namespaces); + } + + // Hide the links if desired. + if (!$this->options['links']) { + hide($build['links']); + } + + if ($item_length != 'title') { + // We render comment contents and force links to be last. + $build['links']['#weight'] = 1000; + $item_text .= drupal_render($build); + } + + $item = new stdClass(); + $item->description = $item_text; + $item->title = $comment->label(); + $item->link = $comment->link; + $item->elements = $comment->rss_elements; + $item->cid = $comment->id(); + + return theme($this->themeFunctions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $item + )); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/row/View.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/row/View.php new file mode 100644 index 0000000..1417d54 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/row/View.php @@ -0,0 +1,114 @@ + TRUE, 'bool' => TRUE); + $options['view_mode'] = array('default' => 'full'); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $options = $this->options_form_summary_options(); + $form['view_mode'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t('View mode'), + '#default_value' => $this->options['view_mode'], + ); + + $form['links'] = array( + '#type' => 'checkbox', + '#title' => t('Display links'), + '#default_value' => $this->options['links'], + ); + } + + /** + * Return the main options, which are shown in the summary title. + */ + function options_form_summary_options() { + $entity_info = entity_get_info('comment'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + if (empty($options)) { + $options = array( + 'full' => t('Full content') + ); + } + + return $options; + } + + function pre_render($result) { + $cids = array(); + + foreach ($result as $row) { + $cids[] = $row->cid; + } + + // Load all comments. + $cresult = comment_load_multiple($cids); + $nids = array(); + foreach ($cresult as $comment) { + $comment->depth = count(explode('.', $comment->thread)) - 1; + $this->comments[$comment->id()] = $comment; + $nids[] = $comment->nid; + } + + // Load all nodes of the comments. + $nodes = node_load_multiple(array_unique($nids)); + foreach ($nodes as $node) { + $this->nodes[$node->nid] = $node; + } + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/NcsLastCommentName.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/NcsLastCommentName.php new file mode 100644 index 0000000..de3511e --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/NcsLastCommentName.php @@ -0,0 +1,47 @@ +ensureMyTable(); + $definition = array( + 'table' => 'users', + 'field' => 'uid', + 'left_table' => $this->tableAlias, + 'left_field' => 'last_comment_uid', + ); + $join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $definition); + + // @todo this might be safer if we had an ensure_relationship rather than guessing + // the table alias. Though if we did that we'd be guessing the relationship name + // so that doesn't matter that much. +// $this->user_table = $this->query->add_relationship(NULL, $join, 'users', $this->relationship); + $this->user_table = $this->query->ensure_table('ncs_users', $this->relationship, $join); + $this->user_field = $this->query->add_field($this->user_table, 'name'); + + // Add the field. + $this->query->add_orderby(NULL, "LOWER(COALESCE($this->user_table.name, $this->tableAlias.$this->field))", $this->options['order'], $this->tableAlias . '_' . $this->field); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/NcsLastUpdated.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/NcsLastUpdated.php new file mode 100644 index 0000000..db4ddc8 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/NcsLastUpdated.php @@ -0,0 +1,31 @@ +ensureMyTable(); + $this->node_table = $this->query->ensure_table('node', $this->relationship); + $this->field_alias = $this->query->add_orderby(NULL, "GREATEST(" . $this->node_table . ".changed, " . $this->tableAlias . ".last_comment_timestamp)", $this->options['order'], $this->tableAlias . '_' . $this->field); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/Thread.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/Thread.php new file mode 100644 index 0000000..b562e85 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/sort/Thread.php @@ -0,0 +1,40 @@ +ensureMyTable(); + + //Read comment_render() in comment.module for an explanation of the + //thinking behind this sort. + if ($this->options['order'] == 'DESC') { + $this->query->add_orderby($this->tableAlias, $this->realField, $this->options['order']); + } + else { + $alias = $this->tableAlias . '_' . $this->realField . 'asc'; + //@todo is this secure? + $this->query->add_orderby(NULL, "SUBSTRING({$this->tableAlias}.{$this->realField}, 1, (LENGTH({$this->tableAlias}.{$this->realField}) - 1))", $this->options['order'], $alias); + } + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/views/wizard/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/views/wizard/Comment.php new file mode 100644 index 0000000..d079af4 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Plugin/views/wizard/Comment.php @@ -0,0 +1,174 @@ + 'cid', + 'table' => 'comment', + 'field' => 'cid', + 'exclude' => TRUE, + 'link_to_comment' => FALSE, + 'alter' => array( + 'alter_text' => TRUE, + 'text' => 'comment/[cid]#comment-[cid]' + ), + ); + + /** + * Set default values for the filters. + */ + protected $filters = array( + 'status' => array( + 'value' => TRUE, + 'table' => 'comment', + 'field' => 'status' + ), + 'status_node' => array( + 'value' => TRUE, + 'table' => 'node', + 'field' => 'status', + 'relationship' => 'nid' + ) + ); + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::row_style_options(). + */ + protected function row_style_options() { + $options = array(); + $options['comment'] = t('comments'); + $options['fields'] = t('fields'); + return $options; + } + + protected function build_form_style(array &$form, array &$form_state, $type) { + parent::build_form_style($form, $form_state, $type); + $style_form =& $form['displays'][$type]['options']['style']; + // Some style plugins don't support row plugins so stop here if that's the + // case. + if (!isset($style_form['row_plugin']['#default_value'])) { + return; + } + $row_plugin = $style_form['row_plugin']['#default_value']; + switch ($row_plugin) { + case 'comment': + $style_form['row_options']['links'] = array( + '#type' => 'select', + '#title_display' => 'invisible', + '#title' => t('Should links be displayed below each comment'), + '#options' => array( + 1 => t('with links (allow users to reply to the comment, etc.)'), + 0 => t('without links'), + ), + '#default_value' => 1, + ); + break; + } + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::page_display_options(). + */ + protected function page_display_options(array $form, array &$form_state) { + $display_options = parent::page_display_options($form, $form_state); + $row_plugin = isset($form_state['values']['page']['style']['row_plugin']) ? $form_state['values']['page']['style']['row_plugin'] : NULL; + $row_options = isset($form_state['values']['page']['style']['row_options']) ? $form_state['values']['page']['style']['row_options'] : array(); + $this->display_options_row($display_options, $row_plugin, $row_options); + return $display_options; + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::page_display_options(). + */ + protected function block_display_options(array $form, array &$form_state) { + $display_options = parent::block_display_options($form, $form_state); + $row_plugin = isset($form_state['values']['block']['style']['row_plugin']) ? $form_state['values']['block']['style']['row_plugin'] : NULL; + $row_options = isset($form_state['values']['block']['style']['row_options']) ? $form_state['values']['block']['style']['row_options'] : array(); + $this->display_options_row($display_options, $row_plugin, $row_options); + return $display_options; + } + + /** + * Set the row style and row style plugins to the display_options. + */ + protected function display_options_row(&$display_options, $row_plugin, $row_options) { + switch ($row_plugin) { + case 'comment': + $display_options['row']['type'] = 'comment'; + $display_options['row']['options']['links'] = !empty($row_options['links']); + break; + } + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::default_display_options(). + */ + protected function default_display_options() { + $display_options = parent::default_display_options(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + + // Add a relationship to nodes. + $display_options['relationships']['nid']['id'] = 'nid'; + $display_options['relationships']['nid']['table'] = 'comment'; + $display_options['relationships']['nid']['field'] = 'nid'; + $display_options['relationships']['nid']['required'] = 1; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + /* Field: Comment: Title */ + $display_options['fields']['subject']['id'] = 'subject'; + $display_options['fields']['subject']['table'] = 'comment'; + $display_options['fields']['subject']['field'] = 'subject'; + $display_options['fields']['subject']['label'] = ''; + $display_options['fields']['subject']['alter']['alter_text'] = 0; + $display_options['fields']['subject']['alter']['make_link'] = 0; + $display_options['fields']['subject']['alter']['absolute'] = 0; + $display_options['fields']['subject']['alter']['trim'] = 0; + $display_options['fields']['subject']['alter']['word_boundary'] = 0; + $display_options['fields']['subject']['alter']['ellipsis'] = 0; + $display_options['fields']['subject']['alter']['strip_tags'] = 0; + $display_options['fields']['subject']['alter']['html'] = 0; + $display_options['fields']['subject']['hide_empty'] = 0; + $display_options['fields']['subject']['empty_zero'] = 0; + $display_options['fields']['subject']['link_to_comment'] = 1; + + return $display_options; + } + +} diff --git a/core/modules/contact/contact.views.inc b/core/modules/contact/contact.views.inc new file mode 100644 index 0000000..307ffbc --- /dev/null +++ b/core/modules/contact/contact.views.inc @@ -0,0 +1,21 @@ + array( + 'title' => t('Link to contact page'), + 'help' => t('Provide a simple link to the user contact page.'), + 'id' => 'contact_link', + ), + ); +} diff --git a/core/modules/contact/lib/Drupal/contact/Plugin/views/field/ContactLink.php b/core/modules/contact/lib/Drupal/contact/Plugin/views/field/ContactLink.php new file mode 100644 index 0000000..4a9a238 --- /dev/null +++ b/core/modules/contact/lib/Drupal/contact/Plugin/views/field/ContactLink.php @@ -0,0 +1,68 @@ +options['text']) ? t('contact') : $this->options['text']; + parent::buildOptionsForm($form, $form_state); + } + + // An example of field level access control. + // We must override the access method in the parent class, as that requires + // the 'access user profiles' permission, which the contact form does not. + public function access() { + return user_access('access user contact forms'); + } + + function render_link($data, $values) { + global $user; + $uid = $this->get_value($values, 'uid'); + + if (empty($uid)) { + return; + } + + $account = user_load($uid); + if (empty($account)) { + return; + } + + // Check access when we pull up the user account so we know + // if the user has made the contact page available. + $menu_item = menu_get_item("user/$uid/contact"); + if (!$menu_item['access'] || empty($account->data['contact'])) { + return; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'user/' . $account->uid . '/contact'; + $this->options['alter']['attributes'] = array('title' => t('Contact %user', array('%user' => $account->name))); + + $text = $this->options['text']; + + return $text; + } + +} diff --git a/core/modules/field/field.views.inc b/core/modules/field/field.views.inc new file mode 100644 index 0000000..5e1e849 --- /dev/null +++ b/core/modules/field/field.views.inc @@ -0,0 +1,463 @@ + $entity_type) { + foreach ($entity_type as $bundle) { + if (isset($bundle[$field_name])) { + $label_counter[$bundle[$field_name]['label']] = isset($label_counter[$bundle[$field_name]['label']]) ? ++$label_counter[$bundle[$field_name]['label']] : 1; + $all_labels[$entity_name][$bundle[$field_name]['label']] = TRUE; + } + } + } + if (empty($label_counter)) { + return array($field_name, $all_labels); + } + // Sort the field lables by it most used label and return the most used one. + arsort($label_counter); + $label_counter = array_keys($label_counter); + return array($label_counter[0], $all_labels); +} + +/** + * Default views data implementation for a field. + */ +function field_views_field_default_views_data($field) { + $field_types = field_info_field_types(); + + // Check the field module is available. + if (!isset($field_types[$field['type']])) { + return; + } + + $data = array(); + + $current_table = _field_sql_storage_tablename($field); + $revision_table = _field_sql_storage_revision_tablename($field); + + // The list of entity:bundle that this field is used in. + $bundles_names = array(); + $supports_revisions = FALSE; + $entity_tables = array(); + $current_tables = array(); + $revision_tables = array(); + $groups = array(); + + $group_name = count($field['bundles']) > 1 ? t('Field') : NULL; + + // Build the relationships between the field table and the entity tables. + foreach ($field['bundles'] as $entity => $bundles) { + $entity_info = entity_get_info($entity); + $groups[$entity] = $entity_info['label']; + + // Override Node to Content. + if ($groups[$entity] == t('Node')) { + $groups[$entity] = t('Content'); + } + + // If only one bundle use this as the default name. + if (empty($group_name)) { + $group_name = $groups[$entity]; + } + + $entity_tables[$entity_info['base table']] = $entity; + $current_tables[$entity] = $entity_info['base table']; + if (isset($entity_info['revision table'])) { + $entity_tables[$entity_info['revision table']] = $entity; + $revision_tables[$entity] = $entity_info['revision table']; + } + + $data[$current_table]['table']['join'][$entity_info['base table']] = array( + 'left_field' => $entity_info['entity keys']['id'], + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => $entity), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + + if (!empty($entity_info['entity keys']['revision']) && !empty($entity_info['revision table'])) { + $data[$revision_table]['table']['join'][$entity_info['revision table']] = array( + 'left_field' => $entity_info['entity keys']['revision'], + 'field' => 'revision_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => $entity), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + + $supports_revisions = TRUE; + } + + foreach ($bundles as $bundle) { + $bundles_names[] = t('@entity:@bundle', array('@entity' => $entity, '@bundle' => $bundle)); + } + } + + $tables = array(); + $tables[FIELD_LOAD_CURRENT] = $current_table; + if ($supports_revisions) { + $tables[FIELD_LOAD_REVISION] = $revision_table; + } + + $add_fields = array('delta', 'langcode', 'bundle'); + foreach ($field['columns'] as $column_name => $attributes) { + $add_fields[] = _field_sql_storage_columnname($field['field_name'], $column_name); + } + + // Note: we don't have a label available here, because we are at the field + // level, not at the instance level. So we just go through all instances + // and take the one which is used the most frequently. + $field_name = $field['field_name']; + list($label, $all_labels) = field_views_field_label($field_name); + foreach ($tables as $type => $table) { + if ($type == FIELD_LOAD_CURRENT) { + $group = $group_name; + $old_column = 'entity_id'; + $column = $field['field_name']; + } + else { + $group = t('@group (historical data)', array('@group' => $group_name)); + $old_column = 'revision_id'; + $column = $field['field_name'] . '-' . $old_column; + } + + $data[$table][$column] = array( + 'group' => $group, + 'title' => $label, + 'title short' => $label, + 'help' => t('Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + + // Go through and create a list of aliases for all possible combinations of + // entity type + name. + $aliases = array(); + $also_known = array(); + foreach ($all_labels as $entity_name => $labels) { + foreach ($labels as $label_name => $true) { + if ($type == FIELD_LOAD_CURRENT) { + if ($group_name != $groups[$entity_name] || $label != $label_name) { + $aliases[] = array( + 'base' => $current_tables[$entity_name], + 'group' => $groups[$entity_name], + 'title' => $label_name, + 'help' => t('This is an alias of @group: @field.', array('@group' => $group_name, '@field' => $label)), + ); + } + $also_known[] = t('@group: @field', array('@group' => $groups[$entity_name], '@field' => $label_name)); + } + else { + if ($group_name != $groups[$entity_name] && $label != $label_name && isset($revision_tables[$entity_name])) { + $aliases[] = array( + 'base' => $revision_tables[$entity_name], + 'group' => t('@group (historical data)', array('@group' => $groups[$entity_name])), + 'title' => $label_name, + 'help' => t('This is an alias of @group: @field.', array('@group' => $group_name, '@field' => $label)), + ); + } + $also_known[] = t('@group (historical data): @field', array('@group' => $groups[$entity_name], '@field' => $label_name)); + } + } + } + if ($aliases) { + $data[$table][$column]['aliases'] = $aliases; + $data[$table][$column]['help'] .= ' ' . t('Also known as: !also.', array('!also' => implode(', ', $also_known))); + } + + $keys = array_keys($field['columns']); + $real_field = reset($keys); + $data[$table][$column]['field'] = array( + 'table' => $table, + 'id' => 'field', + 'click sortable' => TRUE, + 'field_name' => $field['field_name'], + // Provide a real field for group by. + 'real field' => $column . '_' . $real_field, + 'additional fields' => $add_fields, + 'entity_tables' => $entity_tables, + // Default the element type to div, let the UI change it if necessary. + 'element type' => 'div', + 'is revision' => $type == FIELD_LOAD_REVISION, + ); + } + + foreach ($field['columns'] as $column => $attributes) { + $allow_sort = TRUE; + + // Identify likely filters and arguments for each column based on field type. + switch ($attributes['type']) { + case 'int': + case 'mediumint': + case 'tinyint': + case 'bigint': + case 'serial': + case 'numeric': + case 'float': + $filter = 'numeric'; + $argument = 'numeric'; + $sort = 'standard'; + break; + case 'text': + case 'blob': + // It does not make sense to sort by blob or text. + $allow_sort = FALSE; + default: + $filter = 'string'; + $argument = 'string'; + $sort = 'standard'; + break; + } + + if (count($field['columns']) == 1 || $column == 'value') { + $title = t('@label (!name)', array('@label' => $label, '!name' => $field['field_name'])); + // CCK used the first 10 characters of $label. Probably doesn't matter. + $title_short = $label; + } + else { + $title = t('@label (!name:!column)', array('@label' => $label, '!name' => $field['field_name'], '!column' => $column)); + $title_short = t('@label:!column', array('@label' => $label, '!column' => $column)); + } + + foreach ($tables as $type => $table) { + if ($type == FIELD_LOAD_CURRENT) { + $group = $group_name; + } + else { + $group = t('@group (historical data)', array('@group' => $group_name)); + } + $column_real_name = $field['storage']['details']['sql'][$type][$table][$column]; + + // Load all the fields from the table by default. + $additional_fields = array_values($field['storage']['details']['sql'][$type][$table]); + + $data[$table][$column_real_name] = array( + 'group' => $group, + 'title' => $title, + 'title short' => $title_short, + 'help' => t('Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + + // Go through and create a list of aliases for all possible combinations of + // entity type + name. + $aliases = array(); + $also_known = array(); + foreach ($all_labels as $entity_name => $labels) { + foreach ($labels as $label_name => $true) { + if ($group_name != $groups[$entity_name] || $label != $label_name) { + if (count($field['columns']) == 1 || $column == 'value') { + $alias_title = t('@label (!name)', array('@label' => $label_name, '!name' => $field['field_name'])); + // CCK used the first 10 characters of $label. Probably doesn't matter. + } + else { + $alias_title = t('@label (!name:!column)', array('@label' => $label_name, '!name' => $field['field_name'], '!column' => $column)); + } + $aliases[] = array( + 'group' => $groups[$entity_name], + 'title' => $alias_title, + 'help' => t('This is an alias of @group: @field.', array('@group' => $group_name, '@field' => $title)), + ); + } + $also_known[] = t('@group: @field', array('@group' => $groups[$entity_name], '@field' => $title)); + } + } + if ($aliases) { + $data[$table][$column_real_name]['aliases'] = $aliases; + $data[$table][$column_real_name]['help'] .= ' ' . t('Also known as: !also.', array('!also' => implode(', ', $also_known))); + } + + $data[$table][$column_real_name]['argument'] = array( + 'field' => $column_real_name, + 'table' => $table, + 'id' => $argument, + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'empty field name' => t('- No value -'), + ); + $data[$table][$column_real_name]['filter'] = array( + 'field' => $column_real_name, + 'table' => $table, + 'id' => $filter, + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'allow empty' => TRUE, + ); + if (!empty($allow_sort)) { + $data[$table][$column_real_name]['sort'] = array( + 'field' => $column_real_name, + 'table' => $table, + 'id' => $sort, + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + ); + } + + // Expose additional delta column for multiple value fields. + if ($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) { + $title_delta = t('@label (!name:delta)', array('@label' => $label, '!name' => $field['field_name'])); + $title_short_delta = t('@label:delta', array('@label' => $label)); + + $data[$table]['delta'] = array( + 'group' => $group, + 'title' => $title_delta, + 'title short' => $title_short_delta, + 'help' => t('Delta - Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + $data[$table]['delta']['field'] = array( + 'id' => 'numeric', + ); + $data[$table]['delta']['argument'] = array( + 'field' => 'delta', + 'table' => $table, + 'id' => 'numeric', + 'additional fields' => $additional_fields, + 'empty field name' => t('- No value -'), + 'field_name' => $field['field_name'], + ); + $data[$table]['delta']['filter'] = array( + 'field' => 'delta', + 'table' => $table, + 'id' => 'numeric', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'allow empty' => TRUE, + ); + $data[$table]['delta']['sort'] = array( + 'field' => 'delta', + 'table' => $table, + 'id' => 'standard', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + ); + } + + // Expose additional language column for translatable fields. + if (!empty($field['translatable'])) { + $title_language = t('@label (!name:language)', array('@label' => $label, '!name' => $field['field_name'])); + $title_short_language = t('@label:language', array('@label' => $label)); + + $data[$table]['language'] = array( + 'group' => $group, + 'title' => $title_language, + 'title short' => $title_short_language, + 'help' => t('Language - Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + $data[$table]['language']['field'] = array( + 'id' => 'language', + ); + $data[$table]['language']['argument'] = array( + 'field' => 'language', + 'table' => $table, + 'id' => 'language', + 'additional fields' => $additional_fields, + 'empty field name' => t(''), + 'field_name' => $field['field_name'], + ); + $data[$table]['language']['filter'] = array( + 'field' => 'language', + 'table' => $table, + 'id' => 'language', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'allow empty' => TRUE, + ); + $data[$table]['language']['sort'] = array( + 'field' => 'language', + 'table' => $table, + 'id' => 'standard', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + ); + } + } + } + + return $data; +} + +/** + * Have a different filter handler for lists. This should allow to select values of the list. + */ +function list_field_views_data($field) { + $data = field_views_field_default_views_data($field); + foreach ($data as $table_name => $table_data) { + foreach ($table_data as $field_name => $field_data) { + if (isset($field_data['filter']) && $field_name != 'delta') { + $data[$table_name][$field_name]['filter']['id'] = 'field_list'; + } + if (isset($field_data['argument']) && $field_name != 'delta') { + if ($field['type'] == 'list_text') { + $data[$table_name][$field_name]['argument']['id'] = 'field_list_string'; + } + else { + $data[$table_name][$field_name]['argument']['id'] = 'field_list'; + } + } + } + } + return $data; +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/views/argument/FieldList.php b/core/modules/field/lib/Drupal/field/Plugin/views/argument/FieldList.php new file mode 100644 index 0000000..f1b0d35 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/views/argument/FieldList.php @@ -0,0 +1,74 @@ +definition['field_name']); + $this->allowed_values = options_allowed_values($field); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['summary']['contains']['human'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['summary']['human'] = array( + '#title' => t('Display list value as human readable'), + '#type' => 'checkbox', + '#default_value' => $this->options['summary']['human'], + '#states' => array( + 'visible' => array( + ':input[name="options[default_action]"]' => array('value' => 'summary'), + ), + ), + ); + } + + function summary_name($data) { + $value = $data->{$this->name_alias}; + // If the list element has a human readable name show it, + if (isset($this->allowed_values[$value]) && !empty($this->options['summary']['human'])) { + return field_filter_xss($this->allowed_values[$value]); + } + // else fallback to the key. + else { + return check_plain($value); + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/views/argument/ListString.php b/core/modules/field/lib/Drupal/field/Plugin/views/argument/ListString.php new file mode 100644 index 0000000..596d73e --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/views/argument/ListString.php @@ -0,0 +1,76 @@ +definition['field_name']); + $this->allowed_values = options_allowed_values($field); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['summary']['contains']['human'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['summary']['human'] = array( + '#title' => t('Display list value as human readable'), + '#type' => 'checkbox', + '#default_value' => $this->options['summary']['human'], + '#states' => array( + 'visible' => array( + ':input[name="options[default_action]"]' => array('value' => 'summary'), + ), + ), + ); + } + + + function summary_name($data) { + $value = $data->{$this->name_alias}; + // If the list element has a human readable name show it, + if (isset($this->allowed_values[$value]) && !empty($this->options['summary']['human'])) { + return $this->caseTransform(field_filter_xss($this->allowed_values[$value]), $this->options['case']); + } + // else fallback to the key. + else { + return $this->caseTransform(check_plain($value), $this->options['case']); + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php b/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php new file mode 100644 index 0000000..4c5e5da --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php @@ -0,0 +1,854 @@ +base_table. + * + * @var string + */ + public $base_table; + + /** + * Store the field instance. + * + * @var array + */ + public $instance; + + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + $this->field_info = $field = field_info_field($this->definition['field_name']); + $this->multiple = FALSE; + $this->limit_values = FALSE; + + if ($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) { + $this->multiple = TRUE; + + // If "Display all values in the same row" is FALSE, then we always limit + // in order to show a single unique value per row. + if (!$this->options['group_rows']) { + $this->limit_values = TRUE; + } + + // If "First and last only" is chosen, limit the values + if (!empty($this->options['delta_first_last'])) { + $this->limit_values = TRUE; + } + + // Otherwise, we only limit values if the user hasn't selected "all", 0, or + // the value matching field cardinality. + if ((intval($this->options['delta_limit']) && ($this->options['delta_limit'] != $field['cardinality'])) || intval($this->options['delta_offset'])) { + $this->limit_values = TRUE; + } + } + } + + /** + * Check whether current user has access to this handler. + * + * @return bool + * Return TRUE if the user has access to view this field. + */ + public function access() { + $base_table = $this->get_base_table(); + return field_access('view', $this->field_info, $this->definition['entity_tables'][$base_table]); + } + + /** + * Set the base_table and base_table_alias. + * + * @return string + * The base table which is used in the current view "context". + */ + function get_base_table() { + if (!isset($this->base_table)) { + // This base_table is coming from the entity not the field. + $this->base_table = $this->view->storage->base_table; + + // If the current field is under a relationship you can't be sure that the + // base table of the view is the base table of the current field. + // For example a field from a node author on a node view does have users as base table. + if (!empty($this->options['relationship']) && $this->options['relationship'] != 'none') { + $relationships = $this->view->display_handler->getOption('relationships'); + if (!empty($relationships[$this->options['relationship']])) { + $options = $relationships[$this->options['relationship']]; + $data = views_fetch_data($options['table']); + $this->base_table = $data[$options['field']]['relationship']['base']; + } + } + } + + return $this->base_table; + } + + /** + * Called to add the field to a query. + * + * By default, all needed data is taken from entities loaded by the query + * plugin. Columns are added only if they are used in groupings. + */ + public function query($use_groupby = FALSE) { + $this->get_base_table(); + + $entity_type = $this->definition['entity_tables'][$this->base_table]; + $fields = $this->additional_fields; + // No need to add the entity type. + $entity_type_key = array_search('entity_type', $fields); + if ($entity_type_key !== FALSE) { + unset($fields[$entity_type_key]); + } + + if ($use_groupby) { + // Add the fields that we're actually grouping on. + $options = array(); + if ($this->options['group_column'] != 'entity_id') { + $options = array($this->options['group_column'] => $this->options['group_column']); + } + $options += is_array($this->options['group_columns']) ? $this->options['group_columns'] : array(); + + $fields = array(); + $rkey = $this->definition['is revision'] ? 'FIELD_LOAD_REVISION' : 'FIELD_LOAD_CURRENT'; + // Go through the list and determine the actual column name from field api. + foreach ($options as $column) { + $name = $column; + if (isset($this->field_info['storage']['details']['sql'][$rkey][$this->table][$column])) { + $name = $this->field_info['storage']['details']['sql'][$rkey][$this->table][$column]; + } + + $fields[$column] = $name; + } + + $this->group_fields = $fields; + } + + // Add additional fields (and the table join itself) if needed. + if ($this->add_field_table($use_groupby)) { + $this->ensureMyTable(); + $this->add_additional_fields($fields); + + // Filter by langcode, if field translation is enabled. + $field = $this->field_info; + if (field_is_translatable($entity_type, $field) && !empty($this->view->display_handler->options['field_langcode_add_to_query'])) { + $column = $this->tableAlias . '.langcode'; + // By the same reason as field_language the field might be LANGUAGE_NOT_SPECIFIED in reality so allow it as well. + // @see this::field_langcode() + $default_langcode = language_default()->langcode; + $langcode = str_replace(array('***CURRENT_LANGUAGE***', '***DEFAULT_LANGUAGE***'), + array(drupal_container()->get(LANGUAGE_TYPE_CONTENT)->langcode, $default_langcode), + $this->view->display_handler->options['field_langcode']); + $placeholder = $this->placeholder(); + $langcode_fallback_candidates = array($langcode); + if (variable_get('locale_field_language_fallback', TRUE)) { + require_once DRUPAL_ROOT . '/includes/language.inc'; + $langcode_fallback_candidates = array_merge($langcode_fallback_candidates, language_fallback_get_candidates()); + } + else { + $langcode_fallback_candidates[] = LANGUAGE_NOT_SPECIFIED; + } + $this->query->add_where_expression(0, "$column IN($placeholder) OR $column IS NULL", array($placeholder => $langcode_fallback_candidates)); + } + } + } + + /** + * Determine if the field table should be added to the query. + */ + function add_field_table($use_groupby) { + // Grouping is enabled. + if ($use_groupby) { + return TRUE; + } + // This a multiple value field, but "group multiple values" is not checked. + if ($this->multiple && !$this->options['group_rows']) { + return TRUE; + } + return FALSE; + } + + /** + * Determine if this field is click sortable. + */ + function click_sortable() { + // Not click sortable in any case. + if (empty($this->definition['click sortable'])) { + return FALSE; + } + // A field is not click sortable if it's a multiple field with + // "group multiple values" checked, since a click sort in that case would + // add a join to the field table, which would produce unwanted duplicates. + if ($this->multiple && $this->options['group_rows']) { + return FALSE; + } + return TRUE; + } + + /** + * Called to determine what to tell the clicksorter. + */ + function click_sort($order) { + // No column selected, can't continue. + if (empty($this->options['click_sort_column'])) { + return; + } + + $this->ensureMyTable(); + $column = _field_sql_storage_columnname($this->definition['field_name'], $this->options['click_sort_column']); + if (!isset($this->aliases[$column])) { + // Column is not in query; add a sort on it (without adding the column). + $this->aliases[$column] = $this->tableAlias . '.' . $column; + } + $this->query->add_orderby(NULL, NULL, $order, $this->aliases[$column]); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + // defineOptions runs before init/construct, so no $this->field_info + $field = field_info_field($this->definition['field_name']); + $field_type = field_info_field_types($field['type']); + $column_names = array_keys($field['columns']); + $default_column = ''; + // Try to determine a sensible default. + if (count($column_names) == 1) { + $default_column = $column_names[0]; + } + elseif (in_array('value', $column_names)) { + $default_column = 'value'; + } + + // If the field has a "value" column, we probably need that one. + $options['click_sort_column'] = array( + 'default' => $default_column, + ); + $options['type'] = array( + 'default' => $field_type['default_formatter'], + ); + $options['settings'] = array( + 'default' => array(), + ); + $options['group_column'] = array( + 'default' => $default_column, + ); + $options['group_columns'] = array( + 'default' => array(), + ); + + // Options used for multiple value fields. + $options['group_rows'] = array( + 'default' => TRUE, + 'bool' => TRUE, + ); + // If we know the exact number of allowed values, then that can be + // the default. Otherwise, default to 'all'. + $options['delta_limit'] = array( + 'default' => ($field['cardinality'] > 1) ? $field['cardinality'] : 'all', + ); + $options['delta_offset'] = array( + 'default' => 0, + ); + $options['delta_reversed'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + $options['delta_first_last'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + + $options['multi_type'] = array( + 'default' => 'separator' + ); + $options['separator'] = array( + 'default' => ', ' + ); + + $options['field_api_classes'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $field = $this->field_info; + $formatters = _field_view_formatter_options($field['type']); + $column_names = array_keys($field['columns']); + + // If this is a multiple value field, add its options. + if ($this->multiple) { + $this->multiple_options_form($form, $form_state); + } + + // No need to ask the user anything if the field has only one column. + if (count($field['columns']) == 1) { + $form['click_sort_column'] = array( + '#type' => 'value', + '#value' => isset($column_names[0]) ? $column_names[0] : '', + ); + } + else { + $form['click_sort_column'] = array( + '#type' => 'select', + '#title' => t('Column used for click sorting'), + '#options' => drupal_map_assoc($column_names), + '#default_value' => $this->options['click_sort_column'], + '#description' => t('Used by Style: Table to determine the actual column to click sort the field on. The default is usually fine.'), + '#fieldset' => 'more', + ); + } + + $form['type'] = array( + '#type' => 'select', + '#title' => t('Formatter'), + '#options' => $formatters, + '#default_value' => $this->options['type'], + '#ajax' => array( + 'path' => views_ui_build_form_url($form_state), + ), + '#submit' => array('views_ui_config_item_form_submit_temporary'), + '#executes_submit_callback' => TRUE, + ); + + $form['field_api_classes'] = array( + '#title' => t('Use field template'), + '#type' => 'checkbox', + '#default_value' => $this->options['field_api_classes'], + '#description' => t('If checked, field api classes will be added using field.tpl.php (or equivalent). This is not recommended unless your CSS depends upon these classes. If not checked, template will not be used.'), + '#fieldset' => 'style_settings', + '#weight' => 20, + ); + + if ($this->multiple) { + $form['field_api_classes']['#description'] .= ' ' . t('Checking this option will cause the group Display Type and Separator values to be ignored.'); + } + + // Get the currently selected formatter. + $format = $this->options['type']; + + $formatter = field_info_formatter_types($format); + $settings = $this->options['settings'] + field_info_formatter_settings($format); + + // Provide an instance array for hook_field_formatter_settings_form(). + $this->instance = $this->fakeFieldInstance($format, $settings); + + // Get the settings form. + $settings_form = array('#value' => array()); + $function = $formatter['module'] . '_field_formatter_settings_form'; + if (function_exists($function)) { + $settings_form = $function($field, $this->instance, '_custom', $form, $form_state); + } + $form['settings'] = $settings_form; + } + + /** + * Provides a fake field instance. + * + * @param string $formatter + * The machine name of the formatter to use. + * @param array $formatter_settings + * An associative array of settings for the formatter. + * + * @return array + * An associative array of instance date for the fake field. + * + * @see field_info_instance() + */ + function fakeFieldInstance($formatter, $formatter_settings) { + $field_name = $this->definition['field_name']; + $field = field_read_field($field_name); + + $field_type = field_info_field_types($field['type']); + + return array( + // Build a fake entity type and bundle. + 'field_name' => $field_name, + 'entity_type' => 'views_fake', + 'bundle' => 'views_fake', + + // Use the default field settings for settings and widget. + 'settings' => field_info_instance_settings($field['type']), + 'widget' => array( + 'type' => $field_type['default_widget'], + 'settings' => array(), + ), + + // Build a dummy display mode. + 'display' => array( + '_custom' => array( + 'type' => $formatter, + 'settings' => $formatter_settings, + ), + ), + + // Set the other fields to their default values. + // @see _field_write_instance(). + 'required' => FALSE, + 'label' => $field_name, + 'description' => '', + 'deleted' => 0, + ); + } + + /** + * Provide options for multiple value fields. + */ + function multiple_options_form(&$form, &$form_state) { + $field = $this->field_info; + + $form['multiple_field_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Multiple field settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 5, + ); + + $form['group_rows'] = array( + '#title' => t('Display all values in the same row'), + '#type' => 'checkbox', + '#default_value' => $this->options['group_rows'], + '#description' => t('If checked, multiple values for this field will be shown in the same row. If not checked, each value in this field will create a new row. If using group by, please make sure to group by "Entity ID" for this setting to have any effect.'), + '#fieldset' => 'multiple_field_settings', + ); + + // Make the string translatable by keeping it as a whole rather than + // translating prefix and suffix separately. + list($prefix, $suffix) = explode('@count', t('Display @count value(s)')); + + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) { + $type = 'textfield'; + $options = NULL; + $size = 5; + } + else { + $type = 'select'; + $options = drupal_map_assoc(range(1, $field['cardinality'])); + $size = 1; + } + $form['multi_type'] = array( + '#type' => 'radios', + '#title' => t('Display type'), + '#options' => array( + 'ul' => t('Unordered list'), + 'ol' => t('Ordered list'), + 'separator' => t('Simple separator'), + ), + '#states' => array( + 'visible' => array( + ':input[name="options[group_rows]"]' => array('checked' => TRUE), + ), + ), + '#default_value' => $this->options['multi_type'], + '#fieldset' => 'multiple_field_settings', + ); + + $form['separator'] = array( + '#type' => 'textfield', + '#title' => t('Separator'), + '#default_value' => $this->options['separator'], + '#states' => array( + 'visible' => array( + ':input[name="options[group_rows]"]' => array('checked' => TRUE), + ':input[name="options[multi_type]"]' => array('value' => 'separator'), + ), + ), + '#fieldset' => 'multiple_field_settings', + ); + + $form['delta_limit'] = array( + '#type' => $type, + '#size' => $size, + '#field_prefix' => $prefix, + '#field_suffix' => $suffix, + '#options' => $options, + '#default_value' => $this->options['delta_limit'], + '#prefix' => '
', + '#states' => array( + 'visible' => array( + ':input[name="options[group_rows]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'multiple_field_settings', + ); + + list($prefix, $suffix) = explode('@count', t('starting from @count')); + $form['delta_offset'] = array( + '#type' => 'textfield', + '#size' => 5, + '#field_prefix' => $prefix, + '#field_suffix' => $suffix, + '#default_value' => $this->options['delta_offset'], + '#states' => array( + 'visible' => array( + ':input[name="options[group_rows]"]' => array('checked' => TRUE), + ), + ), + '#description' => t('(first item is 0)'), + '#fieldset' => 'multiple_field_settings', + ); + $form['delta_reversed'] = array( + '#title' => t('Reversed'), + '#type' => 'checkbox', + '#default_value' => $this->options['delta_reversed'], + '#suffix' => $suffix, + '#states' => array( + 'visible' => array( + ':input[name="options[group_rows]"]' => array('checked' => TRUE), + ), + ), + '#description' => t('(start from last values)'), + '#fieldset' => 'multiple_field_settings', + ); + $form['delta_first_last'] = array( + '#title' => t('First and last only'), + '#type' => 'checkbox', + '#default_value' => $this->options['delta_first_last'], + '#suffix' => '
', + '#states' => array( + 'visible' => array( + ':input[name="options[group_rows]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'multiple_field_settings', + ); + } + + /** + * Extend the groupby form with group columns. + */ + public function buildGroupByForm(&$form, &$form_state) { + parent::buildGroupByForm($form, $form_state); + // With "field API" fields, the column target of the grouping function + // and any additional grouping columns must be specified. + $group_columns = array( + 'entity_id' => t('Entity ID'), + ) + drupal_map_assoc(array_keys($this->field_info['columns']), 'ucfirst'); + + $form['group_column'] = array( + '#type' => 'select', + '#title' => t('Group column'), + '#default_value' => $this->options['group_column'], + '#description' => t('Select the column of this field to apply the grouping function selected above.'), + '#options' => $group_columns, + ); + + $options = drupal_map_assoc(array('bundle', 'language', 'entity_type'), 'ucfirst'); + + // Add on defined fields, noting that they're prefixed with the field name. + $form['group_columns'] = array( + '#type' => 'checkboxes', + '#title' => t('Group columns (additional)'), + '#default_value' => $this->options['group_columns'], + '#description' => t('Select any additional columns of this field to include in the query and to group on.'), + '#options' => $options + $group_columns, + ); + } + + public function submitGroupByForm(&$form, &$form_state) { + parent::submitGroupByForm($form, $form_state); + $item =& $form_state['handler']->options; + + // Add settings for "field API" fields. + $item['group_column'] = $form_state['values']['options']['group_column']; + $item['group_columns'] = array_filter($form_state['values']['options']['group_columns']); + } + + /** + * Render all items in this field together. + * + * When using advanced render, each possible item in the list is rendered + * individually. Then the items are all pasted together. + */ + function render_items($items) { + if (!empty($items)) { + if (!$this->options['group_rows']) { + return implode('', $items); + } + + if ($this->options['multi_type'] == 'separator') { + return implode(filter_xss_admin($this->options['separator']), $items); + } + else { + return theme('item_list', + array( + 'items' => $items, + 'title' => NULL, + 'type' => $this->options['multi_type'] + )); + } + } + } + + /** + * Return an array of items for the field. + */ + function get_items($values) { + $original_entity = $this->get_entity($values); + if (!$original_entity) { + return array(); + } + $entity = $this->process_entity($original_entity); + if (!$entity) { + return array(); + } + + $display = array( + 'type' => $this->options['type'], + 'settings' => $this->options['settings'], + 'label' => 'hidden', + // Pass the View object in the display so that fields can act on it. + 'views_view' => $this->view, + 'views_field' => $this, + 'views_row_id' => $this->view->row_index, + ); + + $entity_type = $entity->entityType(); + $langcode = $this->field_langcode($entity_type, $entity); + $render_array = field_view_field($entity_type, $entity, $this->definition['field_name'], $display, $langcode); + + $items = array(); + if ($this->options['field_api_classes']) { + // Make a copy. + $array = $render_array; + return array(array('rendered' => drupal_render($render_array))); + } + + foreach (element_children($render_array) as $count) { + $items[$count]['rendered'] = $render_array[$count]; + // field_view_field() adds an #access property to the render array that + // determines whether or not the current user is allowed to view the + // field in the context of the current entity. We need to respect this + // parameter when we pull out the children of the field array for + // rendering. + if (isset($render_array['#access'])) { + $items[$count]['rendered']['#access'] = $render_array['#access']; + } + // Only add the raw field items (for use in tokens) if the current user + // has access to view the field content. + if ((!isset($items[$count]['rendered']['#access']) || $items[$count]['rendered']['#access']) && !empty($render_array['#items'][$count])) { + $items[$count]['raw'] = $render_array['#items'][$count]; + } + } + return $items; + } + + /** + * Process an entity before using it for rendering. + * + * Replaces values with aggregated values if aggregation is enabled. + * Takes delta settings into account (@todo remove in #1758616). + * + * @param $entity + * The entity to be processed. + * + * @return + * TRUE if the processing completed successfully, otherwise FALSE. + */ + function process_entity($entity) { + $processed_entity = clone $entity; + + $entity_type = $entity->entityType(); + $langcode = $this->field_langcode($entity_type, $processed_entity); + // If we are grouping, copy our group fields into the cloned entity. + // It's possible this will cause some weirdness, but there's only + // so much we can hope to do. + if (!empty($this->group_fields)) { + // first, test to see if we have a base value. + $base_value = array(); + // Note: We would copy original values here, but it can cause problems. + // For example, text fields store cached filtered values as + // 'safe_value' which doesn't appear anywhere in the field definition + // so we can't affect it. Other side effects could happen similarly. + $data = FALSE; + foreach ($this->group_fields as $field_name => $column) { + if (property_exists($values, $this->aliases[$column])) { + $base_value[$field_name] = $values->{$this->aliases[$column]}; + if (isset($base_value[$field_name])) { + $data = TRUE; + } + } + } + + // If any of our aggregated fields have data, fake it: + if ($data) { + // Now, overwrite the original value with our aggregated value. + // This overwrites it so there is always just one entry. + $processed_entity->{$this->definition['field_name']}[$langcode] = array($base_value); + } + else { + $processed_entity->{$this->definition['field_name']}[$langcode] = array(); + } + } + + // The field we are trying to display doesn't exist on this entity. + if (!isset($processed_entity->{$this->definition['field_name']})) { + return FALSE; + } + + // We are supposed to show only certain deltas. + if ($this->limit_values && !empty($processed_entity->{$this->definition['field_name']})) { + $all_values = !empty($processed_entity->{$this->definition['field_name']}[$langcode]) ? $processed_entity->{$this->definition['field_name']}[$langcode] : array(); + if ($this->options['delta_reversed']) { + $all_values = array_reverse($all_values); + } + + // Offset is calculated differently when row grouping for a field is + // not enabled. Since there are multiple rows, the delta needs to be + // taken into account, so that different values are shown per row. + if (!$this->options['group_rows'] && isset($this->aliases['delta']) && isset($values->{$this->aliases['delta']})) { + $delta_limit = 1; + $offset = $values->{$this->aliases['delta']}; + } + // Single fields don't have a delta available so choose 0. + elseif (!$this->options['group_rows'] && !$this->multiple) { + $delta_limit = 1; + $offset = 0; + } + else { + $delta_limit = $this->options['delta_limit']; + $offset = intval($this->options['delta_offset']); + + // We should only get here in this case if there's an offset, and + // in that case we're limiting to all values after the offset. + if ($delta_limit == 'all') { + $delta_limit = count($all_values) - $offset; + } + } + + // Determine if only the first and last values should be shown + $delta_first_last = $this->options['delta_first_last']; + + $new_values = array(); + for ($i = 0; $i < $delta_limit; $i++) { + $new_delta = $offset + $i; + + if (isset($all_values[$new_delta])) { + // If first-last option was selected, only use the first and last values + if (!$delta_first_last + // Use the first value. + || $new_delta == $offset + // Use the last value. + || $new_delta == ($delta_limit + $offset - 1)) { + $new_values[] = $all_values[$new_delta]; + } + } + } + $processed_entity->{$this->definition['field_name']}[$langcode] = $new_values; + } + + return $processed_entity; + } + + function render_item($count, $item) { + return render($item['rendered']); + } + + function document_self_tokens(&$tokens) { + $field = $this->field_info; + foreach ($field['columns'] as $id => $column) { + $tokens['[' . $this->options['id'] . '-' . $id . ']'] = t('Raw @column', array('@column' => $id)); + } + } + + function add_self_tokens(&$tokens, $item) { + $field = $this->field_info; + foreach ($field['columns'] as $id => $column) { + // Use filter_xss_admin because it's user data and we can't be sure it is safe. + // We know nothing about the data, though, so we can't really do much else. + + if (isset($item['raw'])) { + // If $item['raw'] is an array then we can use as is, if it's an object + // we cast it to an array, if it's neither, we can't use it. + $raw = is_array($item['raw']) ? $item['raw'] : + (is_object($item['raw']) ? (array)$item['raw'] : NULL); + } + if (isset($raw) && isset($raw[$id]) && is_scalar($raw[$id])) { + $tokens['[' . $this->options['id'] . '-' . $id . ']'] = filter_xss_admin($raw[$id]); + } + else { + // Take sure that empty values are replaced as well. + $tokens['[' . $this->options['id'] . '-' . $id . ']'] = ''; + } + } + } + + /** + * Return the language code of the language the field should be displayed in, + * according to the settings. + */ + function field_langcode($entity_type, $entity) { + if (field_is_translatable($entity_type, $this->field_info)) { + $default_langcode = language_default()->langcode; + $langcode = str_replace(array('***CURRENT_LANGUAGE***', '***DEFAULT_LANGUAGE***'), + array(drupal_container()->get(LANGUAGE_TYPE_CONTENT)->langcode, $default_langcode), + $this->view->display_handler->options['field_language']); + + // Give the Field Language API a chance to fallback to a different language + // (or LANGUAGE_NOT_SPECIFIED), in case the field has no data for the selected language. + // field_view_field() does this as well, but since the returned language code + // is used before calling it, the fallback needs to happen explicitly. + $langcode = field_language($entity_type, $entity, $this->field_info['field_name'], $langcode); + + return $langcode; + } + else { + return LANGUAGE_NOT_SPECIFIED; + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/views/filter/FieldList.php b/core/modules/field/lib/Drupal/field/Plugin/views/filter/FieldList.php new file mode 100644 index 0000000..6dbd16a --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/views/filter/FieldList.php @@ -0,0 +1,30 @@ +definition['field_name']); + $this->value_options = list_allowed_values($field); + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/views/relationship/EntityReverse.php b/core/modules/field/lib/Drupal/field/Plugin/views/relationship/EntityReverse.php new file mode 100644 index 0000000..75ad533 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/views/relationship/EntityReverse.php @@ -0,0 +1,97 @@ +field_info = field_info_field($this->definition['field_name']); + } + + /** + * Called to implement a relationship in a query. + */ + public function query() { + $this->ensureMyTable(); + // First, relate our base table to the current base table to the + // field, using the base table's id field to the field's column. + $views_data = views_fetch_data($this->table); + $left_field = $views_data['table']['base']['field']; + + $first = array( + 'left_table' => $this->tableAlias, + 'left_field' => $left_field, + 'table' => $this->definition['field table'], + 'field' => $this->definition['field field'], + 'adjusted' => TRUE + ); + if (!empty($this->options['required'])) { + $first['type'] = 'INNER'; + } + + if (!empty($this->definition['join_extra'])) { + $first['extra'] = $this->definition['join_extra']; + } + + if (!empty($def['join_id'])) { + $id = $def['join_id']; + } + else { + $id = 'standard'; + } + $first_join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $first); + + + $this->first_alias = $this->query->add_table($this->definition['field table'], $this->relationship, $first_join); + + // Second, relate the field table to the entity specified using + // the entity id on the field table and the entity's id field. + $second = array( + 'left_table' => $this->first_alias, + 'left_field' => 'entity_id', + 'table' => $this->definition['base'], + 'field' => $this->definition['base field'], + 'adjusted' => TRUE + ); + + if (!empty($this->options['required'])) { + $second['type'] = 'INNER'; + } + + if (!empty($def['join_id'])) { + $id = $def['join_id']; + } + else { + $id = 'standard'; + } + $second_join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $second); + $second_join->adjusted = TRUE; + + // use a short alias for this: + $alias = $this->definition['field_name'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $second_join, $this->definition['base'], $this->relationship); + } + +} diff --git a/core/modules/file/file.views.inc b/core/modules/file/file.views.inc new file mode 100644 index 0000000..2871043 --- /dev/null +++ b/core/modules/file/file.views.inc @@ -0,0 +1,534 @@ + 'fid', + 'title' => t('File'), + 'help' => t("Files maintained by Drupal and various modules."), + 'defaults' => array( + 'field' => 'filename' + ), + ); + $data['file_managed']['table']['entity type'] = 'file'; + + // fid + $data['file_managed']['fid'] = array( + 'title' => t('File ID'), + 'help' => t('The ID of the file.'), + 'field' => array( + 'id' => 'file', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'file_fid', + 'name field' => 'filename', // the field to display in the summary. + 'numeric' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // filename + $data['file_managed']['filename'] = array( + 'title' => t('Name'), + 'help' => t('The name of the file.'), + 'field' => array( + 'id' => 'file', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // uri + $data['file_managed']['uri'] = array( + 'title' => t('Path'), + 'help' => t('The path of the file.'), + 'field' => array( + 'id' => 'file_uri', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // filemime + $data['file_managed']['filemime'] = array( + 'title' => t('Mime type'), + 'help' => t('The mime type of the file.'), + 'field' => array( + 'id' => 'file_filemime', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // extension + $data['file_managed']['extension'] = array( + 'title' => t('Extension'), + 'help' => t('The extension of the file.'), + 'real field' => 'filename', + 'field' => array( + 'id' => 'file_extension', + 'click sortable' => FALSE, + ), + ); + + // filesize + $data['file_managed']['filesize'] = array( + 'title' => t('Size'), + 'help' => t('The size of the file.'), + 'field' => array( + 'id' => 'file_size', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'numeric', + ), + ); + + // status + $data['file_managed']['status'] = array( + 'title' => t('Status'), + 'help' => t('The status of the file.'), + 'field' => array( + 'id' => 'file_status', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'file_status', + ), + ); + + // timestamp field + $data['file_managed']['timestamp'] = array( + 'title' => t('Upload date'), + 'help' => t('The date the file was uploaded.'), + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date' + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + // uid + $data['file_managed']['uid'] = array( + 'title' => t('User who uploaded'), + 'help' => t('The user that uploaded the file.'), + 'relationship' => array( + 'title' => t('User who uploaded'), + 'label' => t('User who uploaded'), + 'base' => 'users', + 'base field' => 'uid', + ), + ); + + // ---------------------------------------------------------------------- + // file_usage table + + $data['file_usage']['table']['group'] = t('File Usage'); + + // Provide field-type-things to several base tables; on the core files table ("file_managed") so + // that we can create relationships from files to entities, and then on each core entity type base + // table so that we can provide general relationships between entities and files. + $data['file_usage']['table']['join'] = array( + // Link ourself to the {file_managed} table so we can provide file->entity relationships. + 'file_managed' => array( + 'field' => 'fid', + 'left_field' => 'fid', + ), + // Link ourself to the {node} table so we can provide node->file relationships. + 'node' => array( + 'field' => 'id', + 'left_field' => 'nid', + 'extra' => array(array('field' => 'type', 'value' => 'node')), + ), + // Link ourself to the {users} table so we can provide user->file relationships. + 'users' => array( + 'field' => 'id', + 'left_field' => 'uid', + 'extra' => array(array('field' => 'type', 'value' => 'user')), + ), + // Link ourself to the {comment} table so we can provide comment->file relationships. + 'comment' => array( + 'field' => 'id', + 'left_field' => 'cid', + 'extra' => array(array('field' => 'type', 'value' => 'comment')), + ), + // Link ourself to the {taxonomy_term_data} table so we can provide taxonomy_term->file relationships. + 'taxonomy_term_data' => array( + 'field' => 'id', + 'left_field' => 'tid', + 'extra' => array(array('field' => 'type', 'value' => 'taxonomy_term')), + ), + // Link ourself to the {taxonomy_vocabulary} table so we can provide taxonomy_vocabulary->file relationships. + 'taxonomy_vocabulary' => array( + 'field' => 'id', + 'left_field' => 'vid', + 'extra' => array(array('field' => 'type', 'value' => 'taxonomy_vocabulary')), + ), + ); + + // Provide a relationship between the files table and each entity type, and between each entity + // type and the files table. Entity->file relationships are type-restricted in the joins + // declared above, and file->entity relationships are type-restricted in the relationship + // declarations below. + + // Relationships between files and nodes. + $data['file_usage']['file_to_node'] = array( + 'title' => t('Content'), + 'help' => t('Content that is associated with this file, usually because this file is in a field on the content.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('Content'), + 'label' => t('Content'), + 'base' => 'node', + 'base field' => 'nid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'node')), + ), + ); + $data['file_usage']['node_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this node, usually because it is in a field on the node.'), + // Only provide this field/relationship/etc. when the 'node' base table is present. + 'skip base' => array('file_managed', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Relationships between files and users. + $data['file_usage']['file_to_user'] = array( + 'title' => t('User'), + 'help' => t('A user that is associated with this file, usually because this file is in a field on the user.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('User'), + 'label' => t('User'), + 'base' => 'users', + 'base field' => 'uid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'user')), + ), + ); + $data['file_usage']['user_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this user, usually because it is in a field on the user.'), + // Only provide this field/relationship/etc. when the 'users' base table is present. + 'skip base' => array('file_managed', 'node', 'node_revision', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Relationships between files and comments. + $data['file_usage']['file_to_comment'] = array( + 'title' => t('Comment'), + 'help' => t('A comment that is associated with this file, usually because this file is in a field on the comment.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('Comment'), + 'label' => t('Comment'), + 'base' => 'comment', + 'base field' => 'cid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'comment')), + ), + ); + $data['file_usage']['comment_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this comment, usually because it is in a field on the comment.'), + // Only provide this field/relationship/etc. when the 'comment' base table is present. + 'skip base' => array('file_managed', 'node', 'node_revision', 'users', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Relationships between files and taxonomy_terms. + $data['file_usage']['file_to_taxonomy_term'] = array( + 'title' => t('Taxonomy Term'), + 'help' => t('A taxonomy term that is associated with this file, usually because this file is in a field on the taxonomy term.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('Taxonomy Term'), + 'label' => t('Taxonomy Term'), + 'base' => 'taxonomy_term_data', + 'base field' => 'tid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'taxonomy_term')), + ), + ); + $data['file_usage']['taxonomy_term_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this taxonomy term, usually because it is in a field on the taxonomy term.'), + // Only provide this field/relationship/etc. when the 'taxonomy_term_data' base table is present. + 'skip base' => array('file_managed', 'node', 'node_revision', 'users', 'comment', 'taxonomy_vocabulary'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Relationships between files and taxonomy_vocabulary items. + $data['file_usage']['file_to_taxonomy_vocabulary'] = array( + 'title' => t('Taxonomy Vocabulary'), + 'help' => t('A taxonomy vocabulary that is associated with this file, usually because this file is in a field on the taxonomy vocabulary.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('Taxonomy Vocabulary'), + 'label' => t('Taxonomy Vocabulary'), + 'base' => 'taxonomy_vocabulary', + 'base field' => 'vid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'taxonomy_vocabulary')), + ), + ); + $data['file_usage']['taxonomy_vocabulary_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this taxonomy vocabulary, usually because it is in a field on the taxonomy vocabulary.'), + // Only provide this field/relationship/etc. when the 'taxonomy_vocabulary' base table is present. + 'skip base' => array('file_managed', 'node', 'node_revision', 'users', 'comment', 'taxonomy_term_data'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Provide basic fields from the {file_usage} table to all of the base tables we've declared + // joins to (because there is no 'skip base' property on these fields). + $data['file_usage']['module'] = array( + 'title' => t('Module'), + 'help' => t('The module managing this file relationship.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + $data['file_usage']['type'] = array( + 'title' => t('Entity type'), + 'help' => t('The type of entity that is related to the file.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + $data['file_usage']['id'] = array( + 'title' => t('Entity ID'), + 'help' => t('The ID of the entity that is related to the file.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + $data['file_usage']['count'] = array( + 'title' => t('Use count'), + 'help' => t('The number of times the file is used by this entity.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + return $data; +} + +/** + * Implements hook_field_views_data(). + * + * Views integration for file fields. Adds a file relationship to the default + * field data. + * + * @see field_views_field_default_views_data() + */ +function file_field_views_data($field) { + $data = field_views_field_default_views_data($field); + foreach ($data as $table_name => $table_data) { + // Add the relationship only on the fid field. + $data[$table_name][$field['field_name'] . '_fid']['relationship'] = array( + 'id' => 'standard', + 'base' => 'file_managed', + 'entity type' => 'file', + 'base field' => 'fid', + 'label' => t('file from !field_name', array('!field_name' => $field['field_name'])), + ); + } + + return $data; +} + +/** + * Implements hook_field_views_data_views_data_alter(). + * + * Views integration to provide reverse relationships on file fields. + */ +function file_field_views_data_views_data_alter(&$data, $field) { + foreach ($field['bundles'] as $entity_type => $bundles) { + $entity_info = entity_get_info($entity_type); + $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; + + list($label, $all_labels) = field_views_field_label($field['field_name']); + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + + $data['file_managed'][$pseudo_field_name]['relationship'] = array( + 'title' => t('@entity using @field', array('@entity' => $entity, '@field' => $label)), + 'help' => t('Relate each @entity with a @field set to the file.', array('@entity' => $entity, '@field' => $label)), + 'id' => 'entity_reverse', + 'field_name' => $field['field_name'], + 'field table' => _field_sql_storage_tablename($field), + 'field field' => $field['field_name'] . '_fid', + 'base' => $entity_info['base table'], + 'base field' => $entity_info['entity keys']['id'], + 'label' => t('!field_name', array('!field_name' => $field['field_name'])), + 'join_extra' => array( + 0 => array( + 'field' => 'entity_type', + 'value' => $entity_type, + ), + 1 => array( + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ), + ), + ); + } +} + +function _views_file_status($choice = NULL) { + $status = array( + 0 => t('Temporary'), + FILE_STATUS_PERMANENT => t('Permanent'), + ); + + if (isset($choice)) { + return isset($status[$choice]) ? $status[$choice] : t('Unknown'); + } + + return $status; +} diff --git a/core/modules/file/lib/Drupal/file/Plugin/views/argument/Fid.php b/core/modules/file/lib/Drupal/file/Plugin/views/argument/Fid.php new file mode 100644 index 0000000..16de71b --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Plugin/views/argument/Fid.php @@ -0,0 +1,40 @@ +fields('f', array('filename')) + ->condition('fid', $this->value) + ->execute() + ->fetchCol(); + foreach ($titles as &$title) { + $title = check_plain($title); + } + return $titles; + } + +} diff --git a/core/modules/file/lib/Drupal/file/Plugin/views/field/Extension.php b/core/modules/file/lib/Drupal/file/Plugin/views/field/Extension.php new file mode 100644 index 0000000..012b895 --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Plugin/views/field/Extension.php @@ -0,0 +1,32 @@ +get_value($values); + if (preg_match('/\.([^\.]+)$/', $value, $match)) { + return $this->sanitizeValue($match[1]); + } + } + +} diff --git a/core/modules/file/lib/Drupal/file/Plugin/views/field/File.php b/core/modules/file/lib/Drupal/file/Plugin/views/field/File.php new file mode 100644 index 0000000..facf8a5 --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Plugin/views/field/File.php @@ -0,0 +1,74 @@ +additional_fields['uri'] = 'uri'; + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_to_file'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to file option + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_file'] = array( + '#title' => t('Link this field to download the file'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_file']), + ); + parent::buildOptionsForm($form, $form_state); + } + + /** + * Render whatever the data is as a link to the file. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + if (!empty($this->options['link_to_file']) && $data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = file_create_url($this->get_value($values, 'uri')); + } + + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/file/lib/Drupal/file/Plugin/views/field/FileMime.php b/core/modules/file/lib/Drupal/file/Plugin/views/field/FileMime.php new file mode 100644 index 0000000..74cec34 --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Plugin/views/field/FileMime.php @@ -0,0 +1,49 @@ + FALSE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['filemime_image'] = array( + '#title' => t('Display an icon representing the file type, instead of the MIME text (such as "image/jpeg")'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['filemime_image']), + ); + parent::buildOptionsForm($form, $form_state); + } + + function render($values) { + $data = $values->{$this->field_alias}; + if (!empty($this->options['filemime_image']) && $data !== NULL && $data !== '') { + $fake_file = (object) array('filemime' => $data); + $data = theme('file_icon', array('file' => $fake_file)); + } + + return $this->render_link($data, $values); + } + +} diff --git a/core/modules/file/lib/Drupal/file/Plugin/views/field/Status.php b/core/modules/file/lib/Drupal/file/Plugin/views/field/Status.php new file mode 100644 index 0000000..afb773a --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Plugin/views/field/Status.php @@ -0,0 +1,30 @@ +get_value($values); + return _views_file_status($value); + } + +} diff --git a/core/modules/file/lib/Drupal/file/Plugin/views/field/Uri.php b/core/modules/file/lib/Drupal/file/Plugin/views/field/Uri.php new file mode 100644 index 0000000..78e07c0 --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Plugin/views/field/Uri.php @@ -0,0 +1,46 @@ + FALSE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['file_download_path'] = array( + '#title' => t('Display download path instead of file storage URI'), + '#description' => t('This will provide the full download URL rather than the internal filestream address.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['file_download_path']), + ); + parent::buildOptionsForm($form, $form_state); + } + + function render($values) { + $data = $values->{$this->field_alias}; + if (!empty($this->options['file_download_path']) && $data !== NULL && $data !== '') { + $data = file_create_url($data); + } + return $this->render_link($data, $values); + } + +} diff --git a/core/modules/file/lib/Drupal/file/Plugin/views/filter/Status.php b/core/modules/file/lib/Drupal/file/Plugin/views/filter/Status.php new file mode 100644 index 0000000..4a1c737 --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Plugin/views/filter/Status.php @@ -0,0 +1,31 @@ +value_options)) { + $this->value_options = _views_file_status(); + } + } + +} diff --git a/core/modules/file/lib/Drupal/file/Plugin/views/wizard/File.php b/core/modules/file/lib/Drupal/file/Plugin/views/wizard/File.php new file mode 100644 index 0000000..00d451f --- /dev/null +++ b/core/modules/file/lib/Drupal/file/Plugin/views/wizard/File.php @@ -0,0 +1,74 @@ + 'uri', + 'table' => 'file_managed', + 'field' => 'uri', + 'exclude' => TRUE, + 'file_download_path' => TRUE + ); + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::default_display_options(). + */ + protected function default_display_options() { + $display_options = parent::default_display_options(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + /* Field: File: Name */ + $display_options['fields']['filename']['id'] = 'filename'; + $display_options['fields']['filename']['table'] = 'file_managed'; + $display_options['fields']['filename']['field'] = 'filename'; + $display_options['fields']['filename']['label'] = ''; + $display_options['fields']['filename']['alter']['alter_text'] = 0; + $display_options['fields']['filename']['alter']['make_link'] = 0; + $display_options['fields']['filename']['alter']['absolute'] = 0; + $display_options['fields']['filename']['alter']['trim'] = 0; + $display_options['fields']['filename']['alter']['word_boundary'] = 0; + $display_options['fields']['filename']['alter']['ellipsis'] = 0; + $display_options['fields']['filename']['alter']['strip_tags'] = 0; + $display_options['fields']['filename']['alter']['html'] = 0; + $display_options['fields']['filename']['hide_empty'] = 0; + $display_options['fields']['filename']['empty_zero'] = 0; + $display_options['fields']['filename']['link_to_file'] = 1; + + return $display_options; + } + +} diff --git a/core/modules/filter/filter.views.inc b/core/modules/filter/filter.views.inc new file mode 100644 index 0000000..ca708c4 --- /dev/null +++ b/core/modules/filter/filter.views.inc @@ -0,0 +1,32 @@ + array( + 'left_field' => 'format', + 'field' => 'format', + ), + 'node' => array( + 'left_table' => 'node_revision', + 'left_field' => 'format', + 'field' => 'format', + ), + ); + + return $data; +} diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/views/field/FormatName.php b/core/modules/filter/lib/Drupal/filter/Plugin/views/field/FormatName.php new file mode 100644 index 0000000..82daaca --- /dev/null +++ b/core/modules/filter/lib/Drupal/filter/Plugin/views/field/FormatName.php @@ -0,0 +1,53 @@ +additional_fields['name'] = array('table' => 'filter_formats', 'field' => 'name'); + } + + public function query() { + $this->ensureMyTable(); + $this->add_additional_fields(); + } + + function render($values) { + $format_name = $this->get_value($values, 'name'); + if (!$format_name) { + // Default or invalid input format. + // filter_formats() will reliably return the default format even if the + // current user is unprivileged. + $format = filter_formats(filter_default_format()); + return $this->sanitizeValue($format->name); + } + return $this->sanitizeValue($format_name); + } + +} diff --git a/core/modules/image/image.views.inc b/core/modules/image/image.views.inc new file mode 100644 index 0000000..43c04d6 --- /dev/null +++ b/core/modules/image/image.views.inc @@ -0,0 +1,72 @@ + $table_data) { + // Add the relationship only on the fid field. + $data[$table_name][$field['field_name'] . '_fid']['relationship'] = array( + 'id' => 'standard', + 'base' => 'file_managed', + 'base field' => 'fid', + 'label' => t('image from !field_name', array('!field_name' => $field['field_name'])), + ); + } + + return $data; +} + +/** + * Implements hook_field_views_data_views_data_alter(). + * + * Views integration to provide reverse relationships on image fields. + */ +function image_field_views_data_views_data_alter(&$data, $field) { + foreach ($field['bundles'] as $entity_type => $bundles) { + $entity_info = entity_get_info($entity_type); + $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; + + list($label, $all_labels) = field_views_field_label($field['field_name']); + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + + $data['file_managed'][$pseudo_field_name]['relationship'] = array( + 'title' => t('@entity using @field', array('@entity' => $entity, '@field' => $label)), + 'help' => t('Relate each @entity with a @field set to the image.', array('@entity' => $entity, '@field' => $label)), + 'id' => 'entity_reverse', + 'field_name' => $field['field_name'], + 'field table' => _field_sql_storage_tablename($field), + 'field field' => $field['field_name'] . '_fid', + 'base' => $entity_info['base table'], + 'base field' => $entity_info['entity keys']['id'], + 'label' => t('!field_name', array('!field_name' => $field['field_name'])), + 'join_extra' => array( + 0 => array( + 'field' => 'entity_type', + 'value' => $entity_type, + ), + 1 => array( + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ), + ), + ); + } +} diff --git a/core/modules/language/language.views.inc b/core/modules/language/language.views.inc new file mode 100644 index 0000000..7521d52 --- /dev/null +++ b/core/modules/language/language.views.inc @@ -0,0 +1,91 @@ + 'langcode', + 'title' => t('Language'), + 'help' => t('A language used in drupal.'), + ); + + $data['language']['langcode'] = array( + 'title' => t('Language code'), + 'help' => t("Language code, e.g. 'de' or 'en-US'."), + 'field' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string' + ), + 'argument' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + $data['language']['name'] = array( + 'title' => t('Language name'), + 'help' => t("Language name, e.g. 'German' or 'English'."), + 'field' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string' + ), + 'argument' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + $data['language']['direction'] = array( + 'title' => t('Direction'), + 'help' => t('Direction of language (Left-to-Right = 0, Right-to-Left = 1).'), + 'field' => array( + 'id' => 'numeric', + ), + 'filter' => array( + 'id' => 'numeric' + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + $data['language']['weight'] = array( + 'title' => t('Weight'), + 'help' => t('Weight, used in lists of languages.'), + 'field' => array( + 'id' => 'numeric', + ), + 'filter' => array( + 'id' => 'numeric' + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + return $data; +} diff --git a/core/modules/language/lib/Drupal/language/Plugin/views/argument/Language.php b/core/modules/language/lib/Drupal/language/Plugin/views/argument/Language.php new file mode 100644 index 0000000..c24ee16 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Plugin/views/argument/Language.php @@ -0,0 +1,46 @@ +language($data->{$this->name_alias}); + } + + /** + * Override the behavior of title(). Get the user friendly version + * of the language. + */ + function title() { + return $this->language($this->argument); + } + + function language($langcode) { + $languages = views_language_list(); + return isset($languages[$langcode]) ? $languages[$langcode] : t('Unknown language'); + } + +} diff --git a/core/modules/language/lib/Drupal/language/Plugin/views/field/Language.php b/core/modules/language/lib/Drupal/language/Plugin/views/field/Language.php new file mode 100644 index 0000000..e597027 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Plugin/views/field/Language.php @@ -0,0 +1,50 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['native_language'] = array( + '#title' => t('Native language'), + '#type' => 'checkbox', + '#default_value' => $this->options['native_language'], + '#description' => t('If enabled, the native name of the language will be displayed'), + ); + } + + function render($values) { + // @todo: Drupal Core dropped native language until config translation is + // ready, see http://drupal.org/node/1616594. + $value = $this->get_value($values); + $language = language_load($value); + return $language ? $language->name : ''; + } + +} diff --git a/core/modules/language/lib/Drupal/language/Plugin/views/filter/Language.php b/core/modules/language/lib/Drupal/language/Plugin/views/filter/Language.php new file mode 100644 index 0000000..ab836b3 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Plugin/views/filter/Language.php @@ -0,0 +1,38 @@ +value_options)) { + $this->value_title = t('Language'); + $languages = array( + '***CURRENT_LANGUAGE***' => t("Current user's language"), + '***DEFAULT_LANGUAGE***' => t("Default site language"), + LANGUAGE_NOT_SPECIFIED => t('No language') + ); + $languages = array_merge($languages, views_language_list()); + $this->value_options = $languages; + } + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/Plugin/views/field/LinkEdit.php b/core/modules/locale/lib/Drupal/locale/Plugin/views/field/LinkEdit.php new file mode 100644 index 0000000..871cd2a --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Plugin/views/field/LinkEdit.php @@ -0,0 +1,77 @@ +additional_fields['lid'] = 'lid'; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['text'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + parent::buildOptionsForm($form, $form_state); + } + + public function query() { + $this->ensureMyTable(); + $this->add_additional_fields(); + } + + public function access() { + // Ensure user has access to edit translations. + return user_access('translate interface'); + } + + function render($values) { + $value = $this->get_value($values, 'lid'); + return $this->render_link($this->sanitizeValue($value), $values); + } + + function render_link($data, $values) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'admin/build/translate/edit/' . $data; + $this->options['alter']['query'] = drupal_get_destination(); + + return $text; + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/Plugin/views/filter/Version.php b/core/modules/locale/lib/Drupal/locale/Plugin/views/filter/Version.php new file mode 100644 index 0000000..2a5f153 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Plugin/views/filter/Version.php @@ -0,0 +1,42 @@ +value_options)) { + $this->value_title = t('Version'); + // Enable filtering by the current installed Drupal version. + $versions = array('***CURRENT_VERSION***' => t('Current installed version')); + // Uses db_query() rather than db_select() because the query is static and + // does not include any variables. + $result = db_query('SELECT DISTINCT(version) FROM {locales_source} ORDER BY version'); + foreach ($result as $row) { + if (!empty($row->version)) { + $versions[$row->version] = $row->version; + } + } + $this->value_options = $versions; + } + } + +} diff --git a/core/modules/locale/locale.views.inc b/core/modules/locale/locale.views.inc new file mode 100644 index 0000000..934a077 --- /dev/null +++ b/core/modules/locale/locale.views.inc @@ -0,0 +1,193 @@ + 'lid', + 'title' => t('Locale source'), + 'help' => t('A source string for translation, in English or the default site language.'), + ); + + // lid + $data['locales_source']['lid'] = array( + 'title' => t('LID'), + 'help' => t('The ID of the source string.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + 'numeric' => TRUE, + 'validate type' => 'lid', + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // location + $data['locales_source']['location'] = array( + 'group' => t('Locale source'), + 'title' => t('Location'), + 'help' => t('A description of the location or context of the string.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // Source field + $data['locales_source']['source'] = array( + 'group' => t('Locale source'), + 'title' => t('Source'), + 'help' => t('The full original string.'), + 'field' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + // Source field + $data['locales_source']['context'] = array( + 'group' => t('Locale source'), + 'title' => t('Context'), + 'help' => t('The context this string applies to.'), + 'field' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + // Version field + $data['locales_source']['version'] = array( + 'group' => t('Locale source'), + 'title' => t('Version'), + 'help' => t('The version of Drupal core that this string is for.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'locale_version', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + $data['locales_source']['edit_lid'] = array( + 'group' => t('Locale source'), + 'field' => array( + 'title' => t('Edit link'), + 'help' => t('Provide a simple link to edit the translations.'), + 'id' => 'locale_link_edit', + ), + ); + + // ---------------------------------------------------------------------- + // Locales target table + + // Define the base group of this table. Fields that don't + // have a group defined will go into this field by default. + $data['locales_target']['table']['group'] = t('Locale target'); + + // Join information + $data['locales_target']['table']['join'] = array( + 'locales_source' => array( + 'left_field' => 'lid', + 'field' => 'lid', + ), + ); + + // Translation field + $data['locales_target']['translation'] = array( + 'group' => t('Locale target'), + 'title' => t('Translation'), + 'help' => t('The full translation string.'), + 'field' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + // Language field + $data['locales_target']['language'] = array( + 'group' => t('Locale target'), + 'title' => t('Language'), + 'help' => t('The language this translation is in.'), + 'field' => array( + 'id' => 'locale_language', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'locale_language', + ), + 'argument' => array( + 'id' => 'locale_language', + ), + ); + + $data['locales_target']['plid'] = array( + 'group' => t('Locale target'), + 'title' => t('Singular LID'), + 'help' => t('The ID of the parent translation.'), + 'field' => array( + 'id' => 'standard', + ), + ); + + // Plural + $data['locales_target']['plural'] = array( + 'group' => t('Locale target'), + 'title' => t('Plural'), + 'help' => t('Whether or not the translation is plural.'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Plural'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + return $data; +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedDay.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedDay.php new file mode 100644 index 0000000..1601302 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedDay.php @@ -0,0 +1,55 @@ +formula = $this->extractSQL('DAY'); + return parent::get_formula(); + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $day = str_pad($data->{$this->name_alias}, 2, '0', STR_PAD_LEFT); + // strtotime respects server timezone, so we need to set the time fixed as utc time + return format_date(strtotime("2005" . "05" . $day . " 00:00:00 UTC"), 'custom', $this->definition['format'], 'UTC'); + } + + /** + * Provide a link to the next level of the view + */ + function title() { + $day = str_pad($this->argument, 2, '0', STR_PAD_LEFT); + return format_date(strtotime("2005" . "05" . $day . " 00:00:00 UTC"), 'custom', $this->definition['format'], 'UTC'); + } + + function summary_argument($data) { + // Make sure the argument contains leading zeroes. + return str_pad($data->{$this->base_alias}, 2, '0', STR_PAD_LEFT); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedFullDate.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedFullDate.php new file mode 100644 index 0000000..17f53ef --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedFullDate.php @@ -0,0 +1,48 @@ +formula = $this->getSQLFormat($this->definition['arg_format']); + return parent::get_formula(); + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $created = $data->{$this->name_alias}; + return format_date(strtotime($created . " 00:00:00 UTC"), 'custom', $this->definition['format'], 'UTC'); + } + + /** + * Provide a link to the next level of the view + */ + function title() { + return format_date(strtotime($this->argument . " 00:00:00 UTC"), 'custom', $this->definition['format'], 'UTC'); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedMonth.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedMonth.php new file mode 100644 index 0000000..f521e9f --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedMonth.php @@ -0,0 +1,54 @@ +formula = $this->extractSQL('MONTH'); + return parent::get_formula(); + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $month = str_pad($data->{$this->name_alias}, 2, '0', STR_PAD_LEFT); + return format_date(strtotime("2005" . $month . "15" . " 00:00:00 UTC" ), 'custom', $this->definition['format'], 'UTC'); + } + + /** + * Provide a link to the next level of the view + */ + function title() { + $month = str_pad($this->argument, 2, '0', STR_PAD_LEFT); + return format_date(strtotime("2005" . $month . "15" . " 00:00:00 UTC"), 'custom', $this->definition['format'], 'UTC'); + } + + function summary_argument($data) { + // Make sure the argument contains leading zeroes. + return str_pad($data->{$this->base_alias}, 2, '0', STR_PAD_LEFT); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedWeek.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedWeek.php new file mode 100644 index 0000000..1b75912 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedWeek.php @@ -0,0 +1,40 @@ +formula = $this->extractSQL('WEEK'); + return parent::get_formula(); + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $created = $data->{$this->name_alias}; + return t('Week @week', array('@week' => $created)); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedYear.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedYear.php new file mode 100644 index 0000000..0a666c8 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedYear.php @@ -0,0 +1,32 @@ +formula = $this->extractSQL('YEAR'); + return parent::get_formula(); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedYearMonth.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedYearMonth.php new file mode 100644 index 0000000..a024118 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/CreatedYearMonth.php @@ -0,0 +1,48 @@ +formula = $this->getSQLFormat($this->definition['arg_format']); + return parent::get_formula(); + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $created = $data->{$this->name_alias}; + return format_date(strtotime($created . "15" . " 00:00:00 UTC"), 'custom', $this->definition['format'], 'UTC'); + } + + /** + * Provide a link to the next level of the view + */ + function title() { + return format_date(strtotime($this->argument . "15" . " 00:00:00 UTC"), 'custom', $this->definition['format'], 'UTC'); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/Nid.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/Nid.php new file mode 100644 index 0000000..bda334d --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/Nid.php @@ -0,0 +1,36 @@ +value); + foreach ($nodes as $node) { + $titles[] = check_plain($node->label()); + } + return $titles; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/Type.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/Type.php new file mode 100644 index 0000000..1a11b22 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/Type.php @@ -0,0 +1,47 @@ +node_type($data->{$this->name_alias}); + } + + /** + * Override the behavior of title(). Get the user friendly version of the + * node type. + */ + function title() { + return $this->node_type($this->argument); + } + + function node_type($type) { + $output = node_type_get_label($type); + if (empty($output)) { + $output = t('Unknown content type'); + } + return check_plain($output); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/UidRevision.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/UidRevision.php new file mode 100644 index 0000000..d93611e --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/UidRevision.php @@ -0,0 +1,30 @@ +ensureMyTable(); + $placeholder = $this->placeholder(); + $this->query->add_where_expression(0, "$this->tableAlias.uid = $placeholder OR ((SELECT COUNT(*) FROM {node_revision} nr WHERE nr.uid = $placeholder AND nr.nid = $this->tableAlias.nid) > 0)", array($placeholder => $this->argument)); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument/Vid.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument/Vid.php new file mode 100644 index 0000000..d24c314 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument/Vid.php @@ -0,0 +1,51 @@ +fields('nr', array('vid', 'nid', 'title')) + ->condition('nr.vid', $this->value) + ->execute() + ->fetchAllAssoc('vid', PDO::FETCH_ASSOC); + $nids = array(); + foreach ($results as $result) { + $nids[] = $result['nid']; + } + + $nodes = node_load_multiple(array_unique($nids)); + + foreach ($results as $result) { + $nodes[$result['nid']]->set('title', $result['title']); + $titles[] = check_plain($nodes[$result['nid']]->label()); + } + + return $titles; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument_default/Node.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument_default/Node.php new file mode 100644 index 0000000..690dd31 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument_default/Node.php @@ -0,0 +1,40 @@ +nid; + } + } + + if (arg(0) == 'node' && is_numeric(arg(1))) { + return arg(1); + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/argument_validator/Node.php b/core/modules/node/lib/Drupal/node/Plugin/views/argument_validator/Node.php new file mode 100644 index 0000000..4607e77 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/argument_validator/Node.php @@ -0,0 +1,144 @@ + array()); + $options['access'] = array('default' => FALSE, 'bool' => TRUE); + $options['access_op'] = array('default' => 'view'); + $options['nid_type'] = array('default' => 'nid'); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $types = node_type_get_types(); + $options = array(); + foreach ($types as $type => $info) { + $options[$type] = check_plain(t($info->name)); + } + + $form['types'] = array( + '#type' => 'checkboxes', + '#title' => t('Content types'), + '#options' => $options, + '#default_value' => $this->options['types'], + '#description' => t('Choose one or more content types to validate with.'), + ); + + $form['access'] = array( + '#type' => 'checkbox', + '#title' => t('Validate user has access to the content'), + '#default_value' => $this->options['access'], + ); + $form['access_op'] = array( + '#type' => 'radios', + '#title' => t('Access operation to check'), + '#options' => array('view' => t('View'), 'update' => t('Edit'), 'delete' => t('Delete')), + '#default_value' => $this->options['access_op'], + '#states' => array( + 'visible' => array( + ':input[name="options[validate][options][node][access]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['nid_type'] = array( + '#type' => 'select', + '#title' => t('Filter value format'), + '#options' => array( + 'nid' => t('Node ID'), + 'nids' => t('Node IDs separated by , or +'), + ), + '#default_value' => $this->options['nid_type'], + ); + } + + public function submitOptionsForm(&$form, &$form_state, &$options = array()) { + // filter trash out of the options so we don't store giant unnecessary arrays + $options['types'] = array_filter($options['types']); + } + + function validate_argument($argument) { + $types = $this->options['types']; + + switch ($this->options['nid_type']) { + case 'nid': + if (!is_numeric($argument)) { + return FALSE; + } + $node = node_load($argument); + if (!$node) { + return FALSE; + } + + if (!empty($this->options['access'])) { + if (!node_access($this->options['access_op'], $node)) { + return FALSE; + } + } + + // Save the title() handlers some work. + $this->argument->validated_title = check_plain($node->label()); + + if (empty($types)) { + return TRUE; + } + + return isset($types[$node->type]); + + case 'nids': + $nids = new stdClass(); + $nids->value = array($argument); + $nids = $this->breakPhrase($argument, $nids); + if ($nids->value == array(-1)) { + return FALSE; + } + + $test = drupal_map_assoc($nids->value); + $titles = array(); + + $nodes = node_load_multiple($nids->value); + foreach ($nodes as $node) { + if ($types && empty($types[$node->type])) { + return FALSE; + } + + if (!empty($this->options['access'])) { + if (!node_access($this->options['access_op'], $node)) { + return FALSE; + } + } + + $titles[] = check_plain($node->label()); + unset($test[$node->nid]); + } + + $this->argument->validated_title = implode($nids->operator == 'or' ? ' + ' : ', ', $titles); + // If this is not empty, we did not find a nid. + return empty($test); + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/HistoryUserTimestamp.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/HistoryUserTimestamp.php new file mode 100644 index 0000000..3617bc6 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/HistoryUserTimestamp.php @@ -0,0 +1,95 @@ +uid) { + $this->additional_fields['created'] = array('table' => 'node', 'field' => 'created'); + $this->additional_fields['changed'] = array('table' => 'node', 'field' => 'changed'); + if (module_exists('comment') && !empty($this->options['comments'])) { + $this->additional_fields['last_comment'] = array('table' => 'node_comment_statistics', 'field' => 'last_comment_timestamp'); + } + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['comments'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + if (module_exists('comment')) { + $form['comments'] = array( + '#type' => 'checkbox', + '#title' => t('Check for new comments as well'), + '#default_value' => !empty($this->options['comments']), + '#fieldset' => 'more', + ); + } + } + + public function query() { + // Only add ourselves to the query if logged in. + global $user; + if (!$user->uid) { + return; + } + parent::query(); + } + + function render($values) { + // Let's default to 'read' state. + // This code shadows node_mark, but it reads from the db directly and + // we already have that info. + $mark = MARK_READ; + global $user; + if ($user->uid) { + $last_read = $this->get_value($values); + $changed = $this->get_value($values, 'changed'); + + $last_comment = module_exists('comment') && !empty($this->options['comments']) ? $this->get_value($values, 'last_comment') : 0; + + if (!$last_read && $changed > NODE_NEW_LIMIT) { + $mark = MARK_NEW; + } + elseif ($changed > $last_read && $changed > NODE_NEW_LIMIT) { + $mark = MARK_UPDATED; + } + elseif ($last_comment > $last_read && $last_comment > NODE_NEW_LIMIT) { + $mark = MARK_UPDATED; + } + return $this->render_link(theme('mark', array('type' => $mark)), $values); + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/Language.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/Language.php new file mode 100644 index 0000000..3fb43fb --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/Language.php @@ -0,0 +1,51 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['native_language'] = array( + '#title' => t('Native language'), + '#type' => 'checkbox', + '#default_value' => $this->options['native_language'], + '#description' => t('If enabled, the native name of the language will be displayed'), + ); + } + + function render($values) { + // @todo: Drupal Core dropped native language until config translation is + // ready, see http://drupal.org/node/1616594. + $value = $this->get_value($values); + $language = language_load($value); + $value = $language ? $language->name : ''; + return $this->render_link($value, $values); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/Link.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/Link.php new file mode 100644 index 0000000..83c489a --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/Link.php @@ -0,0 +1,61 @@ + '', 'translatable' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + parent::buildOptionsForm($form, $form_state); + + // The path is set by render_link function so don't allow to set it. + $form['alter']['path'] = array('#access' => FALSE); + $form['alter']['external'] = array('#access' => FALSE); + } + + public function query() {} + + function render($values) { + if ($entity = $this->get_entity($values)) { + return $this->render_link($entity, $values); + } + } + + function render_link($node, $values) { + if (node_access('view', $node)) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/$node->nid"; + $text = !empty($this->options['text']) ? $this->options['text'] : t('view'); + return $text; + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/LinkDelete.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/LinkDelete.php new file mode 100644 index 0000000..6a394ff --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/LinkDelete.php @@ -0,0 +1,42 @@ +options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/$node->nid/delete"; + $this->options['alter']['query'] = drupal_get_destination(); + + $text = !empty($this->options['text']) ? $this->options['text'] : t('delete'); + return $text; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/LinkEdit.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/LinkEdit.php new file mode 100644 index 0000000..bf27a9b --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/LinkEdit.php @@ -0,0 +1,42 @@ +options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/$node->nid/edit"; + $this->options['alter']['query'] = drupal_get_destination(); + + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + return $text; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/Node.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/Node.php new file mode 100644 index 0000000..b78e9f0 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/Node.php @@ -0,0 +1,92 @@ +options['link_to_node'])) { + $this->additional_fields['nid'] = array('table' => 'node', 'field' => 'nid'); + if (module_exists('translation')) { + $this->additional_fields['langcode'] = array('table' => 'node', 'field' => 'langcode'); + } + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_to_node'] = array('default' => isset($this->definition['link_to_node default']) ? $this->definition['link_to_node default'] : FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to node option + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_node'] = array( + '#title' => t('Link this field to the original piece of content'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_node']), + ); + + parent::buildOptionsForm($form, $form_state); + } + + /** + * Render whatever the data is as a link to the node. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + if (!empty($this->options['link_to_node']) && !empty($this->additional_fields['nid'])) { + if ($data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/" . $this->get_value($values, 'nid'); + if (isset($this->aliases['langcode'])) { + $languages = language_list(); + $langcode = $this->get_value($values, 'langcode'); + if (isset($languages[$langcode])) { + $this->options['alter']['language'] = $languages[$langcode]; + } + else { + unset($this->options['alter']['language']); + } + } + } + else { + $this->options['alter']['make_link'] = FALSE; + } + } + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/Path.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/Path.php new file mode 100644 index 0000000..1fca3d3 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/Path.php @@ -0,0 +1,63 @@ +additional_fields['nid'] = 'nid'; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['absolute'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['absolute'] = array( + '#type' => 'checkbox', + '#title' => t('Use absolute link (begins with "http://")'), + '#default_value' => $this->options['absolute'], + '#description' => t('Enable this option to output an absolute link. Required if you want to use the path as a link destination (as in "output this field as a link" above).'), + '#fieldset' => 'alter', + ); + } + + public function query() { + $this->ensureMyTable(); + $this->add_additional_fields(); + } + + function render($values) { + $nid = $this->get_value($values, 'nid'); + return url("node/$nid", array('absolute' => $this->options['absolute'])); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/Revision.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/Revision.php new file mode 100644 index 0000000..163c9f3 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/Revision.php @@ -0,0 +1,80 @@ +options['link_to_node_revision'])) { + $this->additional_fields['vid'] = 'vid'; + $this->additional_fields['nid'] = 'nid'; + if (module_exists('translation')) { + $this->additional_fields['langcode'] = array('table' => 'node', 'field' => 'langcode'); + } + } + } + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_to_node_revision'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to revision option. + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_node_revision'] = array( + '#title' => t('Link this field to its content revision'), + '#description' => t('This will override any other link you have set.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_node_revision']), + ); + parent::buildOptionsForm($form, $form_state); + } + + /** + * Render whatever the data is as a link to the node. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + if (!empty($this->options['link_to_node_revision']) && $data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $nid = $this->get_value($values, 'nid'); + $vid = $this->get_value($values, 'vid'); + $this->options['alter']['path'] = "node/" . $nid . '/revisions/' . $vid . '/view'; + if (module_exists('translation')) { + $langcode = $this->get_value($values, 'langcode'); + $languages = language_list(); + if (isset($languages[$langcode])) { + $this->options['alter']['langcode'] = $languages[$langcode]; + } + } + } + else { + return parent::render_link($data, $values); + } + return $data; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLink.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLink.php new file mode 100644 index 0000000..0701234 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLink.php @@ -0,0 +1,82 @@ +additional_fields['node_vid'] = array('table' => 'node_revision', 'field' => 'vid'); + } + + public function access() { + return user_access('view revisions') || user_access('administer nodes'); + } + + function render_link($data, $values) { + list($node, $vid) = $this->get_revision_entity($values, 'view'); + if (!isset($vid)) { + return; + } + + // Current revision uses the node view path. + $path = 'node/' . $node->nid; + if ($node->vid != $vid) { + $path .= "/revisions/$vid/view"; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = $path; + $this->options['alter']['query'] = drupal_get_destination(); + + return !empty($this->options['text']) ? $this->options['text'] : t('view'); + } + + /** + * Returns the revision values of a node. + * + * @param object $values + * An object containing all retrieved values. + * @param string $op + * The operation being performed. + * + * @return array + * A numerically indexed array containing the current node object and the + * revision ID for this row. + */ + function get_revision_entity($values, $op) { + $vid = $this->get_value($values, 'node_vid'); + $node = $this->get_value($values); + // Unpublished nodes ignore access control. + $node->status = 1; + // Ensure user has access to perform the operation on this node. + if (!node_access($op, $node)) { + return array($node, NULL); + } + return array($node, $vid); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkDelete.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkDelete.php new file mode 100644 index 0000000..2bb53f1 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkDelete.php @@ -0,0 +1,47 @@ +get_revision_entity($values, 'delete'); + if (!isset($vid)) { + return; + } + + // Current revision cannot be deleted. + if ($node->vid == $vid) { + return; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'node/' . $node->nid . "/revisions/$vid/delete"; + $this->options['alter']['query'] = drupal_get_destination(); + + return !empty($this->options['text']) ? $this->options['text'] : t('delete'); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkRevert.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkRevert.php new file mode 100644 index 0000000..11e66ff --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkRevert.php @@ -0,0 +1,47 @@ +get_revision_entity($values, 'update'); + if (!isset($vid)) { + return; + } + + // Current revision cannot be reverted. + if ($node->vid == $vid) { + return; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'node/' . $node->nid . "/revisions/$vid/revert"; + $this->options['alter']['query'] = drupal_get_destination(); + + return !empty($this->options['text']) ? $this->options['text'] : t('revert'); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/Type.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/Type.php new file mode 100644 index 0000000..e444a2e --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/Type.php @@ -0,0 +1,61 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide machine_name option for to node type display. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['machine_name'] = array( + '#title' => t('Output machine name'), + '#description' => t('Display field as the content type machine name.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['machine_name']), + ); + } + + /** + * Render node type as human readable name, unless using machine_name option. + */ + function render_name($data, $values) { + if ($this->options['machine_name'] != 1 && $data !== NULL && $data !== '') { + return t($this->sanitizeValue(node_type_get_label($data))); + } + return $this->sanitizeValue($data); + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->render_name($value, $values), $values); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/filter/Access.php b/core/modules/node/lib/Drupal/node/Plugin/views/filter/Access.php new file mode 100644 index 0000000..af9c38a --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/filter/Access.php @@ -0,0 +1,52 @@ +ensureMyTable(); + $grants = db_or(); + foreach (node_access_grants('view') as $realm => $gids) { + foreach ($gids as $gid) { + $grants->condition(db_and() + ->condition($table . '.gid', $gid) + ->condition($table . '.realm', $realm) + ); + } + } + + $this->query->add_where('AND', $grants); + $this->query->add_where('AND', $table . '.grant_view', 1, '>='); + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/filter/HistoryUserTimestamp.php b/core/modules/node/lib/Drupal/node/Plugin/views/filter/HistoryUserTimestamp.php new file mode 100644 index 0000000..57cb781 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/filter/HistoryUserTimestamp.php @@ -0,0 +1,99 @@ +options['expose']['label'])) { + $label = $this->options['expose']['label']; + } + else { + $label = t('Has new content'); + } + $form['value'] = array( + '#type' => 'checkbox', + '#title' => $label, + '#default_value' => $this->value, + ); + } + } + + public function query() { + global $user; + // This can only work if we're logged in. + if (!$user || !$user->uid) { + return; + } + + // Don't filter if we're exposed and the checkbox isn't selected. + if ((!empty($this->options['exposed'])) && empty($this->value)) { + return; + } + + // Hey, Drupal kills old history, so nodes that haven't been updated + // since NODE_NEW_LIMIT are bzzzzzzzt outta here! + + $limit = REQUEST_TIME - NODE_NEW_LIMIT; + + $this->ensureMyTable(); + $field = "$this->tableAlias.$this->realField"; + $node = $this->query->ensure_table('node', $this->relationship); + + $clause = ''; + $clause2 = ''; + if (module_exists('comment')) { + $ncs = $this->query->ensure_table('node_comment_statistics', $this->relationship); + $clause = ("OR $ncs.last_comment_timestamp > (***CURRENT_TIME*** - $limit)"); + $clause2 = "OR $field < $ncs.last_comment_timestamp"; + } + + // NULL means a history record doesn't exist. That's clearly new content. + // Unless it's very very old content. Everything in the query is already + // type safe cause none of it is coming from outside here. + $this->query->add_where_expression($this->options['group'], "($field IS NULL AND ($node.changed > (***CURRENT_TIME*** - $limit) $clause)) OR $field < $node.changed $clause2"); + } + + public function adminSummary() { + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/filter/Status.php b/core/modules/node/lib/Drupal/node/Plugin/views/filter/Status.php new file mode 100644 index 0000000..49fdb97 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/filter/Status.php @@ -0,0 +1,36 @@ +ensureMyTable(); + $this->query->add_where_expression($this->options['group'], "$table.status = 1 OR ($table.uid = ***CURRENT_USER*** AND ***CURRENT_USER*** <> 0 AND ***VIEW_OWN_UNPUBLISHED_NODES*** = 1) OR ***BYPASS_NODE_ACCESS*** = 1"); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/filter/Type.php b/core/modules/node/lib/Drupal/node/Plugin/views/filter/Type.php new file mode 100644 index 0000000..87f7005 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/filter/Type.php @@ -0,0 +1,38 @@ +value_options)) { + $this->value_title = t('Content types'); + $types = node_type_get_types(); + $options = array(); + foreach ($types as $type => $info) { + $options[$type] = t($info->name); + } + asort($options); + $this->value_options = $options; + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/filter/UidRevision.php b/core/modules/node/lib/Drupal/node/Plugin/views/filter/UidRevision.php new file mode 100644 index 0000000..f435e4d --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/filter/UidRevision.php @@ -0,0 +1,37 @@ +ensureMyTable(); + + $placeholder = $this->placeholder(); + + $args = array_values($this->value); + + $this->query->add_where_expression($this->options['group'], "$this->tableAlias.uid IN($placeholder) OR + ((SELECT COUNT(*) FROM {node_revision} nr WHERE nr.uid IN($placeholder) AND nr.nid = $this->tableAlias.nid) > 0)", array($placeholder => $args), + $args); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/row/Rss.php b/core/modules/node/lib/Drupal/node/Plugin/views/row/Rss.php new file mode 100644 index 0000000..f9f8479 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/row/Rss.php @@ -0,0 +1,182 @@ + 'default'); + $options['links'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['item_length'] = array( + '#type' => 'select', + '#title' => t('Display type'), + '#options' => $this->buildOptionsForm_summary_options(), + '#default_value' => $this->options['item_length'], + ); + $form['links'] = array( + '#type' => 'checkbox', + '#title' => t('Display links'), + '#default_value' => $this->options['links'], + ); + } + + /** + * Return the main options, which are shown in the summary title. + */ + public function buildOptionsForm_summary_options() { + $entity_info = entity_get_info('node'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + $options['title'] = t('Title only'); + $options['default'] = t('Use site default RSS settings'); + return $options; + } + + public function summaryTitle() { + $options = $this->buildOptionsForm_summary_options(); + return check_plain($options[$this->options['item_length']]); + } + + function pre_render($values) { + $nids = array(); + foreach ($values as $row) { + $nids[] = $row->{$this->field_alias}; + } + if (!empty($nids)) { + $this->nodes = node_load_multiple($nids); + } + } + + function render($row) { + // For the most part, this code is taken from node_feed() in node.module + global $base_url; + + $nid = $row->{$this->field_alias}; + if (!is_numeric($nid)) { + return; + } + + $display_mode = $this->options['item_length']; + if ($display_mode == 'default') { + $display_mode = config('system.rss')->get('items.view_mode'); + } + + // Load the specified node: + $node = $this->nodes[$nid]; + if (empty($node)) { + return; + } + + $item_text = ''; + + $uri = $node->uri(); + $node->link = url($uri['path'], $uri['options'] + array('absolute' => TRUE)); + $node->rss_namespaces = array(); + $node->rss_elements = array( + array( + 'key' => 'pubDate', + 'value' => gmdate('r', $node->created), + ), + array( + 'key' => 'dc:creator', + 'value' => $node->name, + ), + array( + 'key' => 'guid', + 'value' => $node->nid . ' at ' . $base_url, + 'attributes' => array('isPermaLink' => 'false'), + ), + ); + + // The node gets built and modules add to or modify $node->rss_elements + // and $node->rss_namespaces. + + $build_mode = $display_mode; + + $build = node_view($node, $build_mode); + unset($build['#theme']); + + if (!empty($node->rss_namespaces)) { + $this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $node->rss_namespaces); + } + elseif (function_exists('rdf_get_namespaces')) { + // Merge RDF namespaces in the XML namespaces in case they are used + // further in the RSS content. + $xml_rdf_namespaces = array(); + foreach (rdf_get_namespaces() as $prefix => $uri) { + $xml_rdf_namespaces['xmlns:' . $prefix] = $uri; + } + $this->view->style_plugin->namespaces += $xml_rdf_namespaces; + } + + // Hide the links if desired. + if (!$this->options['links']) { + hide($build['links']); + } + + if ($display_mode != 'title') { + // We render node contents and force links to be last. + $build['links']['#weight'] = 1000; + $item_text .= drupal_render($build); + } + + $item = new stdClass(); + $item->description = $item_text; + $item->title = $node->label(); + $item->link = $node->link; + $item->elements = $node->rss_elements; + $item->nid = $node->nid; + + return theme($this->themeFunctions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $item + )); + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/row/View.php b/core/modules/node/lib/Drupal/node/Plugin/views/row/View.php new file mode 100644 index 0000000..ce41990 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/row/View.php @@ -0,0 +1,129 @@ +options['teaser'])) { + $this->options['build_mode'] = $this->options['teaser'] ? 'teaser' : 'full'; + } + // Handle existing views which has used build_mode instead of view_mode. + if (isset($this->options['build_mode'])) { + $this->options['view_mode'] = $this->options['build_mode']; + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['view_mode'] = array('default' => 'teaser'); + $options['links'] = array('default' => TRUE, 'bool' => TRUE); + $options['comments'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $options = $this->buildOptionsForm_summary_options(); + $form['view_mode'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t('View mode'), + '#default_value' => $this->options['view_mode'], + ); + $form['links'] = array( + '#type' => 'checkbox', + '#title' => t('Display links'), + '#default_value' => $this->options['links'], + ); + $form['comments'] = array( + '#type' => 'checkbox', + '#title' => t('Display comments'), + '#default_value' => $this->options['comments'], + ); + } + + /** + * Return the main options, which are shown in the summary title. + */ + public function buildOptionsForm_summary_options() { + $entity_info = entity_get_info('node'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + if (empty($options)) { + $options = array( + 'teaser' => t('Teaser'), + 'full' => t('Full content') + ); + } + + return $options; + } + + public function summaryTitle() { + $options = $this->buildOptionsForm_summary_options(); + return check_plain($options[$this->options['view_mode']]); + } + + function pre_render($values) { + $nids = array(); + foreach ($values as $row) { + $nids[] = $row->{$this->field_alias}; + } + $this->nodes = node_load_multiple($nids); + } + + function render($row) { + if (isset($this->nodes[$row->{$this->field_alias}])) { + $node = $this->nodes[$row->{$this->field_alias}]; + $node->view = $this->view; + $build = node_view($node, $this->options['view_mode']); + + return drupal_render($build); + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/wizard/Node.php b/core/modules/node/lib/Drupal/node/Plugin/views/wizard/Node.php new file mode 100644 index 0000000..af28e1a --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/wizard/Node.php @@ -0,0 +1,316 @@ + 'nid', + 'table' => 'node', + 'field' => 'nid', + 'exclude' => TRUE, + 'link_to_node' => FALSE, + 'alter' => array( + 'alter_text' => TRUE, + 'text' => 'node/[nid]' + ) + ); + + /** + * Set default values for the filters. + */ + protected $filters = array( + 'status' => array( + 'value' => TRUE, + 'table' => 'node', + 'field' => 'status' + ) + ); + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::getAvailableSorts(). + * + * @return array + */ + public function getAvailableSorts() { + // You can't execute functions in properties, so override the method + return array( + 'title:DESC' => t('Title') + ); + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::row_style_options(). + */ + protected function row_style_options() { + $options = array(); + $options['teasers'] = t('teasers'); + $options['full_posts'] = t('full posts'); + $options['titles'] = t('titles'); + $options['titles_linked'] = t('titles (linked)'); + $options['fields'] = t('fields'); + return $options; + } + + /** + * Adds the style options to the wizard form. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * @param string $type + * The display ID (e.g. 'page' or 'block'). + */ + protected function build_form_style(array &$form, array &$form_state, $type) { + parent::build_form_style($form, $form_state, $type); + $style_form =& $form['displays'][$type]['options']['style']; + // Some style plugins don't support row plugins so stop here if that's the + // case. + if (!isset($style_form['row_plugin']['#default_value'])) { + return; + } + $row_plugin = $style_form['row_plugin']['#default_value']; + switch ($row_plugin) { + case 'full_posts': + case 'teasers': + $style_form['row_options']['links'] = array( + '#type' => 'select', + '#title_display' => 'invisible', + '#title' => t('Should links be displayed below each node'), + '#options' => array( + 1 => t('with links (allow users to add comments, etc.)'), + 0 => t('without links'), + ), + '#default_value' => 1, + ); + $style_form['row_options']['comments'] = array( + '#type' => 'select', + '#title_display' => 'invisible', + '#title' => t('Should comments be displayed below each node'), + '#options' => array( + 1 => t('with comments'), + 0 => t('without comments'), + ), + '#default_value' => 0, + ); + break; + } + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::default_display_options(). + */ + protected function default_display_options() { + $display_options = parent::default_display_options(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + // Add the title field, so that the display has content if the user switches + // to a row style that uses fields. + /* Field: Content: Title */ + $display_options['fields']['title']['id'] = 'title'; + $display_options['fields']['title']['table'] = 'node'; + $display_options['fields']['title']['field'] = 'title'; + $display_options['fields']['title']['label'] = ''; + $display_options['fields']['title']['alter']['alter_text'] = 0; + $display_options['fields']['title']['alter']['make_link'] = 0; + $display_options['fields']['title']['alter']['absolute'] = 0; + $display_options['fields']['title']['alter']['trim'] = 0; + $display_options['fields']['title']['alter']['word_boundary'] = 0; + $display_options['fields']['title']['alter']['ellipsis'] = 0; + $display_options['fields']['title']['alter']['strip_tags'] = 0; + $display_options['fields']['title']['alter']['html'] = 0; + $display_options['fields']['title']['hide_empty'] = 0; + $display_options['fields']['title']['empty_zero'] = 0; + $display_options['fields']['title']['link_to_node'] = 1; + + return $display_options; + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::default_display_filters_user(). + */ + protected function default_display_filters_user(array $form, array &$form_state) { + $filters = parent::default_display_filters_user($form, $form_state); + + if (!empty($form_state['values']['show']['tagged_with']['tids'])) { + $filters['tid'] = array( + 'id' => 'tid', + 'table' => 'taxonomy_index', + 'field' => 'tid', + 'value' => $form_state['values']['show']['tagged_with']['tids'], + 'vocabulary' => $form_state['values']['show']['tagged_with']['vocabulary'], + ); + // If the user entered more than one valid term in the autocomplete + // field, they probably intended both of them to be applied. + if (count($form_state['values']['show']['tagged_with']['tids']) > 1) { + $filters['tid']['operator'] = 'and'; + // Sort the terms so the filter will be displayed as it normally would + // on the edit screen. + sort($filters['tid']['value']); + } + } + + return $filters; + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::page_display_options(). + */ + protected function page_display_options(array $form, array &$form_state) { + $display_options = parent::page_display_options($form, $form_state); + $row_plugin = isset($form_state['values']['page']['style']['row_plugin']) ? $form_state['values']['page']['style']['row_plugin'] : NULL; + $row_options = isset($form_state['values']['page']['style']['row_options']) ? $form_state['values']['page']['style']['row_options'] : array(); + $this->display_options_row($display_options, $row_plugin, $row_options); + return $display_options; + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::page_display_options(). + */ + protected function block_display_options(array $form, array &$form_state) { + $display_options = parent::block_display_options($form, $form_state); + $row_plugin = isset($form_state['values']['block']['style']['row_plugin']) ? $form_state['values']['block']['style']['row_plugin'] : NULL; + $row_options = isset($form_state['values']['block']['style']['row_options']) ? $form_state['values']['block']['style']['row_options'] : array(); + $this->display_options_row($display_options, $row_plugin, $row_options); + return $display_options; + } + + /** + * Set the row style and row style plugins to the display_options. + */ + protected function display_options_row(&$display_options, $row_plugin, $row_options) { + switch ($row_plugin) { + case 'full_posts': + $display_options['row']['type'] = 'node'; + $display_options['row']['options']['build_mode'] = 'full'; + $display_options['row']['options']['links'] = !empty($row_options['links']); + $display_options['row']['options']['comments'] = !empty($row_options['comments']); + break; + case 'teasers': + $display_options['row']['type'] = 'node'; + $display_options['row']['options']['build_mode'] = 'teaser'; + $display_options['row']['options']['links'] = !empty($row_options['links']); + $display_options['row']['options']['comments'] = !empty($row_options['comments']); + break; + case 'titles_linked': + $display_options['row']['type'] = 'fields'; + $display_options['field']['title']['link_to_node'] = 1; + break; + case 'titles': + $display_options['row']['type'] = 'fields'; + $display_options['field']['title']['link_to_node'] = 0; + break; + } + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::build_filters(). + * + * Add some options for filter by taxonomy terms. + */ + protected function build_filters(&$form, &$form_state) { + parent::build_filters($form, $form_state); + $entity_info = $this->entity_info; + + $selected_bundle = views_ui_get_selected($form_state, array('show', 'type'), 'all', $form['displays']['show']['type']); + + // Add the "tagged with" filter to the view. + + // We construct this filter using taxonomy_index.tid (which limits the + // filtering to a specific vocabulary) rather than taxonomy_term_data.name + // (which matches terms in any vocabulary). This is because it is a more + // commonly-used filter that works better with the autocomplete UI, and + // also to avoid confusion with other vocabularies on the site that may + // have terms with the same name but are not used for free tagging. + + // The downside is that if there *is* more than one vocabulary on the site + // that is used for free tagging, the wizard will only be able to make the + // "tagged with" filter apply to one of them (see below for the method it + // uses to choose). + + // Find all "tag-like" taxonomy fields associated with the view's + // entities. If a particular entity type (i.e., bundle) has been + // selected above, then we only search for taxonomy fields associated + // with that bundle. Otherwise, we use all bundles. + $bundles = array_keys($entity_info['bundles']); + // Double check that this is a real bundle before using it (since above + // we added a dummy option 'all' to the bundle list on the form). + if (isset($selected_bundle) && in_array($selected_bundle, $bundles)) { + $bundles = array($selected_bundle); + } + $tag_fields = array(); + foreach ($bundles as $bundle) { + foreach (field_info_instances($this->entity_type, $bundle) as $instance) { + // We define "tag-like" taxonomy fields as ones that use the + // "Autocomplete term widget (tagging)" widget. + if ($instance['widget']['type'] == 'taxonomy_autocomplete') { + $tag_fields[] = $instance['field_name']; + } + } + } + $tag_fields = array_unique($tag_fields); + if (!empty($tag_fields)) { + // If there is more than one "tag-like" taxonomy field available to + // the view, we can only make our filter apply to one of them (as + // described above). We choose 'field_tags' if it is available, since + // that is created by the Standard install profile in core and also + // commonly used by contrib modules; thus, it is most likely to be + // associated with the "main" free-tagging vocabulary on the site. + if (in_array('field_tags', $tag_fields)) { + $tag_field_name = 'field_tags'; + } + else { + $tag_field_name = reset($tag_fields); + } + // Add the autocomplete textfield to the wizard. + $form['displays']['show']['tagged_with'] = array( + '#type' => 'textfield', + '#title' => t('tagged with'), + '#autocomplete_path' => 'taxonomy/autocomplete/' . $tag_field_name, + '#size' => 30, + '#maxlength' => 1024, + '#field_name' => $tag_field_name, + '#element_validate' => array('views_ui_taxonomy_autocomplete_validate'), + ); + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/wizard/NodeRevision.php b/core/modules/node/lib/Drupal/node/Plugin/views/wizard/NodeRevision.php new file mode 100644 index 0000000..9c5073f --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/wizard/NodeRevision.php @@ -0,0 +1,134 @@ + 'vid', + 'table' => 'node_revision', + 'field' => 'vid', + 'exclude' => TRUE, + 'alter' => array( + 'alter_text' => TRUE, + 'text' => 'node/[nid]/revisions/[vid]/view' + ) + ); + + /** + * Set the additional information for the pathField property. + */ + protected $pathFieldsSupplemental = array( + array( + 'id' => 'nid', + 'table' => 'node', + 'field' => 'nid', + 'exclude' => TRUE, + 'link_to_node' => FALSE + ) + ); + + /** + * Set default values for the filters. + */ + protected $filters = array( + 'status' => array( + 'value' => TRUE, + 'table' => 'node_revision', + 'field' => 'status' + ) + ); + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::row_style_options(). + * + * Node revisions do not support full posts or teasers, so remove them. + */ + protected function row_style_options() { + $options = parent::row_style_options(); + unset($options['teasers']); + unset($options['full_posts']); + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::default_display_options(). + */ + protected function default_display_options() { + $display_options = parent::default_display_options(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + $display_options['access']['perm'] = 'view revisions'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + /* Field: Content revision: Created date */ + $display_options['fields']['timestamp']['id'] = 'timestamp'; + $display_options['fields']['timestamp']['table'] = 'node_revision'; + $display_options['fields']['timestamp']['field'] = 'timestamp'; + $display_options['fields']['timestamp']['alter']['alter_text'] = 0; + $display_options['fields']['timestamp']['alter']['make_link'] = 0; + $display_options['fields']['timestamp']['alter']['absolute'] = 0; + $display_options['fields']['timestamp']['alter']['trim'] = 0; + $display_options['fields']['timestamp']['alter']['word_boundary'] = 0; + $display_options['fields']['timestamp']['alter']['ellipsis'] = 0; + $display_options['fields']['timestamp']['alter']['strip_tags'] = 0; + $display_options['fields']['timestamp']['alter']['html'] = 0; + $display_options['fields']['timestamp']['hide_empty'] = 0; + $display_options['fields']['timestamp']['empty_zero'] = 0; + + /* Field: Content revision: Title */ + $display_options['fields']['title']['id'] = 'title'; + $display_options['fields']['title']['table'] = 'node_revision'; + $display_options['fields']['title']['field'] = 'title'; + $display_options['fields']['title']['label'] = ''; + $display_options['fields']['title']['alter']['alter_text'] = 0; + $display_options['fields']['title']['alter']['make_link'] = 0; + $display_options['fields']['title']['alter']['absolute'] = 0; + $display_options['fields']['title']['alter']['trim'] = 0; + $display_options['fields']['title']['alter']['word_boundary'] = 0; + $display_options['fields']['title']['alter']['ellipsis'] = 0; + $display_options['fields']['title']['alter']['strip_tags'] = 0; + $display_options['fields']['title']['alter']['html'] = 0; + $display_options['fields']['title']['hide_empty'] = 0; + $display_options['fields']['title']['empty_zero'] = 0; + $display_options['fields']['title']['link_to_node'] = 0; + $display_options['fields']['title']['link_to_node_revision'] = 1; + + return $display_options; + } + +} diff --git a/core/modules/node/node.views.inc b/core/modules/node/node.views.inc new file mode 100644 index 0000000..8ac6fd6 --- /dev/null +++ b/core/modules/node/node.views.inc @@ -0,0 +1,770 @@ + 'nid', + 'title' => t('Content'), + 'weight' => -10, + 'access query tag' => 'node_access', + 'defaults' => array( + 'field' => 'title', + ), + ); + $data['node']['table']['entity type'] = 'node'; + + // node table -- fields + + // nid + $data['node']['nid'] = array( + 'title' => t('Nid'), + 'help' => t('The node ID.'), // The help that appears on the UI, + // Information for displaying the nid + 'field' => array( + 'id' => 'node', + 'click sortable' => TRUE, + ), + // Information for accepting a nid as an argument + 'argument' => array( + 'id' => 'node_nid', + 'name field' => 'title', // the field to display in the summary. + 'numeric' => TRUE, + 'validate type' => 'nid', + ), + // Information for accepting a nid as a filter + 'filter' => array( + 'id' => 'numeric', + ), + // Information for sorting on a nid. + 'sort' => array( + 'id' => 'standard', + ), + ); + + // title + // This definition has more items in it than it needs to as an example. + $data['node']['title'] = array( + 'title' => t('Title'), // The item it appears as on the UI, + 'help' => t('The content title.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'field' => 'title', // the real field. This could be left out since it is the same. + 'group' => t('Content'), // The group it appears in on the UI. Could be left out. + 'id' => 'node', + 'click sortable' => TRUE, + 'link_to_node default' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // created field + $data['node']['created'] = array( + 'title' => t('Post date'), // The item it appears as on the UI, + 'help' => t('The date the content was posted.'), // The help that appears on the UI, + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date' + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + // changed field + $data['node']['changed'] = array( + 'title' => t('Updated date'), // The item it appears as on the UI, + 'help' => t('The date the content was last updated.'), // The help that appears on the UI, + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date' + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + // Content type + $data['node']['type'] = array( + 'title' => t('Type'), // The item it appears as on the UI, + 'help' => t('The content type (for example, "blog entry", "forum post", "story", etc).'), // The help that appears on the UI, + 'field' => array( + 'id' => 'node_type', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'node_type', + ), + 'argument' => array( + 'id' => 'node_type', + ), + ); + + // published status + $data['node']['status'] = array( + 'title' => t('Published'), + 'help' => t('Whether or not the content is published.'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'published-notpublished' => array(t('Published'), t('Not published')), + ), + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Published'), + 'type' => 'yes-no', + 'use_equal' => TRUE, // Use status = 1 instead of status <> 0 in WHERE statment + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // published status + extra + $data['node']['status_extra'] = array( + 'title' => t('Published or admin'), + 'help' => t('Filters out unpublished content if the current user cannot view it.'), + 'filter' => array( + 'field' => 'status', + 'id' => 'node_status', + 'label' => t('Published or admin'), + ), + ); + + // promote status + $data['node']['promote'] = array( + 'title' => t('Promoted to front page'), + 'help' => t('Whether or not the content is promoted to the front page.'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'promoted-notpromoted' => array(t('Promoted'), t('Not promoted')), + ), + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Promoted to front page'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // sticky + $data['node']['sticky'] = array( + 'title' => t('Sticky'), // The item it appears as on the UI, + 'help' => t('Whether or not the content is sticky.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'sticky' => array(t('Sticky'), t('Not sticky')), + ), + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Sticky'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'id' => 'standard', + 'help' => t('Whether or not the content is sticky. To list sticky content first, set this to descending.'), + ), + ); + + // Language field + if (module_exists('language')) { + $data['node']['language']['moved to'] = array('node', 'langcode'); + $data['node']['langcode'] = array( + 'title' => t('Language'), + 'help' => t('The language the content is in.'), + 'field' => array( + 'id' => 'node_language', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'language', + ), + 'argument' => array( + 'id' => 'language', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + } + + // Define some fields based upon views_handler_field_entity in the entity + // table so they can be re-used with other query backends. + // @see views_handler_field_entity + + $data['views_entity_node']['table']['group'] = t('Content'); + + $data['views_entity_node']['view_node'] = array( + 'field' => array( + 'title' => t('Link'), + 'help' => t('Provide a simple link to the content.'), + 'id' => 'node_link', + ), + ); + + $data['views_entity_node']['edit_node'] = array( + 'field' => array( + 'title' => t('Edit link'), + 'help' => t('Provide a simple link to edit the content.'), + 'id' => 'node_link_edit', + ), + ); + + $data['views_entity_node']['delete_node'] = array( + 'field' => array( + 'title' => t('Delete link'), + 'help' => t('Provide a simple link to delete the content.'), + 'id' => 'node_link_delete', + ), + ); + + $data['node']['path'] = array( + 'field' => array( + 'title' => t('Path'), + 'help' => t('The aliased path to this content.'), + 'id' => 'node_path', + ), + ); + + // Bogus fields for aliasing purposes. + + $data['node']['created_fulldate'] = array( + 'title' => t('Created date'), + 'help' => t('Date in the form of CCYYMMDD.'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_fulldate', + ), + ); + + $data['node']['created_year_month'] = array( + 'title' => t('Created year + month'), + 'help' => t('Date in the form of YYYYMM.'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_year_month', + ), + ); + + $data['node']['created_year'] = array( + 'title' => t('Created year'), + 'help' => t('Date in the form of YYYY.'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_year', + ), + ); + + $data['node']['created_month'] = array( + 'title' => t('Created month'), + 'help' => t('Date in the form of MM (01 - 12).'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_month', + ), + ); + + $data['node']['created_day'] = array( + 'title' => t('Created day'), + 'help' => t('Date in the form of DD (01 - 31).'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_day', + ), + ); + + $data['node']['created_week'] = array( + 'title' => t('Created week'), + 'help' => t('Date in the form of WW (01 - 53).'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_week', + ), + ); + + $data['node']['changed_fulldate'] = array( + 'title' => t('Updated date'), + 'help' => t('Date in the form of CCYYMMDD.'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_fulldate', + ), + ); + + $data['node']['changed_year_month'] = array( + 'title' => t('Updated year + month'), + 'help' => t('Date in the form of YYYYMM.'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_year_month', + ), + ); + + $data['node']['changed_year'] = array( + 'title' => t('Updated year'), + 'help' => t('Date in the form of YYYY.'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_year', + ), + ); + + $data['node']['changed_month'] = array( + 'title' => t('Updated month'), + 'help' => t('Date in the form of MM (01 - 12).'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_month', + ), + ); + + $data['node']['changed_day'] = array( + 'title' => t('Updated day'), + 'help' => t('Date in the form of DD (01 - 31).'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_day', + ), + ); + + $data['node']['changed_week'] = array( + 'title' => t('Updated week'), + 'help' => t('Date in the form of WW (01 - 53).'), + 'argument' => array( + 'field' => 'changed', + 'id' => 'node_created_week', + ), + ); + + // uid field + $data['node']['uid'] = array( + 'title' => t('Author uid'), + 'help' => t('The user authoring the content. If you need more fields than the uid add the content: author relationship'), + 'relationship' => array( + 'title' => t('Author'), + 'help' => t('Relate content to the user who created it.'), + 'id' => 'standard', + 'base' => 'users', + 'field' => 'uid', + 'label' => t('author'), + ), + 'filter' => array( + 'id' => 'user_name', + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'field' => array( + 'id' => 'user', + ), + ); + + $data['node']['uid_revision'] = array( + 'title' => t('User has a revision'), + 'help' => t('All nodes where a certain user has a revision'), + 'real field' => 'nid', + 'filter' => array( + 'id' => 'node_uid_revision', + ), + 'argument' => array( + 'id' => 'node_uid_revision', + ), + ); + + // ---------------------------------------------------------------------- + // Content revision table + + // Define the base group of this table. Fields that don't + // have a group defined will go into this field by default. + $data['node_revision']['table']['entity type'] = 'node'; + $data['node_revision']['table']['group'] = t('Content revision'); + + // Advertise this table as a possible base table + $data['node_revision']['table']['base'] = array( + 'field' => 'vid', + 'title' => t('Content revision'), + 'help' => t('Content revision is a history of changes to content.'), + 'defaults' => array( + 'field' => 'title', + ), + ); + + // For other base tables, explain how we join + $data['node_revision']['table']['join'] = array( + // Directly links to node table. + 'node' => array( + 'left_field' => 'vid', + 'field' => 'vid', + ), + ); + + // uid field for node revision + $data['node_revision']['uid'] = array( + 'title' => t('User'), + 'help' => t('Relate a content revision to the user who created the revision.'), + 'relationship' => array( + 'id' => 'standard', + 'base' => 'users', + 'base field' => 'uid', + 'label' => t('revision user'), + ), + ); + + // nid + $data['node_revision']['nid'] = array( + 'title' => t('Nid'), + // The help that appears on the UI. + 'help' => t('The revision NID of the content revision.'), + // Information for displaying the nid. + 'field' => array( + 'click sortable' => TRUE, + ), + // Information for accepting a nid as an argument. + 'argument' => array( + 'id' => 'node_nid', + 'click sortable' => TRUE, + 'numeric' => TRUE, + ), + // Information for accepting a nid as a filter. + 'filter' => array( + 'id' => 'numeric', + ), + // Information for sorting on a nid. + 'sort' => array( + 'id' => 'standard', + ), + 'relationship' => array( + 'id' => 'standard', + 'base' => 'node', + 'base field' => 'nid', + 'title' => t('Content'), + 'label' => t('Get the actual content from a content revision.'), + ), + ); + + // vid + $data['node_revision']['vid'] = array( + 'title' => t('Vid'), + 'help' => t('The revision ID of the content revision.'), + // Information for displaying the vid + 'field' => array( + 'click sortable' => TRUE, + ), + // Information for accepting a vid as an argument + 'argument' => array( + 'id' => 'node_vid', + 'click sortable' => TRUE, + 'numeric' => TRUE, + ), + // Information for accepting a vid as a filter + 'filter' => array( + 'id' => 'numeric', + ), + // Information for sorting on a vid. + 'sort' => array( + 'id' => 'standard', + ), + 'relationship' => array( + 'id' => 'standard', + 'base' => 'node', + 'base field' => 'vid', + 'title' => t('Content'), + 'label' => t('Get the actual content from a content revision.'), + ), + ); + + // published status + $data['node_revision']['status'] = array( + 'title' => t('Published'), + 'help' => t('Whether or not the content is published.'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'published-notpublished' => array(t('Published'), t('Not published')), + ), + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Published'), + 'type' => 'yes-no', + 'use_equal' => TRUE, // Use status = 1 instead of status <> 0 in WHERE statment + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // title + $data['node_revision']['title'] = array( + 'title' => t('Title'), // The item it appears as on the UI, + 'help' => t('The content title.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'field' => 'title', // the real field + 'id' => 'node_revision', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // log field + $data['node_revision']['log'] = array( + 'title' => t('Log message'), // The item it appears as on the UI, + 'help' => t('The log message entered when the revision was created.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'id' => 'xss', + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + // revision timestamp + // changed field + $data['node_revision']['timestamp'] = array( + 'title' => t('Updated date'), // The item it appears as on the UI, + 'help' => t('The date the node was last updated.'), // The help that appears on the UI, + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date' + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + $data['node_revision']['link_to_revision'] = array( + 'field' => array( + 'title' => t('Link'), + 'help' => t('Provide a simple link to the revision.'), + 'id' => 'node_revision_link', + ), + ); + + $data['node_revision']['revert_revision'] = array( + 'field' => array( + 'title' => t('Revert link'), + 'help' => t('Provide a simple link to revert to the revision.'), + 'id' => 'node_revision_link_revert', + ), + ); + + $data['node_revision']['delete_revision'] = array( + 'field' => array( + 'title' => t('Delete link'), + 'help' => t('Provide a simple link to delete the content revision.'), + 'id' => 'node_revision_link_delete', + ), + ); + + // ---------------------------------------------------------------------- + // Node access table + + // Define the base group of this table. Fields that don't + // have a group defined will go into this field by default. + $data['node_access']['table']['group'] = t('Content access'); + + // For other base tables, explain how we join + $data['node_access']['table']['join'] = array( + // Directly links to node table. + 'node' => array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + // nid field + $data['node_access']['nid'] = array( + 'title' => t('Access'), + 'help' => t('Filter by access.'), + 'filter' => array( + 'id' => 'node_access', + 'help' => t('Filter for content by view access. Not necessary if you are using node as your base table.'), + ), + ); + + // ---------------------------------------------------------------------- + // History table + + // We're actually defining a specific instance of the table, so let's + // alias it so that we can later add the real table for other purposes if we + // need it. + $data['history']['table']['group'] = t('Content'); + + // Explain how this table joins to others. + $data['history']['table']['join'] = array( + // Directly links to node table. + 'node' => array( + 'table' => 'history', + 'left_field' => 'nid', + 'field' => 'nid', + 'extra' => array( + array('field' => 'uid', 'value' => '***CURRENT_USER***', 'numeric' => TRUE), + ), + ), + ); + + $data['history']['timestamp'] = array( + 'title' => t('Has new content'), + 'field' => array( + 'id' => 'node_history_user_timestamp', + 'help' => t('Show a marker if the content is new or updated.'), + ), + 'filter' => array( + 'help' => t('Show only content that is new or updated.'), + 'id' => 'node_history_user_timestamp', + ), + ); + return $data; +} + +/** + * Implements hook_preprocess_node(). + */ +function node_row_node_view_preprocess_node(&$vars) { + $node = $vars['node']; + $options = $vars['view']->style_plugin->row_plugin->options; + + // Prevent the comment form from showing up if this is not a page display. + if ($vars['view_mode'] == 'full' && !$vars['view']->display_handler->hasPath()) { + $node->comment = FALSE; + } + + if (!$options['links']) { + unset($vars['content']['links']); + } + + if (!empty($options['comments']) && user_access('access comments') && $node->comment) { + $vars['content']['comments'] = comment_node_page_additions($node); + } +} + +/** + * Implements hook_views_query_substitutions(). + */ +function node_views_query_substitutions() { + return array( + '***ADMINISTER_NODES***' => intval(user_access('administer nodes')), + '***VIEW_OWN_UNPUBLISHED_NODES***' => intval(user_access('view own unpublished content')), + '***BYPASS_NODE_ACCESS***' => intval(user_access('bypass node access')), + ); +} + +/** + * Implements hook_views_analyze(). + */ +function node_views_analyze($view) { + $ret = array(); + // Check for something other than the default display: + if ($view->base_table == 'node') { + foreach ($view->displayHandlers as $id => $display) { + if (!$display->isDefaulted('access') || !$display->isDefaulted('filters')) { + // check for no access control + $access = $display->getOption('access'); + if (empty($access['type']) || $access['type'] == 'none') { + $select = db_select('role', 'r'); + $select->innerJoin('role_permission', 'p', 'r.rid = p.rid'); + $result = $select->fields('r', array('rid')) + ->fields('p', array('permission')) + ->condition('r.rid', array('anonymous', 'authenticated'), 'IN') + ->condition('p.permission', 'access content') + ->execute(); + + foreach ($result as $role) { + $role->safe = TRUE; + $roles[$role->rid] = $role; + } + if (!($roles['anonymous']->safe && $roles['authenticated']->safe)) { + $ret[] = Analyzer::formatMessage(t('Some roles lack permission to access content, but display %display has no access control.', array('%display' => $display['display_title'])), 'warning'); + } + $filters = $display->getOption('filters'); + foreach ($filters as $filter) { + if ($filter['table'] == 'node' && ($filter['field'] == 'status' || $filter['field'] == 'status_extra')) { + continue 2; + } + } + $ret[] = Analyzer::formatMessage(t('Display %display has no access control but does not contain a filter for published nodes.', array('%display' => $display['display_title'])), 'warning'); + } + } + } + } + foreach ($view->display as $id => $display) { + if ($display->getPluginId() == 'page') { + if ($display->getOption('path') == 'node/%') { + $ret[] = Analyzer::formatMessage(t('Display %display has set node/% as path. This will not produce what you want. If you want to have multiple versions of the node view, use panels.', array('%display' => $display['display_title'])), 'warning'); + } + } + } + + return $ret; +} + +/** + * Implements hook_views_wizard(). + */ +function node_views_wizard() { + // @todo: figure this piece out. + if (module_exists('statistics')) { + $plugins['node']['available_sorts']['node_counter-totalcount:DESC'] = t('Number of hits'); + } + +} diff --git a/core/modules/poll/poll.views.inc b/core/modules/poll/poll.views.inc new file mode 100644 index 0000000..718ff28 --- /dev/null +++ b/core/modules/poll/poll.views.inc @@ -0,0 +1,47 @@ + array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + // ---------------------------------------------------------------- + // Fields + + // poll active status + $data['poll']['active'] = array( + 'title' => t('Active'), + 'help' => t('Whether the poll is open for voting.'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Active'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + return $data; +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/views/argument/Search.php b/core/modules/search/lib/Drupal/search/Plugin/views/argument/Search.php new file mode 100644 index 0000000..ca66374 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/views/argument/Search.php @@ -0,0 +1,116 @@ +search_query)) { + $this->search_query = db_select('search_index', 'i', array('target' => 'slave'))->extend('Views\search\ViewsSearchQuery'); + $this->search_query->searchExpression($input, $this->view->base_table); + $this->search_query->publicParseSearchExpression(); + } + } + + /** + * Add this argument to the query. + */ + public function query($group_by = FALSE) { + $required = FALSE; + $this->query_parse_search_expression($this->argument); + if (!isset($this->search_query)) { + $required = TRUE; + } + else { + $words = $this->search_query->words(); + if (empty($words)) { + $required = TRUE; + } + } + if ($required) { + if ($this->operator == 'required') { + $this->query->add_where(0, 'FALSE'); + } + } + else { + $search_index = $this->ensureMyTable(); + + $search_condition = db_and(); + + // Create a new join to relate the 'search_total' table to our current 'search_index' table. + $definition = array( + 'table' => 'search_total', + 'field' => 'word', + 'left_table' => $search_index, + 'left_field' => 'word', + ); + $join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $definition); + $search_total = $this->query->add_relationship('search_total', $join, $search_index); + + $this->search_score = $this->query->add_field('', "SUM($search_index.score * $search_total.count)", 'score', array('aggregate' => TRUE)); + + if (empty($this->query->relationships[$this->relationship])) { + $base_table = $this->query->base_table; + } + else { + $base_table = $this->query->relationships[$this->relationship]['base']; + } + $search_condition->condition("$search_index.type", $base_table); + + if (!$this->search_query->simple()) { + $search_dataset = $this->query->add_table('search_dataset'); + $conditions = $this->search_query->conditions(); + $condition_conditions =& $conditions->conditions(); + foreach ($condition_conditions as $key => &$condition) { + // Take sure we just look at real conditions. + if (is_numeric($key)) { + // Replace the conditions with the table alias of views. + $this->search_query->condition_replace_string('d.', "$search_dataset.", $condition); + } + } + $search_conditions =& $search_condition->conditions(); + $search_conditions = array_merge($search_conditions, $condition_conditions); + } + else { + // Stores each condition, so and/or on the filter level will still work. + $or = db_or(); + foreach ($words as $word) { + $or->condition("$search_index.word", $word); + } + + $search_condition->condition($or); + } + + $this->query->add_where(0, $search_condition); + $this->query->add_groupby("$search_index.sid"); + $matches = $this->search_query->matches(); + $placeholder = $this->placeholder(); + $this->query->add_having_expression(0, "COUNT(*) >= $placeholder", array($placeholder => $matches)); + } + } + +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/views/field/Score.php b/core/modules/search/lib/Drupal/search/Plugin/views/field/Score.php new file mode 100644 index 0000000..4db578b --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/views/field/Score.php @@ -0,0 +1,93 @@ + ''); + $options['alternate_order'] = array('default' => 'asc'); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $style_options = $this->view->display_handler->getOption('style_options'); + if (isset($style_options['default']) && $style_options['default'] == $this->options['id']) { + $handlers = $this->view->display_handler->getHandlers('field'); + $options = array('' => t('No alternate')); + foreach ($handlers as $id => $handler) { + $options[$id] = $handler->adminLabel(); + } + + $form['alternate_sort'] = array( + '#type' => 'select', + '#title' => t('Alternative sort'), + '#description' => t('Pick an alternative default table sort field to use when the search score field is unavailable.'), + '#options' => $options, + '#default_value' => $this->options['alternate_sort'], + ); + + $form['alternate_order'] = array( + '#type' => 'select', + '#title' => t('Alternate sort order'), + '#options' => array('asc' => t('Ascending'), 'desc' => t('Descending')), + '#default_value' => $this->options['alternate_order'], + ); + } + + parent::buildOptionsForm($form, $form_state); + } + + public function query() { + // Check to see if the search filter added 'score' to the table. + // Our filter stores it as $handler->search_score -- and we also + // need to check its relationship to make sure that we're using the same + // one or obviously this won't work. + foreach ($this->view->filter as $handler) { + if (isset($handler->search_score) && $handler->relationship == $this->relationship) { + $this->field_alias = $handler->search_score; + $this->tableAlias = $handler->tableAlias; + return; + } + } + + // Hide this field if no search filter is in place. + $this->options['exclude'] = TRUE; + if (!empty($this->options['alternate_sort'])) { + if (isset($this->view->style_plugin->options['default']) && $this->view->style_plugin->options['default'] == $this->options['id']) { + // Since the style handler initiates fields, we plug these values right into the active handler. + $this->view->style_plugin->options['default'] = $this->options['alternate_sort']; + $this->view->style_plugin->options['order'] = $this->options['alternate_order']; + } + } + } + + function render($values) { + // Only render if we exist. + if (isset($this->tableAlias)) { + return parent::render($values); + } + } + +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/views/filter/Search.php b/core/modules/search/lib/Drupal/search/Plugin/views/filter/Search.php new file mode 100644 index 0000000..aff9a69 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/views/filter/Search.php @@ -0,0 +1,198 @@ + 'radios', + '#title' => t('On empty input'), + '#default_value' => $this->operator, + '#options' => array( + 'optional' => t('Show All'), + 'required' => t('Show None'), + ), + ); + } + + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + $form['value'] = array( + '#type' => 'textfield', + '#size' => 15, + '#default_value' => $this->value, + '#attributes' => array('title' => t('Enter the terms you wish to search for.')), + '#title' => empty($form_state['exposed']) ? t('Value') : '', + ); + } + + /** + * Validate the options form. + */ + public function validateExposed(&$form, &$form_state) { + if (!isset($this->options['expose']['identifier'])) { + return; + } + + $key = $this->options['expose']['identifier']; + if (!empty($form_state['values'][$key])) { + $this->query_parse_search_expression($form_state['values'][$key]); + if (count($this->search_query->words()) == 0) { + form_set_error($key, format_plural(config('search.settings')->get('index.minimum_word_size'), 'You must include at least one positive keyword with 1 character or more.', 'You must include at least one positive keyword with @count characters or more.')); + } + } + } + + /** + * Take sure that parseSearchExpression is runned and everything is set up for it. + * + * @param $input + * The search phrase which was input by the user. + */ + function query_parse_search_expression($input) { + if (!isset($this->search_query)) { + $this->parsed = TRUE; + $this->search_query = db_select('search_index', 'i', array('target' => 'slave'))->extend('Views\search\ViewsSearchQuery'); + $this->search_query->searchExpression($input, $this->view->base_table); + $this->search_query->publicParseSearchExpression(); + } + } + + /** + * Add this filter to the query. + * + * Due to the nature of fapi, the value and the operator have an unintended + * level of indirection. You will find them in $this->operator + * and $this->value respectively. + */ + public function query() { + // Since attachment views don't validate the exposed input, parse the search + // expression if required. + if (!$this->parsed) { + $this->query_parse_search_expression($this->value); + } + $required = FALSE; + if (!isset($this->search_query)) { + $required = TRUE; + } + else { + $words = $this->search_query->words(); + if (empty($words)) { + $required = TRUE; + } + } + if ($required) { + if ($this->operator == 'required') { + $this->query->add_where($this->options['group'], 'FALSE'); + } + } + else { + $search_index = $this->ensureMyTable(); + + $search_condition = db_and(); + + // Create a new join to relate the 'serach_total' table to our current 'search_index' table. + $definition = array( + 'table' => 'search_total', + 'field' => 'word', + 'left_table' => $search_index, + 'left_field' => 'word', + ); + $join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $definition); + + $search_total = $this->query->add_relationship('search_total', $join, $search_index); + + $this->search_score = $this->query->add_field('', "SUM($search_index.score * $search_total.count)", 'score', array('aggregate' => TRUE)); + + if (empty($this->query->relationships[$this->relationship])) { + $base_table = $this->query->base_table; + } + else { + $base_table = $this->query->relationships[$this->relationship]['base']; + } + $search_condition->condition("$search_index.type", $base_table); + if (!$this->search_query->simple()) { + $search_dataset = $this->query->add_table('search_dataset'); + $conditions = $this->search_query->conditions(); + $condition_conditions =& $conditions->conditions(); + foreach ($condition_conditions as $key => &$condition) { + // Take sure we just look at real conditions. + if (is_numeric($key)) { + // Replace the conditions with the table alias of views. + $this->search_query->condition_replace_string('d.', "$search_dataset.", $condition); + } + } + $search_conditions =& $search_condition->conditions(); + $search_conditions = array_merge($search_conditions, $condition_conditions); + } + else { + // Stores each condition, so and/or on the filter level will still work. + $or = db_or(); + foreach ($words as $word) { + $or->condition("$search_index.word", $word); + } + + $search_condition->condition($or); + } + + $this->query->add_where($this->options['group'], $search_condition); + $this->query->add_groupby("$search_index.sid"); + $matches = $this->search_query->matches(); + $placeholder = $this->placeholder(); + $this->query->add_having_expression($this->options['group'], "COUNT(*) >= $placeholder", array($placeholder => $matches)); + } + // Set to NULL to prevent PDO exception when views object is cached. + $this->search_query = NULL; + } + +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/views/row/View.php b/core/modules/search/lib/Drupal/search/Plugin/views/row/View.php new file mode 100644 index 0000000..86eaecb --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/views/row/View.php @@ -0,0 +1,54 @@ + TRUE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['score'] = array( + '#type' => 'checkbox', + '#title' => t('Display score'), + '#default_value' => $this->options['score'], + ); + } + + /** + * Override the behavior of the render() function. + */ + function render($row) { + return theme($this->themeFunctions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $row + )); + } + +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/views/sort/Score.php b/core/modules/search/lib/Drupal/search/Plugin/views/sort/Score.php new file mode 100644 index 0000000..c871281 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/views/sort/Score.php @@ -0,0 +1,44 @@ +search_score -- and we also + // need to check its relationship to make sure that we're using the same + // one or obviously this won't work. + foreach (array('filter', 'argument') as $type) { + foreach ($this->view->{$type} as $handler) { + if (isset($handler->search_score) && $handler->relationship == $this->relationship) { + $this->query->add_orderby(NULL, NULL, $this->options['order'], $handler->search_score); + $this->tableAlias = $handler->tableAlias; + return; + } + } + } + + // Do absolutely nothing if there is no filter/argument in place; there is no reason to + // sort on the raw scores with this handler. + } + +} diff --git a/core/modules/search/search.views.inc b/core/modules/search/search.views.inc new file mode 100644 index 0000000..13a2f68 --- /dev/null +++ b/core/modules/search/search.views.inc @@ -0,0 +1,142 @@ + array( + 'left_field' => 'nid', + 'field' => 'sid', + ), + ); + + $data['search_total']['table']['join'] = array( + 'node' => array( + 'left_table' => 'search_index', + 'left_field' => 'word', + 'field' => 'word', + ), + 'users' => array( + 'left_table' => 'search_index', + 'left_field' => 'word', + 'field' => 'word', + ) + ); + + $data['search_dataset']['table']['join'] = array( + 'node' => array( + 'left_table' => 'search_index', + 'left_field' => 'sid', + 'field' => 'sid', + 'extra' => 'search_index.type = search_dataset.type', + 'type' => 'INNER', + ), + 'users' => array( + 'left_table' => 'search_index', + 'left_field' => 'sid', + 'field' => 'sid', + 'extra' => 'search_index.type = search_dataset.type', + 'type' => 'INNER', + ), + ); + + // ---------------------------------------------------------------- + // Fields + + // score + $data['search_index']['score'] = array( + 'title' => t('Score'), + 'help' => t('The score of the search item. This will not be used if the search filter is not also present.'), + 'field' => array( + 'id' => 'search_score', + 'click sortable' => TRUE, + 'float' => TRUE, + 'no group by' => TRUE, + ), + // Information for sorting on a search score. + 'sort' => array( + 'id' => 'search_score', + 'no group by' => TRUE, + ), + ); + + // Search node links: forward links. + $data['search_node_links_from']['table']['group'] = t('Search'); + $data['search_node_links_from']['table']['join'] = array( + 'node' => array( + 'arguments' => array( + 'table' => 'search_node_links', + 'left_table' => 'node', + 'field' => 'nid', + 'left_field' => 'nid', + 'type' => 'INNER' + ), + ), + ); + $data['search_node_links_from']['sid'] = array( + 'title' => t('Links from'), + 'help' => t('Other nodes that are linked from the node.'), + 'argument' => array( + 'id' => 'node_nid', + ), + 'filter' => array( + 'id' => 'equality', + ), + ); + + // Search node links: backlinks. + $data['search_node_links_to']['table']['group'] = t('Search'); + $data['search_node_links_to']['table']['join'] = array( + 'node' => array( + 'arguments' => array( + 'table' => 'search_node_links', + 'left_table' => 'node', + 'field' => 'sid', + 'left_field' => 'nid', + 'type' => 'INNER' + ), + ), + ); + $data['search_node_links_to']['nid'] = array( + 'title' => t('Links to'), + 'help' => t('Other nodes that link to the node.'), + 'argument' => array( + 'id' => 'node_nid', + ), + 'filter' => array( + 'id' => 'equality', + ), + ); + + // search filter + $data['search_index']['keys'] = array( + 'title' => t('Search Terms'), // The item it appears as on the UI, + 'help' => t('The terms to search for.'), // The help that appears on the UI, + // Information for searching terms using the full search syntax + 'filter' => array( + 'id' => 'search', + 'no group by' => TRUE, + ), + 'argument' => array( + 'id' => 'search', + 'no group by' => TRUE, + ), + ); + + return $data; +} diff --git a/core/modules/statistics/lib/Drupal/statistics/Plugin/views/field/AccesslogPath.php b/core/modules/statistics/lib/Drupal/statistics/Plugin/views/field/AccesslogPath.php new file mode 100644 index 0000000..82b5031 --- /dev/null +++ b/core/modules/statistics/lib/Drupal/statistics/Plugin/views/field/AccesslogPath.php @@ -0,0 +1,71 @@ +options['display_as_link'])) { + $this->additional_fields['path'] = 'path'; + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['display_as_link'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide link to the page being visited. + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['display_as_link'] = array( + '#title' => t('Display as link'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['display_as_link']), + ); + parent::buildOptionsForm($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + + function render_link($data, $values) { + if (!empty($this->options['display_as_link'])) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = $this->get_value($values, 'path'); + $this->options['alter']['html'] = TRUE; + } + + return $data; + } + +} diff --git a/core/modules/statistics/statistics.views.inc b/core/modules/statistics/statistics.views.inc new file mode 100644 index 0000000..39a41fb --- /dev/null +++ b/core/modules/statistics/statistics.views.inc @@ -0,0 +1,261 @@ + array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + // totalcount + $data['node_counter']['totalcount'] = array( + 'title' => t('Total views'), + 'help' => t('The total number of times the node has been viewed.'), + + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // daycount + $data['node_counter']['daycount'] = array( + 'title' => t('Views today'), + 'help' => t('The total number of times the node has been viewed today.'), + + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // timestamp + $data['node_counter']['timestamp'] = array( + 'title' => t('Most recent view'), + 'help' => t('The most recent time the node has been viewed.'), + + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'date', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // ---------------------------------------------------------------- + // accesslog table + + $data['accesslog']['table']['group'] = t('Access log'); + + // Advertise this table as a possible base table + $data['accesslog']['table']['base'] = array( + 'field' => 'aid', + 'title' => t('Access log'), + 'help' => t('Stores site access information.'), + 'weight' => 10, + ); + + // For other base tables, explain how we join + $data['accesslog']['table']['join'] = array( + 'users' => array( + 'field' => 'uid', + 'left_field' => 'uid', + ), + ); + + // accesslog.aid + $data['accesslog']['aid'] = array( + 'title' => t('Aid'), + 'help' => t('Unique access event ID.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + 'name field' => 'wid', + 'numeric' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // session id + $data['accesslog']['sid'] = array( + 'title' => t('Session ID'), + 'help' => t('Browser session ID of user that visited page.'), + + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // title + $data['accesslog']['title'] = array( + 'title' => t('Page title'), + 'help' => t('Title of page visited.'), + + 'field' => array( + 'id' => 'accesslog_path', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'standard', + ), + ); + + // path + $data['accesslog']['path'] = array( + 'title' => t('Path'), + 'help' => t('Internal path to page visited (relative to Drupal root.)'), + + 'field' => array( + 'id' => 'accesslog_path', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + //No argument here. Can't send forward slashes as arguments. + //Can be worked around by node ID. + //(but what about aliases?) + ); + + // referrer + $data['accesslog']['url'] = array( + 'title' => t('Referrer'), + 'help' => t('Referrer URI.'), + 'field' => array( + 'id' => 'url', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // hostname + $data['accesslog']['hostname'] = array( + 'title' => t('Hostname'), + 'help' => t('Hostname of user that visited the page.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // user + $data['accesslog']['uid'] = array( + 'title' => t('User'), + 'help' => t('The user who visited the site.'), + 'relationship' => array( + 'id' => 'standard', + 'base' => 'users', + 'base field' => 'uid', + ), + ); + + // timer + $data['accesslog']['timer'] = array( + 'title' => t('Timer'), + 'help' => t('Time in milliseconds that the page took to load.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // timestamp + $data['accesslog']['timestamp'] = array( + 'title' => t('Timestamp'), + 'help' => t('Timestamp of when the page was visited.'), + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + return $data; +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/views/filter/Type.php b/core/modules/system/lib/Drupal/system/Plugin/views/filter/Type.php new file mode 100644 index 0000000..48f5c0c --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/views/filter/Type.php @@ -0,0 +1,35 @@ +value_options)) { + $this->value_title = t('Type'); + // Enable filtering by type. + $types = array(); + // Uses db_query() rather than db_select() because the query is static and + // does not include any variables. + $types = db_query('SELECT DISTINCT(type) FROM {system} ORDER BY type')->fetchAllKeyed(0, 0); + $this->value_options = $types; + } + } + +} diff --git a/core/modules/system/system.views.inc b/core/modules/system/system.views.inc new file mode 100644 index 0000000..fc0086f --- /dev/null +++ b/core/modules/system/system.views.inc @@ -0,0 +1,125 @@ + 'filename', + 'title' => t('Module/Theme/Theme engine'), + 'help' => t('Modules/Themes/Theme engines in your codebase.'), + ); + + // fields + // - filename + $data['system']['filename'] = array( + 'title' => t('Module/Theme/Theme engine filename'), + 'help' => t('The path of the primary file for this item, relative to the Drupal root; e.g. modules/node/node.module.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'string', + 'name field' => 'filename', // the field to display in the summary. + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + // - name + $data['system']['name'] = array( + 'title' => t('Module/Theme/Theme engine name'), + 'help' => t('The name of the item; e.g. node.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'string', + 'name field' => 'name', // the field to display in the summary. + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + // - type + $data['system']['type'] = array( + 'title' => t('Type'), + 'help' => t('The type of the item, either module, theme, or theme_engine.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'string', + 'name field' => 'type', // the field to display in the summary. + ), + 'filter' => array( + 'id' => 'system_type', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + // - status + $data['system']['status'] = array( + 'title' => t('Status'), + 'help' => t('Boolean indicating whether or not this item is enabled.'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + 'name field' => 'status', // the field to display in the summary. + ), + 'filter' => array( + 'id' => 'boolean', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + // - schema version + $data['system']['schema_version'] = array( + 'title' => t('Schema version'), + 'help' => t("The module's database schema version number. -1 if the module is not installed (its tables do not exist); 0 or the largest N of the module's hook_update_N() function that has either been run or existed when the module was first installed."), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + 'name field' => 'schema_version', // the field to display in the summary. + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + return $data; +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTid.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTid.php new file mode 100644 index 0000000..955f80b --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTid.php @@ -0,0 +1,61 @@ + FALSE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['set_breadcrumb'] = array( + '#type' => 'checkbox', + '#title' => t("Set the breadcrumb for the term parents"), + '#description' => t('If selected, the breadcrumb trail will include all parent terms, each one linking to this view. Note that this only works if just one term was received.'), + '#default_value' => !empty($this->options['set_breadcrumb']), + ); + } + + function set_breadcrumb(&$breadcrumb) { + if (empty($this->options['set_breadcrumb']) || !is_numeric($this->argument)) { + return; + } + + return views_taxonomy_set_breadcrumb($breadcrumb, $this); + } + + function title_query() { + $titles = array(); + $result = db_select('taxonomy_term_data', 'td') + ->fields('td', array('name')) + ->condition('td.tid', $this->value) + ->execute(); + foreach ($result as $term) { + $titles[] = check_plain($term->name); + } + return $titles; + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTidDepth.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTidDepth.php new file mode 100644 index 0000000..b5ab713 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTidDepth.php @@ -0,0 +1,161 @@ + 0); + $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE); + $options['set_breadcrumb'] = array('default' => FALSE, 'bool' => TRUE); + $options['use_taxonomy_term_path'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['depth'] = array( + '#type' => 'weight', + '#title' => t('Depth'), + '#default_value' => $this->options['depth'], + '#description' => t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'), + ); + + $form['break_phrase'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple values'), + '#description' => t('If selected, users can enter multiple values in the form of 1+2+3. Due to the number of JOINs it would require, AND will be treated as OR with this filter.'), + '#default_value' => !empty($this->options['break_phrase']), + ); + + $form['set_breadcrumb'] = array( + '#type' => 'checkbox', + '#title' => t("Set the breadcrumb for the term parents"), + '#description' => t('If selected, the breadcrumb trail will include all parent terms, each one linking to this view. Note that this only works if just one term was received.'), + '#default_value' => !empty($this->options['set_breadcrumb']), + ); + + $form['use_taxonomy_term_path'] = array( + '#type' => 'checkbox', + '#title' => t("Use Drupal's taxonomy term path to create breadcrumb links"), + '#description' => t('If selected, the links in the breadcrumb trail will be created using the standard drupal method instead of the custom views method. This is useful if you are using modules like taxonomy redirect to modify your taxonomy term links.'), + '#default_value' => !empty($this->options['use_taxonomy_term_path']), + '#states' => array( + 'visible' => array( + ':input[name="options[set_breadcrumb]"]' => array('checked' => TRUE), + ), + ), + ); + parent::buildOptionsForm($form, $form_state); + } + + function set_breadcrumb(&$breadcrumb) { + if (empty($this->options['set_breadcrumb']) || !is_numeric($this->argument)) { + return; + } + + return views_taxonomy_set_breadcrumb($breadcrumb, $this); + } + + /** + * Override default_actions() to remove summary actions. + */ + function default_actions($which = NULL) { + if ($which) { + if (in_array($which, array('ignore', 'not found', 'empty', 'default'))) { + return parent::default_actions($which); + } + return; + } + $actions = parent::default_actions(); + unset($actions['summary asc']); + unset($actions['summary desc']); + unset($actions['summary asc by count']); + unset($actions['summary desc by count']); + return $actions; + } + + public function query($group_by = FALSE) { + $this->ensureMyTable(); + + if (!empty($this->options['break_phrase'])) { + $tids = new \stdClass(); + $tids->value = $this->argument; + $tids = $this->breakPhrase($this->argument, $tids); + if ($tids->value == array(-1)) { + return FALSE; + } + + if (count($tids->value) > 1) { + $operator = 'IN'; + } + else { + $operator = '='; + } + + $tids = $tids->value; + } + else { + $operator = "="; + $tids = $this->argument; + } + // Now build the subqueries. + $subquery = db_select('taxonomy_index', 'tn'); + $subquery->addField('tn', 'nid'); + $where = db_or()->condition('tn.tid', $tids, $operator); + $last = "tn"; + + if ($this->options['depth'] > 0) { + $subquery->leftJoin('taxonomy_term_hierarchy', 'th', "th.tid = tn.tid"); + $last = "th"; + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.parent = th$count.tid"); + $where->condition("th$count.tid", $tids, $operator); + $last = "th$count"; + } + } + elseif ($this->options['depth'] < 0) { + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.tid = th$count.parent"); + $where->condition("th$count.tid", $tids, $operator); + $last = "th$count"; + } + } + + $subquery->condition($where); + $this->query->add_where(0, "$this->tableAlias.$this->realField", $subquery, 'IN'); + } + + function title() { + $term = taxonomy_term_load($this->argument); + if (!empty($term)) { + return check_plain($term->name); + } + // TODO review text + return t('No name'); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTidDepthModifier.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTidDepthModifier.php new file mode 100644 index 0000000..86608cb --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/IndexTidDepthModifier.php @@ -0,0 +1,78 @@ +view->args[$this->position]) ? $this->view->args[$this->position] : NULL; + if (!is_numeric($argument)) { + return; + } + + if ($argument > 10) { + $argument = 10; + } + + if ($argument < -10) { + $argument = -10; + } + + // figure out which argument preceded us. + $keys = array_reverse(array_keys($this->view->argument)); + $skip = TRUE; + foreach ($keys as $key) { + if ($key == $this->options['id']) { + $skip = FALSE; + continue; + } + + if ($skip) { + continue; + } + + if (empty($this->view->argument[$key])) { + continue; + } + + if (isset($handler)) { + unset($handler); + } + + $handler = &$this->view->argument[$key]; + if (empty($handler->definition['accept depth modifier'])) { + continue; + } + + // Finally! + $handler->options['depth'] = $argument; + } + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/Taxonomy.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/Taxonomy.php new file mode 100644 index 0000000..33a93d3 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/Taxonomy.php @@ -0,0 +1,40 @@ +argument) { + $term = taxonomy_term_load($this->argument); + if (!empty($term)) { + return check_plain($term->name); + } + } + // TODO review text + return t('No name'); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/VocabularyMachineName.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/VocabularyMachineName.php new file mode 100644 index 0000000..c36e406 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/VocabularyMachineName.php @@ -0,0 +1,41 @@ +addField('v', 'name'); + $query->condition('v.machine_name', $this->argument); + $title = $query->execute()->fetchField(); + + if (empty($title)) { + return t('No vocabulary'); + } + + return check_plain($title); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/VocabularyVid.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/VocabularyVid.php new file mode 100644 index 0000000..f479f66 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument/VocabularyVid.php @@ -0,0 +1,40 @@ +addField('v', 'name'); + $query->condition('v.vid', $this->argument); + $title = $query->execute()->fetchField(); + if (empty($title)) { + return t('No vocabulary'); + } + + return check_plain($title); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_default/Tid.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_default/Tid.php new file mode 100644 index 0000000..e657ea6 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_default/Tid.php @@ -0,0 +1,170 @@ +options['vids'])) { + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($this->options['vids'] as $vid) { + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name; + } + } + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['term_page'] = array('default' => TRUE, 'bool' => TRUE); + $options['node'] = array('default' => FALSE, 'bool' => TRUE); + $options['anyall'] = array('default' => ','); + $options['limit'] = array('default' => FALSE, 'bool' => TRUE); + $options['vocabularies'] = array('default' => array()); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['term_page'] = array( + '#type' => 'checkbox', + '#title' => t('Load default filter from term page'), + '#default_value' => $this->options['term_page'], + ); + $form['node'] = array( + '#type' => 'checkbox', + '#title' => t('Load default filter from node page, that\'s good for related taxonomy blocks'), + '#default_value' => $this->options['node'], + ); + + $form['limit'] = array( + '#type' => 'checkbox', + '#title' => t('Limit terms by vocabulary'), + '#default_value' => $this->options['limit'], + '#states' => array( + 'visible' => array( + ':input[name="options[argument_default][taxonomy_tid][node]"]' => array('checked' => TRUE), + ), + ), + ); + + $options = array(); + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + $form['vocabularies'] = array( + '#type' => 'checkboxes', + '#title' => t('Vocabularies'), + '#options' => $options, + '#default_value' => $this->options['vocabularies'], + '#states' => array( + 'visible' => array( + ':input[name="options[argument_default][taxonomy_tid][limit]"]' => array('checked' => TRUE), + ':input[name="options[argument_default][taxonomy_tid][node]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['anyall'] = array( + '#type' => 'radios', + '#title' => t('Multiple-value handling'), + '#default_value' => $this->options['anyall'], + '#options' => array( + ',' => t('Filter to items that share all terms'), + '+' => t('Filter to items that share any term'), + ), + '#states' => array( + 'visible' => array( + ':input[name="options[argument_default][taxonomy_tid][node]"]' => array('checked' => TRUE), + ), + ), + ); + } + + public function submitOptionsForm(&$form, &$form_state, &$options = array()) { + // Filter unselected items so we don't unnecessarily store giant arrays. + $options['vocabularies'] = array_filter($options['vocabularies']); + } + + function get_argument() { + // Load default argument from taxonomy page. + if (!empty($this->options['term_page'])) { + if (arg(0) == 'taxonomy' && arg(1) == 'term' && is_numeric(arg(2))) { + return arg(2); + } + } + // Load default argument from node. + if (!empty($this->options['node'])) { + foreach (range(1, 3) as $i) { + $node = menu_get_object('node', $i); + if (!empty($node)) { + break; + } + } + // Just check, if a node could be detected. + if ($node) { + $taxonomy = array(); + $fields = field_info_instances('node', $node->type); + foreach ($fields as $name => $info) { + $field_info = field_info_field($name); + if ($field_info['type'] == 'taxonomy_term_reference') { + $items = field_get_items('node', $node, $name); + if (is_array($items)) { + foreach ($items as $item) { + $taxonomy[$item['tid']] = $field_info['settings']['allowed_values'][0]['vocabulary']; + } + } + } + } + if (!empty($this->options['limit'])) { + $tids = array(); + // filter by vocabulary + foreach ($taxonomy as $tid => $vocab) { + if (!empty($this->options['vocabularies'][$vocab])) { + $tids[] = $tid; + } + } + return implode($this->options['anyall'], $tids); + } + // Return all tids. + else { + return implode($this->options['anyall'], array_keys($taxonomy)); + } + } + } + + // If the current page is a view that takes tid as an argument, + // find the tid argument and return it. + $views_page = views_get_page_view(); + if ($views_page && isset($views_page->argument['tid'])) { + return $views_page->argument['tid']->argument; + } + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/Term.php new file mode 100644 index 0000000..955be61 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/argument_validator/Term.php @@ -0,0 +1,202 @@ +options['vids'])) { + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($this->options['vids'] as $vid) { + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name; + } + } + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['vocabularies'] = array('default' => array()); + $options['type'] = array('default' => 'tid'); + $options['transform'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $vocabularies = taxonomy_vocabulary_get_names(); + $options = array(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + $form['vocabularies'] = array( + '#type' => 'checkboxes', + '#prefix' => '
', + '#suffix' => '
', + '#title' => t('Vocabularies'), + '#options' => $options, + '#default_value' => $this->options['vocabularies'], + '#description' => t('If you wish to validate for specific vocabularies, check them; if none are checked, all terms will pass.'), + ); + + $form['type'] = array( + '#type' => 'select', + '#title' => t('Filter value type'), + '#options' => array( + 'tid' => t('Term ID'), + 'tids' => t('Term IDs separated by , or +'), + 'name' => t('Term name'), + 'convert' => t('Term name converted to Term ID'), + ), + '#default_value' => $this->options['type'], + '#description' => t('Select the form of this filter value; if using term name, it is generally more efficient to convert it to a term ID and use Taxonomy: Term ID rather than Taxonomy: Term Name" as the filter.'), + ); + + $form['transform'] = array( + '#type' => 'checkbox', + '#title' => t('Transform dashes in URL to spaces in term name filter values'), + '#default_value' => $this->options['transform'], + ); + } + + public function submitOptionsForm(&$form, &$form_state, &$options = array()) { + // Filter unselected items so we don't unnecessarily store giant arrays. + $options['vocabularies'] = array_filter($options['vocabularies']); + } + + function validate_argument($argument) { + $vocabularies = array_filter($this->options['vocabularies']); + $type = $this->options['type']; + $transform = $this->options['transform']; + + switch ($type) { + case 'tid': + if (!is_numeric($argument)) { + return FALSE; + } + // @todo Deal with missing addTag('term access') that was removed when + // the db_select that was replaced by the entity_load. + $term = entity_load('taxonomy_term', $argument); + if (!$term) { + return FALSE; + } + $this->argument->validated_title = check_plain($term->name); + return empty($vocabularies) || !empty($vocabularies[$term->vocabulary_machine_name]); + + case 'tids': + // An empty argument is not a term so doesn't pass. + if (empty($argument)) { + return FALSE; + } + + $tids = new stdClass(); + $tids->value = $argument; + $tids = $this->breakPhrase($argument, $tids); + if ($tids->value == array(-1)) { + return FALSE; + } + + $test = drupal_map_assoc($tids->value); + $titles = array(); + + // check, if some tids already verified + static $validated_cache = array(); + foreach ($test as $tid) { + if (isset($validated_cache[$tid])) { + if ($validated_cache[$tid] === FALSE) { + return FALSE; + } + else { + $titles[] = $validated_cache[$tid]; + unset($test[$tid]); + } + } + } + + // if unverified tids left - verify them and cache results + if (count($test)) { + $result = entity_load_multiple('taxonomy_term', $test); + foreach ($result as $term) { + if ($vocabularies && empty($vocabularies[$term->vocabulary_machine_name])) { + $validated_cache[$term->id()] = FALSE; + return FALSE; + } + + $titles[] = $validated_cache[$term->id()] = check_plain($term->name); + unset($test[$term->id()]); + } + } + + // Remove duplicate titles + $titles = array_unique($titles); + + $this->argument->validated_title = implode($tids->operator == 'or' ? ' + ' : ', ', $titles); + // If this is not empty, we did not find a tid. + return empty($test); + + case 'name': + case 'convert': + $terms = entity_load_multiple_by_properties('taxonomy_term', array('name' => $argument)); + $term = reset($terms); + if ($transform) { + $term->name = str_replace(' ', '-', $term->name); + } + + if ($term && (empty($vocabularies) || !empty($vocabularies[$term->vocabulary_machine_name]))) { + if ($type == 'convert') { + $this->argument->argument = $term->id(); + } + $this->argument->validated_title = check_plain($term->name); + return TRUE; + } + return FALSE; + } + } + + function process_summary_arguments(&$args) { + $type = $this->options['type']; + $transform = $this->options['transform']; + $vocabularies = array_filter($this->options['vocabularies']); + + if ($type == 'convert') { + $arg_keys = array_flip($args); + + $result = entity_load_multiple('taxonomy_term', $args); + + if ($transform) { + foreach ($result as $term) { + $term->name = str_replace(' ', '-', $term->name); + } + } + + foreach ($result as $tid => $term) { + $args[$arg_keys[$tid]] = $term; + } + } + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/Language.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/Language.php new file mode 100644 index 0000000..7f2319a --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/Language.php @@ -0,0 +1,32 @@ +get_value($values); + $language = language_load($value); + $value = $language ? $language->name : ''; + + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/LinkEdit.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/LinkEdit.php new file mode 100644 index 0000000..fcdd258 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/LinkEdit.php @@ -0,0 +1,79 @@ +additional_fields['tid'] = 'tid'; + $this->additional_fields['vid'] = 'vid'; + $this->additional_fields['vocabulary_machine_name'] = array( + 'table' => 'taxonomy_vocabulary', + 'field' => 'machine_name', + ); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['text'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + parent::buildOptionsForm($form, $form_state); + } + + public function query() { + $this->ensureMyTable(); + $this->add_additional_fields(); + } + + function render($values) { + // Check there is an actual value, as on a relationship there may not be. + if ($tid = $this->get_value($values, 'tid')) { + // Mock a term object for taxonomy_term_access(). Use machine name and + // vid to ensure compatibility with vid based and machine name based + // access checks. See http://drupal.org/node/995156 + $term = entity_create('taxonomy_term', array( + 'vid' => $values->{$this->aliases['vid']}, + 'vocabulary_machine_name' => $values->{$this->aliases['vocabulary_machine_name']}, + )); + if (taxonomy_term_access('edit', $term)) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + return l($text, 'taxonomy/term/'. $tid . '/edit', array('query' => drupal_get_destination())); + } + } + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/Taxonomy.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/Taxonomy.php new file mode 100644 index 0000000..ec56116 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/Taxonomy.php @@ -0,0 +1,102 @@ +additional_fields['vid'] = 'vid'; + $this->additional_fields['tid'] = 'tid'; + $this->additional_fields['vocabulary_machine_name'] = array( + 'table' => 'taxonomy_vocabulary', + 'field' => 'machine_name', + ); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_to_taxonomy'] = array('default' => FALSE, 'bool' => TRUE); + $options['convert_spaces'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to taxonomy option + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_taxonomy'] = array( + '#title' => t('Link this field to its taxonomy term page'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_taxonomy']), + ); + $form['convert_spaces'] = array( + '#title' => t('Convert spaces in term names to hyphens'), + '#description' => t('This allows links to work with Views taxonomy term arguments.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['convert_spaces']), + ); + parent::buildOptionsForm($form, $form_state); + } + + /** + * Render whatever the data is as a link to the taxonomy. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + $tid = $this->get_value($values, 'tid'); + if (!empty($this->options['link_to_taxonomy']) && !empty($tid) && $data !== NULL && $data !== '') { + $term = entity_create('taxonomy_term', array( + 'tid' => $tid, + 'vid' => $this->get_value($values, 'vid'), + 'vocabulary_machine_name' => $values->{$this->aliases['vocabulary_machine_name']}, + )); + $this->options['alter']['make_link'] = TRUE; + $uri = $term->uri(); + $this->options['alter']['path'] = $uri['path']; + } + + if (!empty($this->options['convert_spaces'])) { + $data = str_replace(' ', '-', $data); + } + + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/TaxonomyIndexTid.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/TaxonomyIndexTid.php new file mode 100644 index 0000000..0b49698 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/field/TaxonomyIndexTid.php @@ -0,0 +1,161 @@ +base_table and no if here? + if ($view->storage->base_table == 'node_revision') { + $this->additional_fields['nid'] = array('table' => 'node_revision', 'field' => 'nid'); + } + else { + $this->additional_fields['nid'] = array('table' => 'node', 'field' => 'nid'); + } + + // Convert legacy vids option to machine name vocabularies. + if (!empty($this->options['vids'])) { + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($this->options['vids'] as $vid) { + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name; + } + } + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['link_to_taxonomy'] = array('default' => TRUE, 'bool' => TRUE); + $options['limit'] = array('default' => FALSE, 'bool' => TRUE); + $options['vocabularies'] = array('default' => array()); + + return $options; + } + + /** + * Provide "link to term" option. + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_taxonomy'] = array( + '#title' => t('Link this field to its term page'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_taxonomy']), + ); + + $form['limit'] = array( + '#type' => 'checkbox', + '#title' => t('Limit terms by vocabulary'), + '#default_value' => $this->options['limit'], + ); + + $options = array(); + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + $form['vocabularies'] = array( + '#type' => 'checkboxes', + '#title' => t('Vocabularies'), + '#options' => $options, + '#default_value' => $this->options['vocabularies'], + '#states' => array( + 'visible' => array( + ':input[name="options[limit]"]' => array('checked' => TRUE), + ), + ), + + ); + + parent::buildOptionsForm($form, $form_state); + } + + /** + * Add this term to the query + */ + public function query() { + $this->add_additional_fields(); + } + + function pre_render(&$values) { + $this->field_alias = $this->aliases['nid']; + $nids = array(); + foreach ($values as $result) { + if (!empty($result->{$this->aliases['nid']})) { + $nids[] = $result->{$this->aliases['nid']}; + } + } + + if ($nids) { + $query = db_select('taxonomy_term_data', 'td'); + $query->innerJoin('taxonomy_index', 'tn', 'td.tid = tn.tid'); + $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->addField('tn', 'nid', 'node_nid'); + $query->addField('tv', 'name', 'vocabulary'); + $query->addField('tv', 'machine_name', 'vocabulary_machine_name'); + $query->orderby('td.weight'); + $query->orderby('td.name'); + $query->condition('tn.nid', $nids); + $query->addTag('term_access'); + $vocabs = array_filter($this->options['vocabularies']); + if (!empty($this->options['limit']) && !empty($vocabs)) { + $query->condition('tv.machine_name', $vocabs); + } + $result = $query->execute(); + + foreach ($result as $term) { + $this->items[$term->node_nid][$term->tid]['name'] = check_plain($term->name); + $this->items[$term->node_nid][$term->tid]['tid'] = $term->tid; + $this->items[$term->node_nid][$term->tid]['vocabulary_machine_name'] = check_plain($term->vocabulary_machine_name); + $this->items[$term->node_nid][$term->tid]['vocabulary'] = check_plain($term->vocabulary); + + if (!empty($this->options['link_to_taxonomy'])) { + $this->items[$term->node_nid][$term->tid]['make_link'] = TRUE; + $this->items[$term->node_nid][$term->tid]['path'] = 'taxonomy/term/' . $term->tid; + } + } + } + } + + function render_item($count, $item) { + return $item['name']; + } + + function document_self_tokens(&$tokens) { + $tokens['[' . $this->options['id'] . '-tid' . ']'] = t('The taxonomy term ID for the term.'); + $tokens['[' . $this->options['id'] . '-name' . ']'] = t('The taxonomy term name for the term.'); + $tokens['[' . $this->options['id'] . '-vocabulary-machine-name' . ']'] = t('The machine name for the vocabulary the term belongs to.'); + $tokens['[' . $this->options['id'] . '-vocabulary' . ']'] = t('The name for the vocabulary the term belongs to.'); + } + + function add_self_tokens(&$tokens, $item) { + foreach (array('tid', 'name', 'vocabulary_machine_name', 'vocabulary') as $token) { + // Replace _ with - for the vocabulary machine name. + $tokens['[' . $this->options['id'] . '-' . str_replace('_', '-', $token) . ']'] = isset($item[$token]) ? $item[$token] : ''; + } + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/TaxonomyIndexTid.php new file mode 100644 index 0000000..fa8f3de --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/TaxonomyIndexTid.php @@ -0,0 +1,376 @@ +definition['vocabulary'])) { + $this->options['vocabulary'] = $this->definition['vocabulary']; + } + + // Convert legacy vid option to machine name vocabulary. + if (isset($this->options['vid']) && !empty($this->options['vid']) & empty($this->options['vocabulary'])) { + $vocabularies = taxonomy_vocabulary_get_names(); + $vid = $this->options['vid']; + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabulary'] = $vocabularies[$vid]->machine_name; + } + } + } + + public function hasExtraOptions() { return TRUE; } + + function get_value_options() { /* don't overwrite the value options */ } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['type'] = array('default' => 'textfield'); + $options['limit'] = array('default' => TRUE, 'bool' => TRUE); + $options['vocabulary'] = array('default' => 0); + $options['hierarchy'] = array('default' => 0); + $options['error_message'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + public function buildExtraOptionsForm(&$form, &$form_state) { + $vocabularies = taxonomy_vocabulary_get_names(); + $options = array(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + if ($this->options['limit']) { + // We only do this when the form is displayed. + if (empty($this->options['vocabulary'])) { + $first_vocabulary = reset($vocabularies); + $this->options['vocabulary'] = $first_vocabulary->machine_name; + } + + if (empty($this->definition['vocabulary'])) { + $form['vocabulary'] = array( + '#type' => 'radios', + '#title' => t('Vocabulary'), + '#options' => $options, + '#description' => t('Select which vocabulary to show terms for in the regular options.'), + '#default_value' => $this->options['vocabulary'], + ); + } + } + + $form['type'] = array( + '#type' => 'radios', + '#title' => t('Selection type'), + '#options' => array('select' => t('Dropdown'), 'textfield' => t('Autocomplete')), + '#default_value' => $this->options['type'], + ); + + $form['hierarchy'] = array( + '#type' => 'checkbox', + '#title' => t('Show hierarchy in dropdown'), + '#default_value' => !empty($this->options['hierarchy']), + '#states' => array( + 'visible' => array( + ':input[name="options[type]"]' => array('value' => 'select'), + ), + ), + ); + } + + function value_form(&$form, &$form_state) { + $vocabulary = taxonomy_vocabulary_machine_name_load($this->options['vocabulary']); + if (empty($vocabulary) && $this->options['limit']) { + $form['markup'] = array( + '#markup' => '
' . t('An invalid vocabulary is selected. Please change it in the options.') . '
', + ); + return; + } + + if ($this->options['type'] == 'textfield') { + $default = ''; + if ($this->value) { + $result = db_select('taxonomy_term_data', 'td') + ->fields('td') + ->condition('td.tid', $this->value) + ->execute(); + foreach ($result as $term) { + if ($default) { + $default .= ', '; + } + $default .= $term->name; + } + } + + $form['value'] = array( + '#title' => $this->options['limit'] ? t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)) : t('Select terms'), + '#type' => 'textfield', + '#default_value' => $default, + ); + + if ($this->options['limit']) { + $form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->vid; + } + } + else { + if (!empty($this->options['hierarchy']) && $this->options['limit']) { + $tree = taxonomy_get_tree($vocabulary->vid); + $options = array(); + + if ($tree) { + foreach ($tree as $term) { + $choice = new stdClass(); + $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name); + $options[] = $choice; + } + } + } + else { + $options = array(); + $query = db_select('taxonomy_term_data', 'td'); + $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->orderby('tv.weight'); + $query->orderby('tv.name'); + $query->orderby('td.weight'); + $query->orderby('td.name'); + $query->addTag('term_access'); + if ($this->options['limit']) { + $query->condition('tv.machine_name', $vocabulary->machine_name); + } + $result = $query->execute(); + foreach ($result as $term) { + $options[$term->tid] = $term->name; + } + } + + $default_value = (array) $this->value; + + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (!empty($this->options['expose']['reduce'])) { + $options = $this->reduce_value_options($options); + + if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) { + $default_value = array(); + } + } + + if (empty($this->options['expose']['multiple'])) { + if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) { + $default_value = 'All'; + } + elseif (empty($default_value)) { + $keys = array_keys($options); + $default_value = array_shift($keys); + } + // Due to #1464174 there is a chance that array('') was saved in the admin ui. + // Let's choose a safe default value. + elseif ($default_value == array('')) { + $default_value = 'All'; + } + else { + $copy = $default_value; + $default_value = array_shift($copy); + } + } + } + $form['value'] = array( + '#type' => 'select', + '#title' => $this->options['limit'] ? t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)) : t('Select terms'), + '#multiple' => TRUE, + '#options' => $options, + '#size' => min(9, count($options)), + '#default_value' => $default_value, + ); + + if (!empty($form_state['exposed']) && isset($identifier) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $default_value; + } + } + + if (empty($form_state['exposed'])) { + // Retain the helper option + $this->helper->buildOptionsForm($form, $form_state); + } + } + + function value_validate($form, &$form_state) { + // We only validate if they've chosen the text field style. + if ($this->options['type'] != 'textfield') { + return; + } + + $values = drupal_explode_tags($form_state['values']['options']['value']); + $tids = $this->validate_term_strings($form['value'], $values); + + if ($tids) { + $form_state['values']['options']['value'] = $tids; + } + } + + public function acceptExposedInput($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + // If view is an attachment and is inheriting exposed filters, then assume + // exposed input has already been validated + if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) { + $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']]; + } + + // If it's non-required and there's no value don't bother filtering. + if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) { + return FALSE; + } + + $rc = parent::acceptExposedInput($input); + if ($rc) { + // If we have previously validated input, override. + if (isset($this->validated_exposed_input)) { + $this->value = $this->validated_exposed_input; + } + } + + return $rc; + } + + public function validateExposed(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + $identifier = $this->options['expose']['identifier']; + + // We only validate if they've chosen the text field style. + if ($this->options['type'] != 'textfield') { + if ($form_state['values'][$identifier] != 'All') { + $this->validated_exposed_input = (array) $form_state['values'][$identifier]; + } + return; + } + + if (empty($this->options['expose']['identifier'])) { + return; + } + + $values = drupal_explode_tags($form_state['values'][$identifier]); + + $tids = $this->validate_term_strings($form[$identifier], $values); + if ($tids) { + $this->validated_exposed_input = $tids; + } + } + + /** + * Validate the user string. Since this can come from either the form + * or the exposed filter, this is abstracted out a bit so it can + * handle the multiple input sources. + * + * @param $form + * The form which is used, either the views ui or the exposed filters. + * @param $values + * The taxonomy names which will be converted to tids. + * + * @return array + * The taxonomy ids fo all validated terms. + */ + function validate_term_strings(&$form, $values) { + if (empty($values)) { + return array(); + } + + $tids = array(); + $names = array(); + $missing = array(); + foreach ($values as $value) { + $missing[strtolower($value)] = TRUE; + $names[] = $value; + } + + if (!$names) { + return FALSE; + } + + $query = db_select('taxonomy_term_data', 'td'); + $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->condition('td.name', $names); + $query->condition('tv.machine_name', $this->options['vocabulary']); + $query->addTag('term_access'); + $result = $query->execute(); + foreach ($result as $term) { + unset($missing[strtolower($term->name)]); + $tids[] = $term->tid; + } + + if ($missing && !empty($this->options['error_message'])) { + form_error($form, format_plural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing))))); + } + elseif ($missing && empty($this->options['error_message'])) { + $tids = array(0); + } + + return $tids; + } + + function value_submit($form, &$form_state) { + // prevent array_filter from messing up our arrays in parent submit. + } + + public function buildExposeForm(&$form, &$form_state) { + parent::buildExposeForm($form, $form_state); + if ($this->options['type'] != 'select') { + unset($form['expose']['reduce']); + } + $form['error_message'] = array( + '#type' => 'checkbox', + '#title' => t('Display error message'), + '#default_value' => !empty($this->options['error_message']), + ); + } + + public function adminSummary() { + // set up $this->value_options for the parent summary + $this->value_options = array(); + + if ($this->value) { + $this->value = array_filter($this->value); + $result = db_select('taxonomy_term_data', 'td') + ->fields('td') + ->condition('td.tid', $this->value) + ->execute(); + foreach ($result as $term) { + $this->value_options[$term->tid] = $term->name; + } + } + return parent::adminSummary(); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/TaxonomyIndexTidDepth.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/TaxonomyIndexTidDepth.php new file mode 100644 index 0000000..ba96a52 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/TaxonomyIndexTidDepth.php @@ -0,0 +1,111 @@ + t('Is one of'), + ); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['depth'] = array('default' => 0); + + return $options; + } + + public function buildExtraOptionsForm(&$form, &$form_state) { + parent::buildExtraOptionsForm($form, $form_state); + + $form['depth'] = array( + '#type' => 'weight', + '#title' => t('Depth'), + '#default_value' => $this->options['depth'], + '#description' => t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'), + ); + } + + public function query() { + // If no filter values are present, then do nothing. + if (count($this->value) == 0) { + return; + } + elseif (count($this->value) == 1) { + // Somethis $this->value is an array with a single element so convert it. + if (is_array($this->value)) { + $this->value = current($this->value); + } + $operator = '='; + } + else { + $operator = 'IN';# " IN (" . implode(', ', array_fill(0, sizeof($this->value), '%d')) . ")"; + } + + // The normal use of ensureMyTable() here breaks Views. + // So instead we trick the filter into using the alias of the base table. + // See http://drupal.org/node/271833 + // If a relationship is set, we must use the alias it provides. + if (!empty($this->relationship)) { + $this->tableAlias = $this->relationship; + } + // If no relationship, then use the alias of the base table. + elseif (isset($this->query->table_queue[$this->query->base_table]['alias'])) { + $this->tableAlias = $this->query->table_queue[$this->query->base_table]['alias']; + } + // This should never happen, but if it does, we fail quietly. + else { + return; + } + + // Now build the subqueries. + $subquery = db_select('taxonomy_index', 'tn'); + $subquery->addField('tn', 'nid'); + $where = db_or()->condition('tn.tid', $this->value, $operator); + $last = "tn"; + + if ($this->options['depth'] > 0) { + $subquery->leftJoin('taxonomy_term_hierarchy', 'th', "th.tid = tn.tid"); + $last = "th"; + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.parent = th$count.tid"); + $where->condition("th$count.tid", $this->value, $operator); + $last = "th$count"; + } + } + elseif ($this->options['depth'] < 0) { + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.tid = th$count.parent"); + $where->condition("th$count.tid", $this->value, $operator); + $last = "th$count"; + } + } + + $subquery->condition($where); + $this->query->add_where($this->options['group'], "$this->tableAlias.$this->realField", $subquery, 'IN'); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/VocabularyMachineName.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/VocabularyMachineName.php new file mode 100644 index 0000000..8be71e6 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/VocabularyMachineName.php @@ -0,0 +1,37 @@ +value_options)) { + return; + } + + $this->value_options = array(); + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($vocabularies as $voc) { + $this->value_options[$voc->machine_name] = $voc->name; + } + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/VocabularyVid.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/VocabularyVid.php new file mode 100644 index 0000000..1264319 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/filter/VocabularyVid.php @@ -0,0 +1,37 @@ +value_options)) { + return; + } + + $this->value_options = array(); + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($vocabularies as $voc) { + $this->value_options[$voc->vid] = $voc->name; + } + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/relationship/NodeTermData.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/relationship/NodeTermData.php new file mode 100644 index 0000000..ab2021a --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/relationship/NodeTermData.php @@ -0,0 +1,105 @@ +options['vids'])) { + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($this->options['vids'] as $vid) { + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name; + } + } + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['vocabularies'] = array('default' => array()); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $vocabularies = taxonomy_vocabulary_get_names(); + $options = array(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + $form['vocabularies'] = array( + '#type' => 'checkboxes', + '#title' => t('Vocabularies'), + '#options' => $options, + '#default_value' => $this->options['vocabularies'], + '#description' => t('Choose which vocabularies you wish to relate. Remember that every term found will create a new record, so this relationship is best used on just one vocabulary that has only one term per node.'), + ); + parent::buildOptionsForm($form, $form_state); + } + + /** + * Called to implement a relationship in a query. + */ + public function query() { + $this->ensureMyTable(); + + $def = $this->definition; + $def['table'] = 'taxonomy_term_data'; + + if (!array_filter($this->options['vocabularies'])) { + $taxonomy_index = $this->query->add_table('taxonomy_index', $this->relationship); + $def['left_table'] = $taxonomy_index; + $def['left_field'] = 'tid'; + $def['field'] = 'tid'; + $def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER'; + } + else { + // If vocabularies are supplied join a subselect instead + $def['left_table'] = $this->tableAlias; + $def['left_field'] = 'nid'; + $def['field'] = 'nid'; + $def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER'; + $def['adjusted'] = TRUE; + + $query = db_select('taxonomy_term_data', 'td'); + $query->addJoin($def['type'], 'taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->addJoin($def['type'], 'taxonomy_index', 'tn', 'tn.tid = td.tid'); + $query->condition('tv.machine_name', array_filter($this->options['vocabularies'])); + $query->addTag('term_access'); + $query->fields('td'); + $query->fields('tn', array('nid')); + $def['table formula'] = $query; + } + + $join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $def); + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, 'taxonomy_term_data', $this->relationship); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/wizard/TaxonomyTerm.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/wizard/TaxonomyTerm.php new file mode 100644 index 0000000..984f533 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/views/wizard/TaxonomyTerm.php @@ -0,0 +1,72 @@ + 'tid', + 'table' => 'taxonomy_term_data', + 'field' => 'tid', + 'exclude' => TRUE, + 'alter' => array( + 'alter_text' => TRUE, + 'text' => 'taxonomy/term/[tid]' + ) + ); + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::default_display_options(). + */ + protected function default_display_options() { + $display_options = parent::default_display_options(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + /* Field: Taxonomy: Term */ + $display_options['fields']['name']['id'] = 'name'; + $display_options['fields']['name']['table'] = 'taxonomy_term_data'; + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['label'] = ''; + $display_options['fields']['name']['alter']['alter_text'] = 0; + $display_options['fields']['name']['alter']['make_link'] = 0; + $display_options['fields']['name']['alter']['absolute'] = 0; + $display_options['fields']['name']['alter']['trim'] = 0; + $display_options['fields']['name']['alter']['word_boundary'] = 0; + $display_options['fields']['name']['alter']['ellipsis'] = 0; + $display_options['fields']['name']['alter']['strip_tags'] = 0; + $display_options['fields']['name']['alter']['html'] = 0; + $display_options['fields']['name']['hide_empty'] = 0; + $display_options['fields']['name']['empty_zero'] = 0; + $display_options['fields']['name']['link_to_taxonomy'] = 1; + + return $display_options; + } + +} diff --git a/core/modules/taxonomy/taxonomy.views.inc b/core/modules/taxonomy/taxonomy.views.inc new file mode 100644 index 0000000..1779bf0 --- /dev/null +++ b/core/modules/taxonomy/taxonomy.views.inc @@ -0,0 +1,504 @@ + array( + 'left_field' => 'vid', + 'field' => 'vid', + ), + ); + + // vocabulary name + $data['taxonomy_vocabulary']['name'] = array( + 'title' => t('Name'), // The item it appears as on the UI, + 'field' => array( + 'help' => t('Name of the vocabulary a term is a member of. This will be the vocabulary that whichever term the "Taxonomy: Term" field is; and can similarly cause duplicates.'), + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + 'help' => t('The taxonomy vocabulary name'), + ), + ); + $data['taxonomy_vocabulary']['machine_name'] = array( + 'title' => t('Machine name'), // The item it appears as on the UI, + 'field' => array( + 'help' => t('Machine-Name of the vocabulary a term is a member of. This will be the vocabulary that whichever term the "Taxonomy: Term" field is; and can similarly cause duplicates.'), + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'help' => t('Filter the results of "Taxonomy: Term" to a particular vocabulary.'), + 'id' => 'vocabulary_machine_name', + ), + 'argument' => array( + 'help' => t('Filter the results of "Taxonomy: Term" to a particular vocabulary.'), + 'id' => 'vocabulary_machine_name', + ), + ); + $data['taxonomy_vocabulary']['vid'] = array( + 'title' => t('Vocabulary ID'), // The item it appears as on the UI, + 'help' => t('The taxonomy vocabulary ID'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'vocabulary_vid', + 'name field' => 'name', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + $data['taxonomy_vocabulary']['description'] = array( + 'title' => t('Description'), // The item it appears as on the UI, + 'help' => t('The taxonomy vocabulary description'), + 'field' => array( + 'id' => 'standard', + ), + ); + $data['taxonomy_vocabulary']['weight'] = array( + 'title' => t('Weight'), + 'help' => t('The taxonomy vocabulary weight'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + 'name field' => 'weight', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'numeric', + ), + ); + + // ---------------------------------------------------------------------- + // taxonomy_term_data table + + $data['taxonomy_term_data']['table']['group'] = t('Taxonomy term'); + $data['taxonomy_term_data']['table']['base'] = array( + 'field' => 'tid', + 'title' => t('Term'), + 'help' => t('Taxonomy terms are attached to nodes.'), + 'access query tag' => 'term_access', + ); + $data['taxonomy_term_data']['table']['entity type'] = 'taxonomy_term'; + + // The term data table + $data['taxonomy_term_data']['table']['join'] = array( + 'taxonomy_vocabulary' => array( + 'field' => 'vid', + 'left_field' => 'vid', + ), + // This is provided for many_to_one argument + 'taxonomy_index' => array( + 'field' => 'tid', + 'left_field' => 'tid', + ), + ); + + // tid field + $data['taxonomy_term_data']['tid'] = array( + 'title' => t('Term ID'), + 'help' => t('The tid of a taxonomy term.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'taxonomy', + 'name field' => 'name', + 'zero is null' => TRUE, + ), + 'filter' => array( + 'title' => t('Term'), + 'help' => t('Taxonomy term chosen from autocomplete or select widget.'), + 'id' => 'taxonomy_index_tid', + 'hierarchy table' => 'taxonomy_term_hierarchy', + 'numeric' => TRUE, + ), + ); + + // raw tid field + $data['taxonomy_term_data']['tid_raw'] = array( + 'title' => t('Term ID'), + 'help' => t('The tid of a taxonomy term.'), + 'real field' => 'tid', + 'filter' => array( + 'id' => 'numeric', + 'allow empty' => TRUE, + ), + ); + + $data['taxonomy_term_data']['tid_representative'] = array( + 'relationship' => array( + 'title' => t('Representative node'), + 'label' => t('Representative node'), + 'help' => t('Obtains a single representative node for each term, according to a chosen sort criterion.'), + 'id' => 'groupwise_max', + 'relationship field' => 'tid', + 'outer field' => 'taxonomy_term_data.tid', + 'argument table' => 'taxonomy_term_data', + 'argument field' => 'tid', + 'base' => 'node', + 'field' => 'nid', + ), + ); + + // Term name field + $data['taxonomy_term_data']['name'] = array( + 'title' => t('Name'), + 'help' => t('The taxonomy term name.'), + 'field' => array( + 'id' => 'taxonomy', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + 'help' => t('Taxonomy term name.'), + ), + 'argument' => array( + 'id' => 'string', + 'help' => t('Taxonomy term name.'), + 'many to one' => TRUE, + 'empty field name' => t('Uncategorized'), + ), + ); + + // taxonomy weight + $data['taxonomy_term_data']['weight'] = array( + 'title' => t('Weight'), + 'help' => t('The term weight field'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'argument' => array( + 'id' => 'numeric', + ), + ); + + // Term description + $data['taxonomy_term_data']['description'] = array( + 'title' => t('Term description'), + 'help' => t('The description associated with a taxonomy term.'), + 'field' => array( + 'id' => 'markup', + 'format' => array('field' => 'format'), + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + // Term vocabulary + $data['taxonomy_term_data']['vid'] = array( + 'title' => t('Vocabulary'), + 'help' => t('Filter the results of "Taxonomy: Term" to a particular vocabulary.'), + 'filter' => array( + 'id' => 'vocabulary_vid', + ), + ); + + $data['taxonomy_term_data']['langcode'] = array( + 'title' => t('Language'), // The item it appears as on the UI, + 'help' => t('Language of the term'), + 'field' => array( + 'id' => 'taxonomy_term_language', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'language', + ), + 'argument' => array( + 'id' => 'language', + ), + ); + + // Link to edit the term + $data['taxonomy_term_data']['edit_term'] = array( + 'field' => array( + 'title' => t('Term edit link'), + 'help' => t('Provide a simple link to edit the term.'), + 'id' => 'term_link_edit', + ), + ); + + // ---------------------------------------------------------------------- + // taxonomy_index table + + $data['taxonomy_index']['table']['group'] = t('Taxonomy term'); + + $data['taxonomy_index']['table']['join'] = array( + 'taxonomy_term_data' => array( + // links directly to taxonomy_term_data via tid + 'left_field' => 'tid', + 'field' => 'tid', + ), + 'node' => array( + // links directly to node via nid + 'left_field' => 'nid', + 'field' => 'nid', + ), + 'taxonomy_term_hierarchy' => array( + 'left_field' => 'tid', + 'field' => 'tid', + ), + ); + + $data['taxonomy_index']['nid'] = array( + 'title' => t('Content with term'), + 'help' => t('Relate all content tagged with a term.'), + 'relationship' => array( + 'id' => 'standard', + 'base' => 'node', + 'base field' => 'nid', + 'label' => t('node'), + 'skip base' => 'node', + ), + ); + + // @todo This stuff needs to move to a node field since + // really it's all about nodes. + // tid field + $data['taxonomy_index']['tid'] = array( + 'group' => t('Content'), + 'title' => t('Has taxonomy term ID'), + 'help' => t('Display content if it has the selected taxonomy terms.'), + 'argument' => array( + 'id' => 'taxonomy_index_tid', + 'name table' => 'taxonomy_term_data', + 'name field' => 'name', + 'empty field name' => t('Uncategorized'), + 'numeric' => TRUE, + 'skip base' => 'taxonomy_term_data', + ), + 'filter' => array( + 'title' => t('Has taxonomy term'), + 'id' => 'taxonomy_index_tid', + 'hierarchy table' => 'taxonomy_term_hierarchy', + 'numeric' => TRUE, + 'skip base' => 'taxonomy_term_data', + 'allow empty' => TRUE, + ), + ); + + // ---------------------------------------------------------------------- + // term_hierarchy table + + $data['taxonomy_term_hierarchy']['table']['group'] = t('Taxonomy term'); + + $data['taxonomy_term_hierarchy']['table']['join'] = array( + 'taxonomy_term_hierarchy' => array( + // links to self through left.parent = right.tid (going down in depth) + 'left_field' => 'tid', + 'field' => 'parent', + ), + 'taxonomy_term_data' => array( + // links directly to taxonomy_term_data via tid + 'left_field' => 'tid', + 'field' => 'tid', + ), + ); + + $data['taxonomy_term_hierarchy']['parent'] = array( + 'title' => t('Parent term'), + 'help' => t('The parent term of the term. This can produce duplicate entries if you are using a vocabulary that allows multiple parents.'), + 'relationship' => array( + 'base' => 'taxonomy_term_data', + 'field' => 'parent', + 'label' => t('Parent'), + ), + 'filter' => array( + 'help' => t('Filter the results of "Taxonomy: Term" by the parent pid.'), + 'id' => 'numeric', + ), + 'argument' => array( + 'help' => t('The parent term of the term.'), + 'id' => 'taxonomy', + ), + ); + + return $data; +} + +/** + * Implements hook_views_data_alter(). + */ +function taxonomy_views_data_alter(&$data) { + $data['node']['term_node_tid'] = array( + 'title' => t('Taxonomy terms on node'), + 'help' => t('Relate nodes to taxonomy terms, specifiying which vocabulary or vocabularies to use. This relationship will cause duplicated records if there are multiple terms.'), + 'relationship' => array( + 'id' => 'node_term_data', + 'label' => t('term'), + 'base' => 'taxonomy_term_data', + ), + 'field' => array( + 'title' => t('All taxonomy terms'), + 'help' => t('Display all taxonomy terms associated with a node from specified vocabularies.'), + 'id' => 'taxonomy_index_tid', + 'no group by' => TRUE, + ), + ); + + $data['node']['term_node_tid_depth'] = array( + 'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth.'), + 'real field' => 'nid', + 'argument' => array( + 'title' => t('Has taxonomy term ID (with depth)'), + 'id' => 'taxonomy_index_tid_depth', + 'accept depth modifier' => TRUE, + ), + 'filter' => array( + 'title' => t('Has taxonomy terms (with depth)'), + 'id' => 'taxonomy_index_tid_depth', + ), + ); + + $data['node']['term_node_tid_depth_modifier'] = array( + 'title' => t('Has taxonomy term ID depth modifier'), + 'help' => t('Allows the "depth" for Taxonomy: Term ID (with depth) to be modified via an additional contextual filter value.'), + 'argument' => array( + 'id' => 'taxonomy_index_tid_depth_modifier', + ), + ); +} + +/** + * Implements hook_field_views_data(). + * + * Views integration for taxonomy_term_reference fields. Adds a term relationship to the default + * field data. + * + * @see field_views_field_default_views_data() + */ +function taxonomy_field_views_data($field) { + $data = field_views_field_default_views_data($field); + foreach ($data as $table_name => $table_data) { + foreach ($table_data as $field_name => $field_data) { + if (isset($field_data['filter']) && $field_name != 'delta') { + $data[$table_name][$field_name]['filter']['id'] = 'taxonomy_index_tid'; + $data[$table_name][$field_name]['filter']['vocabulary'] = $field['settings']['allowed_values'][0]['vocabulary']; + } + } + + // Add the relationship only on the tid field. + $data[$table_name][$field['field_name'] . '_tid']['relationship'] = array( + 'id' => 'standard', + 'base' => 'taxonomy_term_data', + 'base field' => 'tid', + 'label' => t('term from !field_name', array('!field_name' => $field['field_name'])), + ); + + } + + return $data; +} + +/** + * Implements hook_field_views_data_views_data_alter(). + * + * Views integration to provide reverse relationships on term references. + */ +function taxonomy_field_views_data_views_data_alter(&$data, $field) { + foreach ($field['bundles'] as $entity_type => $bundles) { + $entity_info = entity_get_info($entity_type); + $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; + + list($label, $all_labels) = field_views_field_label($field['field_name']); + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + + $data['taxonomy_term_data'][$pseudo_field_name]['relationship'] = array( + 'title' => t('@entity using @field', array('@entity' => $entity, '@field' => $label)), + 'help' => t('Relate each @entity with a @field set to the term.', array('@entity' => $entity, '@field' => $label)), + 'id' => 'entity_reverse', + 'field_name' => $field['field_name'], + 'field table' => _field_sql_storage_tablename($field), + 'field field' => $field['field_name'] . '_tid', + 'base' => $entity_info['base table'], + 'base field' => $entity_info['entity keys']['id'], + 'label' => t('!field_name', array('!field_name' => $field['field_name'])), + 'join_extra' => array( + 0 => array( + 'field' => 'entity_type', + 'value' => $entity_type, + ), + 1 => array( + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ), + ), + ); + } +} + +/** + * Helper function to set a breadcrumb for taxonomy. + */ +function views_taxonomy_set_breadcrumb(&$breadcrumb, &$argument) { + if (empty($argument->options['set_breadcrumb'])) { + return; + } + + $args = $argument->view->args; + $parents = taxonomy_get_parents_all($argument->argument); + foreach (array_reverse($parents) as $parent) { + // Unfortunately parents includes the current argument. Skip. + if ($parent->tid == $argument->argument) { + continue; + } + if (!empty($argument->options['use_taxonomy_term_path'])) { + $path = taxonomy_term_uri($parent); + $path = $path['path']; + } + else { + $args[$argument->position] = $parent->tid; + $path = $argument->view->getUrl($args); + } + $breadcrumb[$path] = check_plain($parent->name); + } +} diff --git a/core/modules/translation/lib/Drupal/translation/Plugin/views/argument/NodeTnid.php b/core/modules/translation/lib/Drupal/translation/Plugin/views/argument/NodeTnid.php new file mode 100644 index 0000000..8f2ca4c --- /dev/null +++ b/core/modules/translation/lib/Drupal/translation/Plugin/views/argument/NodeTnid.php @@ -0,0 +1,41 @@ +addField('n', 'title'); + $query->condition('n.tnid', $this->value); + $result = $query->execute(); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } + +} diff --git a/core/modules/translation/lib/Drupal/translation/Plugin/views/field/NodeLinkTranslate.php b/core/modules/translation/lib/Drupal/translation/Plugin/views/field/NodeLinkTranslate.php new file mode 100644 index 0000000..b77ec82 --- /dev/null +++ b/core/modules/translation/lib/Drupal/translation/Plugin/views/field/NodeLinkTranslate.php @@ -0,0 +1,41 @@ +get_value($values); + $node->status = 1; // unpublished nodes ignore access control + if (empty($node->langcode) || !translation_supported_type($node->type) || !node_access('view', $node) || !user_access('translate content')) { + return; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/$node->nid/translate"; + $this->options['alter']['query'] = drupal_get_destination(); + + $text = !empty($this->options['text']) ? $this->options['text'] : t('translate'); + return $text; + } + +} diff --git a/core/modules/translation/lib/Drupal/translation/Plugin/views/field/NodeTranslationLink.php b/core/modules/translation/lib/Drupal/translation/Plugin/views/field/NodeTranslationLink.php new file mode 100644 index 0000000..8a1cf2e --- /dev/null +++ b/core/modules/translation/lib/Drupal/translation/Plugin/views/field/NodeTranslationLink.php @@ -0,0 +1,66 @@ +additional_fields['nid'] = 'nid'; + $this->additional_fields['tnid'] = 'tnid'; + $this->additional_fields['title'] = 'title'; + $this->additional_fields['langcode'] = 'langcode'; + } + + public function query() { + $this->ensureMyTable(); + $this->add_additional_fields(); + } + + function render($values) { + $value = $this->get_value($values, 'tnid'); + return $this->render_link($this->sanitizeValue($value), $values); + } + + function render_link($data, $values) { + $language_interface = language(LANGUAGE_TYPE_INTERFACE); + + $tnid = $this->get_value($values, 'tnid'); + // Only load translations if the node isn't in the current language. + if ($this->get_value($values, 'langcode') != $language_interface->langcode) { + $translations = translation_node_get_translations($tnid); + if (isset($translations[$language_interface->langcode])) { + $values->{$this->aliases['nid']} = $translations[$language_interface->langcode]->nid; + $values->{$this->aliases['title']} = $translations[$language_interface->langcode]->title; + } + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/" . $this->get_value($values, 'nid'); + return $this->get_value($values, 'title'); + } + +} diff --git a/core/modules/translation/lib/Drupal/translation/Plugin/views/filter/NodeTnid.php b/core/modules/translation/lib/Drupal/translation/Plugin/views/filter/NodeTnid.php new file mode 100644 index 0000000..1e446d1 --- /dev/null +++ b/core/modules/translation/lib/Drupal/translation/Plugin/views/filter/NodeTnid.php @@ -0,0 +1,58 @@ + 'radios', + '#title' => t('Include untranslated content'), + '#default_value' => $this->operator, + '#options' => array( + 1 => t('Yes'), + 0 => t('No'), + ), + ); + } + + public function canExpose() { return FALSE; } + + public function query() { + $table = $this->ensureMyTable(); + // Select for source translations (tnid = nid). Conditionally, also accept either untranslated nodes (tnid = 0). + $this->query->add_where_expression($this->options['group'], "$table.tnid = $table.nid" . ($this->operator ? " OR $table.tnid = 0" : '')); + } + +} diff --git a/core/modules/translation/lib/Drupal/translation/Plugin/views/filter/NodeTnidChild.php b/core/modules/translation/lib/Drupal/translation/Plugin/views/filter/NodeTnidChild.php new file mode 100644 index 0000000..3881e77 --- /dev/null +++ b/core/modules/translation/lib/Drupal/translation/Plugin/views/filter/NodeTnidChild.php @@ -0,0 +1,36 @@ +ensureMyTable(); + $this->query->add_where_expression($this->options['group'], "$table.tnid <> $table.nid AND $table.tnid > 0"); + } + +} diff --git a/core/modules/translation/lib/Drupal/translation/Plugin/views/relationship/Translation.php b/core/modules/translation/lib/Drupal/translation/Plugin/views/relationship/Translation.php new file mode 100644 index 0000000..1324537 --- /dev/null +++ b/core/modules/translation/lib/Drupal/translation/Plugin/views/relationship/Translation.php @@ -0,0 +1,113 @@ + 'current'); + + return $options; + } + + /** + * Add a translation selector. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $options = array( + 'all' => t('All'), + 'current' => t('Current language'), + 'default' => t('Default language'), + ); + $options = array_merge($options, views_language_list()); + $form['language'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => $this->options['language'], + '#title' => t('Translation option'), + '#description' => t('The translation options allows you to select which translation or translations in a translation set join on. Select "Current language" or "Default language" to join on the translation in the current or default language respectively. Select a specific language to join on a translation in that language. If you select "All", each translation will create a new row, which may appear to cause duplicates.'), + ); + } + + /** + * Called to implement a relationship in a query. + */ + public function query() { + // Figure out what base table this relationship brings to the party. + $table_data = views_fetch_data($this->definition['base']); + $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field']; + + $this->ensureMyTable(); + + $def = $this->definition; + $def['table'] = $this->definition['base']; + $def['field'] = $base_field; + $def['left_table'] = $this->tableAlias; + $def['left_field'] = $this->field; + $def['adjusted'] = TRUE; + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + $def['extra'] = array(); + if ($this->options['language'] != 'all') { + switch ($this->options['language']) { + case 'current': + $def['extra'][] = array( + 'field' => 'langcode', + 'value' => '***CURRENT_LANGUAGE***', + ); + break; + case 'default': + $def['extra'][] = array( + 'field' => 'langcode', + 'value' => '***DEFAULT_LANGUAGE***', + ); + break; + // Other values will be the language codes. + default: + $def['extra'][] = array( + 'field' => 'langcode', + 'value' => $this->options['language'], + ); + break; + } + } + + if (!empty($def['join_id'])) { + $id = $def['join_id']; + } + else { + $id = 'standard'; + } + $join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $def); + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, $this->definition['base'], $this->relationship); + } + +} diff --git a/core/modules/translation/translation.views.inc b/core/modules/translation/translation.views.inc new file mode 100644 index 0000000..6564b31 --- /dev/null +++ b/core/modules/translation/translation.views.inc @@ -0,0 +1,120 @@ + 'tnid', + 'field' => 'tnid', + ); + + // The translation ID (nid of the "source" translation) + $data['node']['tnid'] = array( + 'group' => t('Content translation'), + 'title' => t('Translation set node ID'), + 'help' => t('The ID of the translation set the content belongs to.'), + 'field' => array( + 'id' => 'node', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'argument' => array( + 'id' => 'node_tnid', + 'name field' => 'title', // the field to display in the summary. + 'numeric' => TRUE, + 'validate type' => 'tnid', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'relationship' => array( + 'title' => t('Source translation'), + 'help' => t('The source that this content was translated from.'), + 'base' => 'node', + 'base field' => 'nid', + 'id' => 'standard', + 'label' => t('Source translation'), + ), + ); + + // The source translation. + $data['node']['translation'] = array( + 'group' => t('Content translation'), + 'title' => t('Translations'), + 'help' => t('Versions of content in different languages.'), + 'relationship' => array( + 'title' => t('Translations'), + 'help' => t('Versions of content in different languages.'), + 'base' => 'node', + 'base field' => 'tnid', + 'relationship table' => 'node', + 'relationship field' => 'nid', + 'id' => 'translation', + 'label' => t('Translations'), + ), + ); + + // The source translation. + $data['node']['source_translation'] = array( + 'group' => t('Content translation'), + 'title' => t('Source translation'), + 'help' => t('Content that is either untranslated or is the original version of a translation set.'), + 'filter' => array( + 'id' => 'node_tnid', + ), + ); + + // The source translation. + $data['node']['child_translation'] = array( + 'group' => t('Node translation'), + 'title' => t('Child translation'), + 'help' => t('Content that is a translation of a source translation.'), + 'filter' => array( + 'id' => 'node_tnid_child', + ), + ); + + // Translation status + $data['node']['translate'] = array( + 'group' => t('Content translation'), + 'title' => t('Translation status'), + 'help' => t('The translation status of the content - whether or not the translation needs to be updated.'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Outdated'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // Translate node link. + $data['node']['translate_node'] = array( + 'group' => t('Content translation'), + 'title' => t('Translate link'), + 'help' => t('Provide a simple link to translate the node.'), + 'field' => array( + 'id' => 'node_link_translate', + ), + ); + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/argument/RolesRid.php b/core/modules/user/lib/Drupal/user/Plugin/views/argument/RolesRid.php new file mode 100644 index 0000000..634307e --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/argument/RolesRid.php @@ -0,0 +1,38 @@ +addField('r', 'name'); + $query->condition('r.rid', $this->value); + $result = $query->execute(); + foreach ($result as $term) { + $titles[] = check_plain($term->name); + } + return $titles; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/argument/Uid.php b/core/modules/user/lib/Drupal/user/Plugin/views/argument/Uid.php new file mode 100644 index 0000000..02341c1 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/argument/Uid.php @@ -0,0 +1,45 @@ +argument) { + return array(config('user.settings')->get('anonymous')); + } + + $titles = array(); + + $users = user_load_multiple($this->value); + foreach ($users as $account) { + $titles[] = check_plain($account->label()); + } + return $titles; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/argument_default/CurrentUser.php b/core/modules/user/lib/Drupal/user/Plugin/views/argument_default/CurrentUser.php new file mode 100644 index 0000000..8ba91b5 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/argument_default/CurrentUser.php @@ -0,0 +1,32 @@ +uid; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/argument_default/User.php b/core/modules/user/lib/Drupal/user/Plugin/views/argument_default/User.php new file mode 100644 index 0000000..70daaa7 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/argument_default/User.php @@ -0,0 +1,85 @@ + '', 'bool' => TRUE, 'translatable' => FALSE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['user'] = array( + '#type' => 'checkbox', + '#title' => t('Also look for a node and use the node author'), + '#default_value' => $this->options['user'], + ); + } + + function get_argument() { + foreach (range(1, 3) as $i) { + $user = menu_get_object('user', $i); + if (!empty($user)) { + return $user->uid; + } + } + + foreach (range(1, 3) as $i) { + $user = menu_get_object('user_uid_optional', $i); + if (!empty($user)) { + return $user->uid; + } + } + + if (!empty($this->options['user'])) { + foreach (range(1, 3) as $i) { + $node = menu_get_object('node', $i); + if (!empty($node)) { + return $node->uid; + } + } + } + + if (arg(0) == 'user' && is_numeric(arg(1))) { + return arg(1); + } + + if (!empty($this->options['user'])) { + if (arg(0) == 'node' && is_numeric(arg(1))) { + $node = node_load(arg(1)); + if ($node) { + return $node->uid; + } + } + } + + // If the current page is a view that takes uid as an argument, return the uid. + $view = views_get_page_view(); + + if ($view && isset($view->argument['uid'])) { + return $view->argument['uid']->argument; + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/argument_validator/User.php b/core/modules/user/lib/Drupal/user/Plugin/views/argument_validator/User.php new file mode 100644 index 0000000..a31b0af --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/argument_validator/User.php @@ -0,0 +1,152 @@ + 'uid'); + $options['restrict_roles'] = array('default' => FALSE, 'bool' => TRUE); + $options['roles'] = array('default' => array()); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['type'] = array( + '#type' => 'radios', + '#title' => t('Type of user filter value to allow'), + '#options' => array( + 'uid' => t('Only allow numeric UIDs'), + 'name' => t('Only allow string usernames'), + 'either' => t('Allow both numeric UIDs and string usernames'), + ), + '#default_value' => $this->options['type'], + ); + + $form['restrict_roles'] = array( + '#type' => 'checkbox', + '#title' => t('Restrict user based on role'), + '#default_value' => $this->options['restrict_roles'], + ); + + $form['roles'] = array( + '#type' => 'checkboxes', + '#title' => t('Restrict to the selected roles'), + '#options' => array_map('check_plain', user_roles(TRUE)), + '#default_value' => $this->options['roles'], + '#description' => t('If no roles are selected, users from any role will be allowed.'), + '#states' => array( + 'visible' => array( + ':input[name="options[validate][options][user][restrict_roles]"]' => array('checked' => TRUE), + ), + ), + ); + } + + public function submitOptionsForm(&$form, &$form_state, &$options = array()) { + // filter trash out of the options so we don't store giant unnecessary arrays + $options['roles'] = array_filter($options['roles']); + } + + function validate_argument($argument) { + $type = $this->options['type']; + // is_numeric() can return false positives, so we ensure it's an integer. + // However, is_integer() will always fail, since $argument is a string. + if (is_numeric($argument) && $argument == (int)$argument) { + if ($type == 'uid' || $type == 'either') { + if ($argument == $GLOBALS['user']->uid) { + // If you assign an object to a variable in PHP, the variable + // automatically acts as a reference, not a copy, so we use + // clone to ensure that we don't actually mess with the + // real global $user object. + $account = clone $GLOBALS['user']; + } + $condition = 'uid'; + } + } + else { + if ($type == 'name' || $type == 'either') { + $name = !empty($GLOBALS['user']->name) ? $GLOBALS['user']->name : config('user.settings')->get('anonymous'); + if ($argument == $name) { + $account = clone $GLOBALS['user']; + } + $condition = 'name'; + } + } + + // If we don't have a WHERE clause, the argument is invalid. + if (empty($condition)) { + return FALSE; + } + + if (!isset($account)) { + $account = db_select('users', 'u') + ->fields('u', array('uid', 'name')) + ->condition($condition, $argument) + ->execute() + ->fetchObject(); + } + if (empty($account)) { + // User not found. + return FALSE; + } + + // See if we're filtering users based on roles. + if (!empty($this->options['restrict_roles']) && !empty($this->options['roles'])) { + $roles = $this->options['roles']; + $account->roles = array(); + $account->roles[] = $account->uid ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID; + $query = db_select('users_roles', 'u'); + $query->addField('u', 'rid'); + $query->condition('u.uid', $account->uid); + $result = $query->execute(); + foreach ($result as $role) { + $account->roles[] = $role->rid; + } + if (!(bool) array_intersect($account->roles, $roles)) { + return FALSE; + } + } + + $this->argument->argument = $account->uid; + $this->argument->validated_title = check_plain(user_format_name($account)); + return TRUE; + } + + function process_summary_arguments(&$args) { + // If the validation says the input is an username, we should reverse the + // argument so it works for example for generation summary urls. + $uids_arg_keys = array_flip($args); + if ($this->options['type'] == 'name') { + $users = user_load_multiple($args); + foreach ($users as $uid => $account) { + $args[$uids_arg_keys[$uid]] = $account->name; + } + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/Language.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/Language.php new file mode 100644 index 0000000..3fb9b23 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/Language.php @@ -0,0 +1,49 @@ +get_value($values, 'uid'); + if (!empty($this->options['link_to_user'])) { + $uid = $this->get_value($values, 'uid'); + if (user_access('access user profiles') && $uid) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'user/' . $uid; + } + } + if (empty($data)) { + $lang = language_default(); + } + else { + $lang = language_list(); + $lang = $lang[$data]; + } + + return $this->sanitizeValue($lang->name); + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/Link.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/Link.php new file mode 100644 index 0000000..c7f0561 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/Link.php @@ -0,0 +1,74 @@ +additional_fields['uid'] = 'uid'; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['text'] = array('default' => '', 'translatable' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + parent::buildOptionsForm($form, $form_state); + } + + // An example of field level access control. + public function access() { + return user_access('access user profiles'); + } + + public function query() { + $this->ensureMyTable(); + $this->add_additional_fields(); + } + + function render($values) { + $value = $this->get_value($values, 'uid'); + return $this->render_link($this->sanitizeValue($value), $values); + } + + function render_link($data, $values) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('view'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "user/" . $data; + + return $text; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkCancel.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkCancel.php new file mode 100644 index 0000000..d1ce197 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkCancel.php @@ -0,0 +1,43 @@ +{$this->aliases['uid']}; + + // Build a pseudo account object to be able to check the access. + $account = entity_create('user', array()); + $account->uid = $uid; + + if ($uid && user_cancel_access($account)) { + $this->options['alter']['make_link'] = TRUE; + + $text = !empty($this->options['text']) ? $this->options['text'] : t('cancel'); + + $this->options['alter']['path'] = "user/$uid/cancel"; + $this->options['alter']['query'] = drupal_get_destination(); + + return $text; + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkEdit.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkEdit.php new file mode 100644 index 0000000..4d85c7f --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/LinkEdit.php @@ -0,0 +1,41 @@ +uid = $data; + + if ($data && user_edit_access($account)) { + $this->options['alter']['make_link'] = TRUE; + + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + + $this->options['alter']['path'] = "user/$data/edit"; + $this->options['alter']['query'] = drupal_get_destination(); + + return $text; + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/Mail.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/Mail.php new file mode 100644 index 0000000..713d42a --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/Mail.php @@ -0,0 +1,56 @@ + 'mailto'); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['link_to_user'] = array( + '#title' => t('Link this field'), + '#type' => 'radios', + '#options' => array( + 0 => t('No link'), + 'user' => t('To the user'), + 'mailto' => t("With a mailto:"), + ), + '#default_value' => $this->options['link_to_user'], + ); + } + + function render_link($data, $values) { + parent::render_link($data, $values); + + if ($this->options['link_to_user'] == 'mailto') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "mailto:" . $data; + } + + return $data; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/Name.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/Name.php new file mode 100644 index 0000000..5b7a101 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/Name.php @@ -0,0 +1,98 @@ +options['overwrite_anonymous']) || !empty($this->options['format_username'])) { + $this->additional_fields['uid'] = 'uid'; + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['overwrite_anonymous'] = array('default' => FALSE, 'bool' => TRUE); + $options['anonymous_text'] = array('default' => '', 'translatable' => TRUE); + $options['format_username'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['format_username'] = array( + '#title' => t('Use formatted username'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['format_username']), + '#description' => t('If checked, the username will be formatted by the system. If unchecked, it will be displayed raw.'), + '#fieldset' => 'more', + ); + $form['overwrite_anonymous'] = array( + '#title' => t('Overwrite the value to display for anonymous users'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['overwrite_anonymous']), + '#description' => t('Enable to display different text for anonymous users.'), + '#fieldset' => 'more', + ); + $form['anonymous_text'] = array( + '#title' => t('Text to display for anonymous users'), + '#type' => 'textfield', + '#default_value' => $this->options['anonymous_text'], + '#states' => array( + 'visible' => array( + ':input[name="options[overwrite_anonymous]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'more', + ); + + parent::buildOptionsForm($form, $form_state); + } + + function render_link($data, $values) { + $account = entity_create('user', array()); + $account->uid = $this->get_value($values, 'uid'); + $account->name = $this->get_value($values); + if (!empty($this->options['link_to_user']) || !empty($this->options['overwrite_anonymous'])) { + if (!empty($this->options['overwrite_anonymous']) && !$account->uid) { + // This is an anonymous user, and we're overriting the text. + return check_plain($this->options['anonymous_text']); + } + elseif (!empty($this->options['link_to_user'])) { + $account->name = $this->get_value($values); + return theme('username', array('account' => $account)); + } + } + // If we want a formatted username, do that. + if (!empty($this->options['format_username'])) { + return user_format_name($account); + } + // Otherwise, there's no special handling, so return the data directly. + return $data; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/Permissions.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/Permissions.php new file mode 100644 index 0000000..98cb021 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/Permissions.php @@ -0,0 +1,91 @@ +additional_fields['uid'] = array('table' => 'users', 'field' => 'uid'); + } + + public function query() { + $this->add_additional_fields(); + $this->field_alias = $this->aliases['uid']; + } + + function pre_render(&$values) { + $uids = array(); + $this->items = array(); + + foreach ($values as $result) { + $uids[] = $this->get_value($result); + } + + if ($uids) { + // Get a list of all the modules implementing a hook_permission() and sort by + // display name. + $module_info = system_get_info('module'); + $modules = array(); + foreach (module_implements('permission') as $module) { + $modules[$module] = $module_info[$module]['name']; + } + asort($modules); + + $permissions = module_invoke_all('permission'); + + $query = db_select('role_permission', 'rp'); + $query->join('users_roles', 'u', 'u.rid = rp.rid'); + $query->fields('u', array('uid', 'rid')); + $query->addField('rp', 'permission'); + $query->condition('u.uid', $uids); + $query->condition('rp.module', array_keys($modules)); + $query->orderBy('rp.permission'); + $result = $query->execute(); + + foreach ($result as $perm) { + $this->items[$perm->uid][$perm->permission]['permission'] = $permissions[$perm->permission]['title']; + } + } + } + + function render_item($count, $item) { + return $item['permission']; + } + + /* + function document_self_tokens(&$tokens) { + $tokens['[' . $this->options['id'] . '-role' . ']'] = t('The name of the role.'); + $tokens['[' . $this->options['id'] . '-rid' . ']'] = t('The role ID of the role.'); + } + + function add_self_tokens(&$tokens, $item) { + $tokens['[' . $this->options['id'] . '-role' . ']'] = $item['role']; + $tokens['[' . $this->options['id'] . '-rid' . ']'] = $item['rid']; + } + */ + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/Picture.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/Picture.php new file mode 100644 index 0000000..8446fac --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/Picture.php @@ -0,0 +1,131 @@ +additional_fields['uid'] = 'uid'; + $this->additional_fields['name'] = 'name'; + $this->additional_fields['mail'] = 'mail'; + } + + function element_type($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) { + if ($inline) { + return 'span'; + } + if ($none_supported) { + if ($this->options['element_type'] === '0') { + return ''; + } + } + if ($this->options['element_type']) { + return check_plain($this->options['element_type']); + } + if ($default_empty) { + return ''; + } + if (isset($this->definition['element type'])) { + return $this->definition['element type']; + } + + return 'div'; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_photo_to_profile'] = array('default' => TRUE, 'bool' => TRUE); + $options['image_style'] = array('default' => ''); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['link_photo_to_profile'] = array( + '#title' => t("Link to user's profile"), + '#description' => t("Link the user picture to the user's profile"), + '#type' => 'checkbox', + '#default_value' => $this->options['link_photo_to_profile'], + ); + + if (module_exists('image')) { + $styles = image_styles(); + $style_options = array('' => t('Default')); + foreach ($styles as $style) { + $style_options[$style['name']] = $style['name']; + } + + $form['image_style'] = array( + '#title' => t('Image style'), + '#description' => t('Using Default will use the site-wide image style for user pictures set in the Account settings.', array('!account-settings' => url('admin/config/people/accounts', array('fragment' => 'edit-personalization')))), + '#type' => 'select', + '#options' => $style_options, + '#default_value' => $this->options['image_style'], + ); + } + } + + function render($values) { + if ($this->options['image_style'] && module_exists('image')) { + // @todo: Switch to always using theme('user_picture') when it starts + // supporting image styles. See http://drupal.org/node/1021564 + if ($picture_fid = $this->get_value($values)) { + $picture = file_load($picture_fid); + $picture_filepath = $picture->uri; + } + else { + $picture_filepath = variable_get('user_picture_default', ''); + } + if (file_valid_uri($picture_filepath)) { + $output = theme('image_style', array('style_name' => $this->options['image_style'], 'path' => $picture_filepath)); + if ($this->options['link_photo_to_profile'] && user_access('access user profiles')) { + $uid = $this->get_value($values, 'uid'); + $output = l($output, "user/$uid", array('html' => TRUE)); + } + } + else { + $output = ''; + } + } + else { + // Fake an account object. + $account = entity_create('user', array()); + if ($this->options['link_photo_to_profile']) { + // Prevent template_preprocess_user_picture from adding a link + // by not setting the uid. + $account->uid = $this->get_value($values, 'uid'); + } + $account->name = $this->get_value($values, 'name'); + $account->mail = $this->get_value($values, 'mail'); + $account->picture = $this->get_value($values); + $output = theme('user_picture', array('account' => $account)); + } + + return $output; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/Roles.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/Roles.php new file mode 100644 index 0000000..947175a --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/Roles.php @@ -0,0 +1,79 @@ +additional_fields['uid'] = array('table' => 'users', 'field' => 'uid'); + } + + public function query() { + $this->add_additional_fields(); + $this->field_alias = $this->aliases['uid']; + } + + function pre_render(&$values) { + $uids = array(); + $this->items = array(); + + foreach ($values as $result) { + $uids[] = $this->get_value($result); + } + + if ($uids) { + $query = db_select('role', 'r'); + $query->join('users_roles', 'u', 'u.rid = r.rid'); + $query->addField('r', 'name'); + $query->fields('u', array('uid', 'rid')); + $query->condition('u.uid', $uids); + $query->orderBy('r.name'); + $result = $query->execute(); + foreach ($result as $role) { + $this->items[$role->uid][$role->rid]['role'] = check_plain($role->name); + $this->items[$role->uid][$role->rid]['rid'] = $role->rid; + } + } + } + + function render_item($count, $item) { + return $item['role']; + } + + function document_self_tokens(&$tokens) { + $tokens['[' . $this->options['id'] . '-role' . ']'] = t('The name of the role.'); + $tokens['[' . $this->options['id'] . '-rid' . ']'] = t('The role machine-name of the role.'); + } + + function add_self_tokens(&$tokens, $item) { + if (!empty($item['role'])) { + $tokens['[' . $this->options['id'] . '-role' . ']'] = $item['role']; + $tokens['[' . $this->options['id'] . '-rid' . ']'] = $item['rid']; + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/field/User.php b/core/modules/user/lib/Drupal/user/Plugin/views/field/User.php new file mode 100644 index 0000000..b958d3e --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/field/User.php @@ -0,0 +1,68 @@ +options['link_to_user'])) { + $this->additional_fields['uid'] = 'uid'; + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['link_to_user'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to node option + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['link_to_user'] = array( + '#title' => t('Link this field to its user'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_user'], + ); + parent::buildOptionsForm($form, $form_state); + } + + function render_link($data, $values) { + if (!empty($this->options['link_to_user']) && user_access('access user profiles') && ($uid = $this->get_value($values, 'uid')) && $data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "user/" . $uid; + } + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitizeValue($value), $values); + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/filter/Current.php b/core/modules/user/lib/Drupal/user/Plugin/views/filter/Current.php new file mode 100644 index 0000000..b24e658 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/filter/Current.php @@ -0,0 +1,53 @@ +value_value = t('Is the logged in user'); + } + + public function query() { + $this->ensureMyTable(); + + $field = $this->tableAlias . '.' . $this->realField . ' '; + $or = db_or(); + + if (empty($this->value)) { + $or->condition($field, '***CURRENT_USER***', '<>'); + if ($this->accept_null) { + $or->isNull($field); + } + } + else { + $or->condition($field, '***CURRENT_USER***', '='); + } + $this->query->add_where($this->options['group'], $or); + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/filter/Name.php b/core/modules/user/lib/Drupal/user/Plugin/views/filter/Name.php new file mode 100644 index 0000000..b12eb07 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/filter/Name.php @@ -0,0 +1,173 @@ +value) { + $result = entity_load_multiple_by_properties('user', array('uid' => $this->value)); + foreach ($result as $account) { + if ($account->uid) { + $values[] = $account->name; + } + else { + $values[] = 'Anonymous'; // Intentionally NOT translated. + } + } + } + + sort($values); + $default_value = implode(', ', $values); + $form['value'] = array( + '#type' => 'textfield', + '#title' => t('Usernames'), + '#description' => t('Enter a comma separated list of user names.'), + '#default_value' => $default_value, + '#autocomplete_path' => 'admin/views/ajax/autocomplete/user', + ); + + if (!empty($form_state['exposed']) && !isset($form_state['input'][$this->options['expose']['identifier']])) { + $form_state['input'][$this->options['expose']['identifier']] = $default_value; + } + } + + function value_validate($form, &$form_state) { + $values = drupal_explode_tags($form_state['values']['options']['value']); + $uids = $this->validate_user_strings($form['value'], $values); + + if ($uids) { + $form_state['values']['options']['value'] = $uids; + } + } + + public function acceptExposedInput($input) { + $rc = parent::acceptExposedInput($input); + + if ($rc) { + // If we have previously validated input, override. + if (isset($this->validated_exposed_input)) { + $this->value = $this->validated_exposed_input; + } + } + + return $rc; + } + + public function validateExposed(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + if (empty($this->options['expose']['identifier'])) { + return; + } + + $identifier = $this->options['expose']['identifier']; + $input = $form_state['values'][$identifier]; + + if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) { + $this->operator = $this->options['group_info']['group_items'][$input]['operator']; + $input = $this->options['group_info']['group_items'][$input]['value']; + } + + $values = drupal_explode_tags($input); + + if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) { + $uids = $this->validate_user_strings($form[$identifier], $values); + } + else { + $uids = FALSE; + } + + if ($uids) { + $this->validated_exposed_input = $uids; + } + } + + /** + * Validate the user string. Since this can come from either the form + * or the exposed filter, this is abstracted out a bit so it can + * handle the multiple input sources. + */ + function validate_user_strings(&$form, $values) { + $uids = array(); + $placeholders = array(); + $args = array(); + $results = array(); + foreach ($values as $value) { + if (strtolower($value) == 'anonymous') { + $uids[] = 0; + } + else { + $missing[strtolower($value)] = TRUE; + $args[] = $value; + $placeholders[] = "'%s'"; + } + } + + if (!$args) { + return $uids; + } + + $result = entity_load_multiple_by_properties('user', array('name' => $args)); + foreach ($result as $account) { + unset($missing[strtolower($account->name)]); + $uids[] = $account->uid; + } + + if ($missing) { + form_error($form, format_plural(count($missing), 'Unable to find user: @users', 'Unable to find users: @users', array('@users' => implode(', ', array_keys($missing))))); + } + + return $uids; + } + + function value_submit($form, &$form_state) { + // prevent array filter from removing our anonymous user. + } + + // Override to do nothing. + function get_value_options() { } + + public function adminSummary() { + // set up $this->value_options for the parent summary + $this->value_options = array(); + + if ($this->value) { + $result = entity_load_multiple_by_properties('user', array('uid' => $this->value)); + foreach ($result as $account) { + if ($account->uid) { + $this->value_options[$account->uid] = $account->name; + } + else { + $this->value_options[$account->uid] = 'Anonymous'; // Intentionally NOT translated. + } + } + } + + return parent::adminSummary(); + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/filter/Permissions.php b/core/modules/user/lib/Drupal/user/Plugin/views/filter/Permissions.php new file mode 100644 index 0000000..7f26db9 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/filter/Permissions.php @@ -0,0 +1,47 @@ +value_options = array(); + foreach ($modules as $module => $display_name) { + if ($permissions = module_invoke($module, 'permission')) { + foreach ($permissions as $perm => $perm_item) { + // @todo: group by module but views_handler_filter_many_to_one does not support this. + $this->value_options[$perm] = check_plain(strip_tags($perm_item['title'])); + } + } + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/filter/Roles.php b/core/modules/user/lib/Drupal/user/Plugin/views/filter/Roles.php new file mode 100644 index 0000000..8ee3569 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/filter/Roles.php @@ -0,0 +1,40 @@ +value_options = user_roles(TRUE); + unset($this->value_options[DRUPAL_AUTHENTICATED_RID]); + } + + /** + * Override empty and not empty operator labels to be clearer for user roles. + */ + function operators() { + $operators = parent::operators(); + $operators['empty']['title'] = t("Only has the 'authenticated user' role"); + $operators['not empty']['title'] = t("Has roles in addition to 'authenticated user'"); + return $operators; + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/row/View.php b/core/modules/user/lib/Drupal/user/Plugin/views/row/View.php new file mode 100644 index 0000000..d769602 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/row/View.php @@ -0,0 +1,97 @@ + 'full'); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $options = $this->buildOptionsForm_summary_options(); + $form['view_mode'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t('View mode'), + '#default_value' => $this->options['view_mode'], + ); + $form['help']['#markup'] = t("Display the user with standard user view. It might be necessary to add a user-profile.tpl.php in your themes template folder, because the default user-profilee template don't show the username per default.", array('@user-profile-api-link' => url('http://api.drupal.org/api/drupal/modules--user--user-profile.tpl.php/7'))); + } + + /** + * Return the main options, which are shown in the summary title. + */ + public function buildOptionsForm_summary_options() { + $entity_info = entity_get_info('user'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + if (empty($options)) { + $options = array( + 'full' => t('User account') + ); + } + + return $options; + } + + public function summaryTitle() { + $options = $this->buildOptionsForm_summary_options(); + return check_plain($options[$this->options['view_mode']]); + } + + function pre_render($values) { + $uids = array(); + foreach ($values as $row) { + $uids[] = $row->{$this->field_alias}; + } + $this->users = user_load_multiple($uids); + } + + function render($row) { + $account = $this->users[$row->{$this->field_alias}]; + $account->view = $this->view; + $build = user_view($account, $this->options['view_mode']); + + return drupal_render($build); + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/views/wizard/Users.php b/core/modules/user/lib/Drupal/user/Plugin/views/wizard/Users.php new file mode 100644 index 0000000..9a334c1 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/views/wizard/Users.php @@ -0,0 +1,95 @@ + 'uid', + 'table' => 'users', + 'field' => 'uid', + 'exclude' => TRUE, + 'link_to_user' => FALSE, + 'alter' => array( + 'alter_text' => TRUE, + 'text' => 'user/[uid]' + ) + ); + + /** + * Set default values for the filters. + */ + protected $filters = array( + 'status' => array( + 'value' => TRUE, + 'table' => 'users', + 'field' => 'status' + ) + ); + + /** + * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::default_display_options(). + */ + protected function default_display_options() { + $display_options = parent::default_display_options(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + $display_options['access']['perm'] = 'access user profiles'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + /* Field: User: Name */ + $display_options['fields']['name']['id'] = 'name'; + $display_options['fields']['name']['table'] = 'users'; + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['label'] = ''; + $display_options['fields']['name']['alter']['alter_text'] = 0; + $display_options['fields']['name']['alter']['make_link'] = 0; + $display_options['fields']['name']['alter']['absolute'] = 0; + $display_options['fields']['name']['alter']['trim'] = 0; + $display_options['fields']['name']['alter']['word_boundary'] = 0; + $display_options['fields']['name']['alter']['ellipsis'] = 0; + $display_options['fields']['name']['alter']['strip_tags'] = 0; + $display_options['fields']['name']['alter']['html'] = 0; + $display_options['fields']['name']['hide_empty'] = 0; + $display_options['fields']['name']['empty_zero'] = 0; + $display_options['fields']['name']['link_to_user'] = 1; + $display_options['fields']['name']['overwrite_anonymous'] = 0; + + return $display_options; + } + +} diff --git a/core/modules/user/user.views.inc b/core/modules/user/user.views.inc new file mode 100644 index 0000000..6879370 --- /dev/null +++ b/core/modules/user/user.views.inc @@ -0,0 +1,485 @@ + 'uid', + 'title' => t('User'), + 'help' => t('Users who have created accounts on your site.'), + 'access query tag' => 'user_access', + ); + $data['users']['table']['entity type'] = 'user'; + + // uid + $data['users']['uid'] = array( + 'title' => t('Uid'), + 'help' => t('The user ID'), // The help that appears on the UI, + 'field' => array( + 'id' => 'user', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'user_uid', + 'name field' => 'name', // display this field in the summary + ), + 'filter' => array( + 'title' => t('Name'), + 'id' => 'user_name', + ), + 'sort' => array( + 'id' => 'standard', + ), + 'relationship' => array( + 'title' => t('Content authored'), + 'help' => t('Relate content to the user who created it. This relationship will create one record for each content item created by the user.'), + 'id' => 'standard', + 'base' => 'node', + 'base field' => 'uid', + 'field' => 'uid', + 'label' => t('nodes'), + ), + ); + + // uid_raw + $data['users']['uid_raw'] = array( + 'help' => t('The raw numeric user ID.'), + 'real field' => 'uid', + 'filter' => array( + 'title' => t('The user ID'), + 'id' => 'numeric', + ), + ); + + // uid + $data['users']['uid_representative'] = array( + 'relationship' => array( + 'title' => t('Representative node'), + 'label' => t('Representative node'), + 'help' => t('Obtains a single representative node for each user, according to a chosen sort criterion.'), + 'id' => 'groupwise_max', + 'relationship field' => 'uid', + 'outer field' => 'users.uid', + 'argument table' => 'users', + 'argument field' => 'uid', + 'base' => 'node', + 'field' => 'nid', + ), + ); + + // uid + $data['users']['uid_current'] = array( + 'real field' => 'uid', + 'title' => t('Current'), + 'help' => t('Filter the view to the currently logged in user.'), + 'filter' => array( + 'id' => 'user_current', + 'type' => 'yes-no', + ), + ); + + // name + $data['users']['name'] = array( + 'title' => t('Name'), // The item it appears as on the UI, + 'help' => t('The user or author name.'), // The help that appears on the UI, + 'field' => array( + 'id' => 'user_name', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'argument' => array( + 'id' => 'string', + ), + 'filter' => array( + 'id' => 'string', + 'title' => t('Name (raw)'), + 'help' => t('The user or author name. This filter does not check if the user exists and allows partial matching. Does not utilize autocomplete.') + ), + ); + + // mail + // Note that this field implements field level access control. + $data['users']['mail'] = array( + 'title' => t('E-mail'), // The item it appears as on the UI, + 'help' => t('Email address for a given user. This field is normally not shown to users, so be cautious when using it.'), // The help that appears on the UI, + 'field' => array( + 'id' => 'user_mail', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // language + $data['users']['language']['moved to'] = array('users', 'langcode'); + $data['users']['langcode'] = array( + 'title' => t('Language'), // The item it appears as on the UI, + 'help' => t('Language of the user'), + 'field' => array( + 'id' => 'user_language', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'language', + ), + 'argument' => array( + 'id' => 'language', + ), + ); + + // picture + $data['users']['picture'] = array( + 'title' => t('Picture'), + 'help' => t("The user's picture, if allowed."), // The help that appears on the UI, + // Information for displaying the uid + 'field' => array( + 'id' => 'user_picture', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Has Avatar'), + 'type' => 'yes-no', + ), + ); + + // link + $data['users']['view_user'] = array( + 'field' => array( + 'title' => t('Link'), + 'help' => t('Provide a simple link to the user.'), + 'id' => 'user_link', + ), + ); + + // created field + $data['users']['created'] = array( + 'title' => t('Created date'), // The item it appears as on the UI, + 'help' => t('The date the user was created.'), // The help that appears on the UI, + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date' + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + $data['users']['created_fulldate'] = array( + 'title' => t('Created date'), + 'help' => t('Date in the form of CCYYMMDD.'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_fulldate', + ), + ); + + $data['users']['created_year_month'] = array( + 'title' => t('Created year + month'), + 'help' => t('Date in the form of YYYYMM.'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_year_month', + ), + ); + + $data['users']['created_year'] = array( + 'title' => t('Created year'), + 'help' => t('Date in the form of YYYY.'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_year', + ), + ); + + $data['users']['created_month'] = array( + 'title' => t('Created month'), + 'help' => t('Date in the form of MM (01 - 12).'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_month', + ), + ); + + $data['users']['created_day'] = array( + 'title' => t('Created day'), + 'help' => t('Date in the form of DD (01 - 31).'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_day', + ), + ); + + $data['users']['created_week'] = array( + 'title' => t('Created week'), + 'help' => t('Date in the form of WW (01 - 53).'), + 'argument' => array( + 'field' => 'created', + 'id' => 'node_created_week', + ), + ); + + // access field + $data['users']['access'] = array( + 'title' => t('Last access'), // The item it appears as on the UI, + 'help' => t("The user's last access date."), // The help that appears on the UI, + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date' + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + // login field + $data['users']['login'] = array( + 'title' => t('Last login'), // The item it appears as on the UI, + 'help' => t("The user's last login date."), // The help that appears on the UI, + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date' + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + // active status + $data['users']['status'] = array( + 'title' => t('Active'), // The item it appears as on the UI, + 'help' => t('Whether a user is active or blocked.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'active-blocked' => array(t('Active'), t('Blocked')), + ), + ), + 'filter' => array( + 'id' => 'boolean', + 'label' => t('Active'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // log field + $data['users']['signature'] = array( + 'title' => t('Signature'), // The item it appears as on the UI, + 'help' => t("The user's signature."), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'id' => 'markup', + 'format' => filter_fallback_format(), + ), + 'filter' => array( + 'id' => 'string', + ), + ); + + $data['users']['edit_node'] = array( + 'field' => array( + 'title' => t('Edit link'), + 'help' => t('Provide a simple link to edit the user.'), + 'id' => 'user_link_edit', + ), + ); + + $data['users']['cancel_node'] = array( + 'field' => array( + 'title' => t('Cancel link'), + 'help' => t('Provide a simple link to cancel the user.'), + 'id' => 'user_link_cancel', + ), + ); + + $data['users']['data'] = array( + 'title' => t('Data'), + 'help' => t('Provide serialized data of the user'), + 'field' => array( + 'id' => 'serialized', + ), + ); + + // ---------------------------------------------------------------------- + // users_roles table + + $data['users_roles']['table']['group'] = t('User'); + + // Explain how this table joins to others. + $data['users_roles']['table']['join'] = array( + // Directly links to users table. + 'users' => array( + 'left_field' => 'uid', + 'field' => 'uid', + ), + ); + + $data['users_roles']['rid'] = array( + 'title' => t('Roles'), + 'help' => t('Roles that a user belongs to.'), + 'field' => array( + 'id' => 'user_roles', + 'no group by' => TRUE, + ), + 'filter' => array( + 'id' => 'user_roles', + 'allow empty' => TRUE, + ), + 'argument' => array( + 'id' => 'users_roles_rid', + 'name table' => 'role', + 'name field' => 'name', + 'empty field name' => t('No role'), + 'zero is null' => TRUE, + 'numeric' => TRUE, + ), + ); + + // ---------------------------------------------------------------------- + // role table + + $data['role']['table']['join'] = array( + // Directly links to users table. + 'users' => array( + 'left_table' => 'users_roles', + 'left_field' => 'rid', + 'field' => 'rid', + ), + // needed for many to one helper sometimes + 'users_roles' => array( + 'left_field' => 'rid', + 'field' => 'rid', + ), + ); + + // permission table + $data['role_permission']['table']['group'] = t('User'); + $data['role_permission']['table']['join'] = array( + // Directly links to users table. + 'users' => array( + 'left_table' => 'users_roles', + 'left_field' => 'rid', + 'field' => 'rid', + ), + ); + + $data['role_permission']['permission'] = array( + 'title' => t('Permission'), + 'help' => t('The user permissions.'), + 'field' => array( + 'id' => 'user_permissions', + 'no group by' => TRUE, + ), + 'filter' => array( + 'id' => 'user_permissions', + ), + ); + + // ---------------------------------------------------------------------- + // authmap table + + $data['authmap']['table']['group'] = t('User'); + $data['authmap']['table']['join'] = array( + // Directly links to users table. + 'users' => array( + 'left_field' => 'uid', + 'field' => 'uid', + ), + ); + + $data['authmap']['aid'] = array( + 'title' => t('Authmap ID'), + 'help' => t('The Authmap ID.'), + 'field' => array( + 'id' => 'numeric', + ), + 'filter' => array( + 'id' => 'numeric', + 'numeric' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + 'numeric' => TRUE, + ), + ); + $data['authmap']['authname'] = array( + 'title' => t('Authentication name'), + 'help' => t('The unique authentication name.'), + 'field' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'numeric', + ), + ); + $data['authmap']['module'] = array( + 'title' => t('Authentication module'), + 'help' => t('The name of the module managing the authentication entry.'), + 'field' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + return $data; +} + +/** + * Allow replacement of current userid so we can cache these queries + */ +function user_views_query_substitutions($view) { + global $user; + return array('***CURRENT_USER***' => intval($user->uid)); +} diff --git a/core/modules/views/config/views.settings.yml b/core/modules/views/config/views.settings.yml new file mode 100644 index 0000000..4b9c81c --- /dev/null +++ b/core/modules/views/config/views.settings.yml @@ -0,0 +1,35 @@ +debug: + output: '0' + region: 'footer' +display_extenders: { } +no_javascript: '0' +skip_cache: '0' +sql_signature: '0' +ui: + show: + additional_queries: '0' + advanced_column: '0' + listing_filters: '0' + master_display: '0' + performance_statistics: '0' + preview_information: '1' + sql_query: + enabled: '0' + where: 'above' + display_embed: '0' + custom_theme: '_default' + always_live_preview: '1' + always_live_preview_button: '1' + exposed_filter_any_label: 'new_any' +field_rewrite_elements: + div: DIV + span: SPAN + h1: H1 + h2: H2 + h3: H3 + h4: H4 + h5: H5 + h6: H6 + p: P + strong: STRONG + em: EM diff --git a/core/modules/views/config/views.view.archive.yml b/core/modules/views/config/views.view.archive.yml new file mode 100644 index 0000000..b5ef6a0 --- /dev/null +++ b/core/modules/views/config/views.view.archive.yml @@ -0,0 +1,103 @@ +disabled: true +api_version: '3.0' +module: node +name: archive +description: 'Display a list of months that link to content for that month.' +tag: default +base_table: node +human_name: Archive +core: '8' +display: + default: + id: default + display_title: Master + display_plugin: default + position: '1' + display_options: + query: + type: views_query + options: + query_comment: false + title: 'Monthly archive' + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + sorts: + created: + id: created + table: node + field: created + order: DESC + arguments: + created_year_month: + id: created_year_month + table: node + field: created_year_month + default_action: summary + exception: + title_enable: 1 + title_enable: 1 + title: '%1' + default_argument_type: fixed + summary: + sort_order: desc + format: default_summary + summary_options: + override: true + items_per_page: '30' + specify_validation: 1 + filters: + status: + id: status + table: node + field: status + value: 1 + group: 0 + expose: + operator: false + style: + type: default + row: + type: node + page: + id: page + display_title: Page + display_plugin: page + position: '2' + display_options: + query: + type: views_query + options: { } + path: archive + block: + id: block + display_title: Block + display_plugin: block + position: '3' + display_options: + query: + type: views_query + options: { } + defaults: + arguments: false + arguments: + created_year_month: + id: created_year_month + table: node + field: created_year_month + default_action: summary + exception: + title_enable: 1 + title_enable: 1 + title: '%1' + default_argument_type: fixed + summary: + format: default_summary + summary_options: + items_per_page: '30' + specify_validation: 1 diff --git a/core/modules/views/config/views.view.backlinks.yml b/core/modules/views/config/views.view.backlinks.yml new file mode 100644 index 0000000..c3739e8 --- /dev/null +++ b/core/modules/views/config/views.view.backlinks.yml @@ -0,0 +1,123 @@ +disabled: true +api_version: '3.0' +module: search +name: backlinks +description: 'Displays a list of nodes that link to the node, using the search backlinks table.' +tag: default +base_table: node +human_name: Backlinks +core: '8' +display: + default: + id: default + display_title: Master + display_plugin: default + position: '1' + display_options: + query: + type: views_query + options: + query_comment: false + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + options: + items_per_page: 30 + empty: + text: + id: area + table: views + field: area + empty: false + content: 'No backlinks found.' + format: '1' + fields: + title: + id: title + table: node + field: title + label: '' + link_to_node: 1 + arguments: + nid: + id: nid + table: search_node_links_to + field: nid + default_action: 'not found' + title_enable: 1 + title: 'Pages that link to %1' + default_argument_type: fixed + summary: + format: default_summary + specify_validation: 1 + validate: + type: node + filters: + status: + id: status + table: node + field: status + value: 1 + group: 0 + expose: + operator: false + style: + type: html_list + options: + type: ol + row: + type: fields + page: + id: page + display_title: Page + display_plugin: page + position: '2' + display_options: + query: + type: views_query + options: { } + path: node/%/backlinks + menu: + type: tab + title: 'What links here' + weight: '0' + block: + id: block + display_title: 'What links here' + display_plugin: block + position: '3' + display_options: + query: + type: views_query + options: { } + defaults: + use_more: false + style_plugin: false + style_options: false + row_plugin: false + row_options: false + arguments: false + use_more: true + arguments: + nid: + id: nid + table: search_node_links_to + field: nid + default_action: default + title_enable: 1 + title: 'What links here' + default_argument_type: node + summary: + format: default_summary + specify_validation: 1 + validate: + type: node + style: + type: html_list + row: + type: fields diff --git a/core/modules/views/config/views.view.comments_recent.yml b/core/modules/views/config/views.view.comments_recent.yml new file mode 100644 index 0000000..337c356 --- /dev/null +++ b/core/modules/views/config/views.view.comments_recent.yml @@ -0,0 +1,126 @@ +disabled: true +api_version: '3.0' +module: comment +name: comments_recent +description: 'Contains a block and a page to list recent comments; the block will automatically link to the page, which displays the comment body as well as a link to the node.' +tag: default +base_table: comment +human_name: 'Recent comments' +core: '8' +display: + default: + id: default + display_title: Master + display_plugin: default + position: '1' + display_options: + query: + type: views_query + options: + query_comment: false + title: 'Recent comments' + use_more: true + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: some + options: + items_per_page: 5 + relationships: + nid: + id: nid + table: comment + field: nid + fields: + subject: + id: subject + table: comment + field: subject + label: '' + link_to_comment: 1 + timestamp: + id: timestamp + table: comment + field: changed + label: '' + date_format: 'time ago' + sorts: + timestamp: + id: timestamp + table: comment + field: changed + order: DESC + filters: + status_extra: + id: status_extra + table: node + field: status_extra + relationship: nid + group: 0 + style: + type: html_list + row: + type: fields + page: + id: page + display_title: Page + display_plugin: page + position: '2' + display_options: + query: + type: views_query + options: { } + defaults: + style_plugin: false + style_options: false + row_plugin: false + row_options: false + fields: false + fields: + title: + id: title + table: node + field: title + relationship: nid + label: 'Reply to' + link_to_node: 1 + timestamp: + id: timestamp + table: comment + field: changed + label: '' + date_format: 'time ago' + subject: + id: subject + table: comment + field: subject + label: '' + link_to_comment: 1 + comment: + id: comment + table: field_data_comment_body + field: comment_body + label: '' + path: comments/recent + style: + type: html_list + row: + type: fields + options: + inline: + title: title + timestamp: timestamp + separator: ' ' + block: + id: block + display_title: Block + display_plugin: block + position: '3' + display_options: + query: + type: views_query + options: { } diff --git a/core/modules/views/config/views.view.frontpage.yml b/core/modules/views/config/views.view.frontpage.yml new file mode 100644 index 0000000..4ad6164 --- /dev/null +++ b/core/modules/views/config/views.view.frontpage.yml @@ -0,0 +1,95 @@ +disabled: true +api_version: '3.0' +module: node +name: frontpage +description: 'Emulates the default Drupal front page; you may set the default home page path to this view to make it your front page.' +tag: default +base_table: node +human_name: 'Front page' +core: '8' +display: + default: + id: default + display_title: Master + display_plugin: default + position: '1' + display_options: + query: + type: views_query + options: + query_comment: false + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: node + options: + links: 1 + sorts: + sticky: + id: sticky + table: node + field: sticky + order: DESC + created: + id: created + table: node + field: created + order: DESC + filters: + promote: + id: promote + table: node + field: promote + value: '1' + group: 0 + expose: + operator: false + status: + id: status + table: node + field: status + value: '1' + group: 0 + expose: + operator: false + page: + id: page + display_title: Page + display_plugin: page + position: '2' + display_options: + query: + type: views_query + options: { } + path: frontpage + feed: + id: feed + display_title: Feed + display_plugin: feed + position: '3' + display_options: + query: + type: views_query + options: { } + defaults: + title: false + title: 'Front page feed' + pager: + type: some + style: + type: rss + row: + type: node_rss + path: rss.xml + displays: + default: default + page: page + sitename_title: '1' diff --git a/core/modules/views/config/views.view.glossary.yml b/core/modules/views/config/views.view.glossary.yml new file mode 100644 index 0000000..88de699 --- /dev/null +++ b/core/modules/views/config/views.view.glossary.yml @@ -0,0 +1,150 @@ +disabled: true +api_version: '3.0' +module: node +name: glossary +description: 'A list of all content, by letter.' +tag: default +base_table: node +human_name: Glossary +core: '8' +display: + default: + id: default + display_title: Master + display_plugin: default + position: '1' + display_options: + query: + type: views_query + options: + query_comment: false + use_ajax: true + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + options: + items_per_page: 36 + fields: + title: + id: title + table: node + field: title + link_to_node: 1 + name: + id: name + table: users + field: name + label: Author + link_to_user: 1 + relationship: uid + changed: + id: changed + table: node + field: changed + label: 'Last update' + date_format: large + arguments: + title: + id: title + table: node + field: title + default_action: default + exception: + title_enable: 1 + default_argument_type: fixed + default_argument_options: + argument: a + summary: + format: default_summary + specify_validation: 1 + glossary: 1 + limit: '1' + case: upper + path_case: lower + transform_dash: 0 + relationships: + uid: + id: uid + table: node + field: uid + style: + type: table + options: + columns: + title: title + name: name + changed: changed + default: title + info: + title: + sortable: 1 + separator: '' + name: + sortable: 1 + separator: '' + changed: + sortable: 1 + separator: '' + override: 1 + sticky: 0 + page: + id: page + display_title: Page + display_plugin: page + position: '2' + display_options: + query: + type: views_query + options: { } + path: glossary + menu: + type: normal + title: Glossary + weight: '0' + attachment: + id: attachment + display_title: Attachment + display_plugin: attachment + position: '3' + display_options: + query: + type: views_query + options: { } + pager: + type: none + options: + offset: '0' + defaults: + arguments: false + arguments: + title: + id: title + table: node + field: title + default_action: summary + exception: + title_enable: 1 + default_argument_type: fixed + default_argument_options: + argument: a + summary: + format: unformatted_summary + summary_options: + items_per_page: '25' + inline: 1 + separator: ' | ' + specify_validation: 1 + glossary: 1 + limit: '1' + case: upper + path_case: lower + transform_dash: 0 + displays: + default: default + page: page + inherit_arguments: 0 diff --git a/core/modules/views/config/views.view.taxonomy_term.yml b/core/modules/views/config/views.view.taxonomy_term.yml new file mode 100644 index 0000000..8cd3c79 --- /dev/null +++ b/core/modules/views/config/views.view.taxonomy_term.yml @@ -0,0 +1,110 @@ +disabled: true +api_version: '3.0' +module: taxonomy +name: taxonomy_term +description: 'A view to emulate Drupal core''s handling of taxonomy/term.' +tag: default +base_table: node +human_name: 'Taxonomy term' +core: '8' +display: + default: + id: default + display_title: Master + display_plugin: default + position: '1' + display_options: + query: + type: views_query + options: + query_comment: false + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + sorts: + sticky: + id: sticky + table: node + field: sticky + order: DESC + created: + id: created + table: node + field: created + order: DESC + arguments: + term_node_tid_depth: + id: term_node_tid_depth + table: node + field: term_node_tid_depth + default_action: 'not found' + exception: + title_enable: 1 + title_enable: 1 + title: '%1' + default_argument_type: fixed + summary: + format: default_summary + specify_validation: 1 + validate: + type: taxonomy_term + depth: '0' + break_phrase: 1 + term_node_tid_depth_modifier: + id: term_node_tid_depth_modifier + table: node + field: term_node_tid_depth_modifier + exception: + title_enable: 1 + default_argument_type: fixed + summary: + format: default_summary + specify_validation: 1 + filters: + status_extra: + id: status_extra + table: node + field: status_extra + group: 0 + expose: + operator: false + style: + type: default + row: + type: node + page: + id: page + display_title: Page + display_plugin: page + position: '2' + display_options: + query: + type: views_query + options: { } + path: taxonomy/term/% + feed: + id: feed + display_title: Feed + display_plugin: feed + position: '3' + display_options: + query: + type: views_query + options: { } + pager: + type: full + options: + items_per_page: 15 + path: taxonomy/term/%/%/feed + displays: + page: page + default: 0 + style: + type: rss + row: + type: node_rss diff --git a/core/modules/views/config/views.view.tracker.yml b/core/modules/views/config/views.view.tracker.yml new file mode 100644 index 0000000..6d4d4a4 --- /dev/null +++ b/core/modules/views/config/views.view.tracker.yml @@ -0,0 +1,150 @@ +disabled: true +api_version: '3.0' +module: node +name: tracker +description: 'Shows all new activity on system.' +tag: default +base_table: node +human_name: Tracker +core: '8' +display: + default: + id: default + display_title: Master + display_plugin: default + position: '1' + display_options: + query: + type: views_query + options: + query_comment: false + title: 'Recent posts' + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + options: + items_per_page: '25' + relationships: + uid: + id: uid + table: node + field: uid + fields: + type: + id: type + table: node + field: type + title: + id: title + table: node + field: title + name: + id: name + table: users + field: name + relationship: uid + label: Author + comment_count: + id: comment_count + table: node_comment_statistics + field: comment_count + label: Replies + last_comment_timestamp: + id: last_comment_timestamp + table: node_comment_statistics + field: last_comment_timestamp + label: 'Last Post' + timestamp: + id: timestamp + table: history + field: timestamp + label: '' + link_to_node: 0 + comments: 1 + new_comments: + id: new_comments + table: node + field: new_comments + label: '' + hide_empty: true + suffix: ' new' + link_to_comment: 1 + sorts: + last_comment_timestamp: + id: last_comment_timestamp + table: node_comment_statistics + field: last_comment_timestamp + arguments: + uid_touch: + id: uid_touch + table: node + field: uid_touch + exception: + title_enable: 1 + title_enable: 1 + title: 'Recent posts for %1' + default_argument_type: fixed + summary: + format: default_summary + specify_validation: 1 + filters: + status: + id: status + table: node + field: status + value: '1' + group: 0 + expose: + operator: false + style: + type: table + options: + columns: + type: type + title: title + name: name + comment_count: comment_count + last_comment_timestamp: last_comment_timestamp + timestamp: title + new_comments: comment_count + default: last_comment_timestamp + info: + type: + sortable: 1 + separator: '' + title: + sortable: 1 + separator: ' ' + name: + sortable: 1 + separator: '' + comment_count: + sortable: 1 + separator: '
' + last_comment_timestamp: + sortable: 1 + separator: ' ' + timestamp: + separator: '' + new_comments: + separator: '' + override: 1 + order: desc + page: + id: page + display_title: Page + display_plugin: page + position: '2' + display_options: + query: + type: views_query + options: { } + path: tracker + menu: + type: normal + title: 'Recent posts' diff --git a/core/modules/views/css/views.base-rtl.css b/core/modules/views/css/views.base-rtl.css new file mode 100644 index 0000000..3755668 --- /dev/null +++ b/core/modules/views/css/views.base-rtl.css @@ -0,0 +1,3 @@ +.views-exposed-form .views-exposed-widget { + float: right; /* RTL */ +} diff --git a/core/modules/views/css/views.base.css b/core/modules/views/css/views.base.css new file mode 100644 index 0000000..51750a5 --- /dev/null +++ b/core/modules/views/css/views.base.css @@ -0,0 +1,32 @@ +.views-exposed-form .views-exposed-widget { + float: left; /* LTR */ +} + +.views-exposed-form .views-exposed-widget .form-submit { + margin-top: 0.5em; +} + +.views-exposed-form .form-item, +.views-exposed-form .form-submit { + margin-top: 0; + margin-bottom: 0; +} + +.views-exposed-widgets { + margin-bottom: 0.5em; +} + +/* table style column align */ +.views-align-left { + text-align: left; +} +.views-align-right { + text-align: right; +} +.views-align-center { + text-align: center; +} + +.view .progress-disabled { + float: none; +} diff --git a/core/modules/views/drush/views.drush.inc b/core/modules/views/drush/views.drush.inc new file mode 100644 index 0000000..ef64b39 --- /dev/null +++ b/core/modules/views/drush/views.drush.inc @@ -0,0 +1,382 @@ + 'views_development_settings', + 'drupal dependencies' => array('views'), + 'description' => 'Set the Views settings to more developer-oriented values.', + 'aliases' => array('vd'), + ); + + $items['views-list'] = array( + 'drupal dependencies' => array('views'), + 'description' => 'Get a list of all views in the system.', + 'aliases' => array('vl'), + 'options' => array( + 'name' => 'String contained in view\'s name by which filter the results.', + 'tags' => 'A comma-separated list of views tags by which to filter the results.', + 'status' => 'Status of the views by which to filter the results. Choices: enabled, disabled.', + ), + 'examples' => array( + 'drush vl' => 'Show a list of all available views.', + 'drush vl --name=blog' => 'Show a list of views which names contain "blog".', + 'drush vl --tags=tag1,tag2' => 'Show a list of views tagged with "tag1" or "tag2".', + 'drush vl --status=enabled' => 'Show a list of enabled views.', + ), + ); + + $items['views-analyze'] = array( + 'drupal dependencies' => array('views', 'views_ui'), + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, + 'description' => 'Get a list of all Views analyze warnings', + 'aliases' => array('va'), + ); + + $items['views-enable'] = array( + 'drupal dependencies' => array('views'), + 'description' => 'Enable the specified views.', + 'arguments' => array( + 'views' => 'A space delimited list of view names.', + ), + 'aliases' => array('ven'), + 'examples' => array( + 'drush ven frontpage taxonomy_term' => 'Enable the frontpage and taxonomy_term views.', + ), + ); + + $items['views-disable'] = array( + 'drupal dependencies' => array('views'), + 'description' => 'Disable the specified views.', + 'arguments' => array( + 'views' => 'A space delimited list of view names.', + ), + 'aliases' => array('vdis'), + 'examples' => array( + 'drush vdis frontpage taxonomy_term' => 'Disable the frontpage and taxonomy_term views.', + ), + ); + + return $items; +} + +/** + * Drush views dev command. + * + * Changes the settings to more developer oriented values. + */ +function views_development_settings() { + config('views.settings') + ->set('ui.show.listing_filters', TRUE) + ->set('ui.show.master_display', TRUE) + ->set('ui.show.advanced_column', TRUE) + ->set('ui.always_live_preview', FALSE) + ->set('ui.always_live_preview_button', TRUE) + ->set('ui.show.preview_information', TRUE) + ->set('ui.show.sql_query.enabled', TRUE) + ->set('ui.show.sql_query.where', 'above') + ->set('ui.show.performance_statistics', TRUE) + ->set('ui.show.additional_queries', TRUE) + ->set('debug.output', TRUE) + ->set('debug.region', 'message') + ->set('ui.show.display_embed', TRUE) + ->save(); + + drush_log(dt('New views configuration saved.'), 'success'); +} + +/** + * Callback function for views-list command. + */ +function drush_views_list() { + // Initialize stuf + $rows = array(); + $disabled_views = array(); + $enabled_views = array(); + + $views = views_get_all_views(); + + // Get the --name option. + $name = array_filter(drush_get_option_list('name')); + $with_name = !empty($name) ? TRUE : FALSE; + + // Get the --tags option. + $tags = array_filter(drush_get_option_list('tags')); + $with_tags = !empty($tags) ? TRUE : FALSE; + + // Get the --status option. Store user input appart to reuse it after. + $status = drush_get_option('status', FALSE); + + // Throw an error if it's an invalid status. + if ($status && !in_array($status, array('enabled', 'disabled'))) { + return drush_set_error(dt('Invalid status: @status. Available options are "enabled" or "disabled"', array('@status' => $status))); + } + + // Set the table headers. + $header = array( + dt('Machine name'), + dt('Human name'), + dt('Description'), + dt('Status'), + dt('Tag'), + ); + + // Setup a row for each view. + foreach ($views as $id => $view) { + // If options were specified, check that first mismatch push the loop to the + // next view. + if ($with_name && !stristr($view->name, $name[0])) { + continue; + } + if ($with_tags && !in_array($view->tag, $tags)) { + continue; + } + + $status_bool = $status == 'enabled'; + if ($status && ($view->isEnabled() !== $status_bool)) { + continue; + } + + $row = array(); + + $row[] = $view->id(); + $row[] = $view->label(); + $row[] = $view->description; + $row[] = $view->isEnabled() ? dt('Enabled') : dt('Disabled'); + $row[] = $view->tag; + + // Place the row in the appropiate array, so we can have disabled views at + // the bottom. + if ($view->isEnabled()) { + $enabled_views[] = $row; + } + else{ + $disabled_views[] = $row; + } + + unset($row); + } + + $disabled = count($disabled_views); + + // Sort alphabeticaly. + asort($disabled_views); + asort($enabled_views); + + // If options were used. + $summary = ''; + if ($with_name || $with_tags || $status) { + $summary = dt('Views'); + + if ($with_name) { + $summary .= ' ' . dt('named "@name"', array('@name' => $name[0])); + } + + if ($with_tags) { + $tags = implode(' or ', $tags); + $summary .= ' ' . dt('tagged with "@tags"', array('@tags' => $tags)); + } + + if ($status) { + $summary .= ' ' . dt('with a status of "@status"', array('@status' => $status)); + } + + drush_print($summary . ":\n"); + } + + // Print all rows as a table. + if (count($enabled_views) || count($disabled_views)) { + $rows = array_merge($enabled_views, $disabled_views); + $total = count($rows); + // Put the headers as first row. + array_unshift($rows, $header); + + drush_print_table($rows, TRUE); + + // Print the statistics messages. + drush_print(dt('A total of @total views were found in this Drupal installation:', array('@total' => $total))); + drush_print(' ' . dt('@dis views are disabled', array('@dis' => $disabled)) . "\n"); + } + else { + drush_set_error(dt('No views found.')); + } + +} + +/** + * Drush views analyze command. + */ +function drush_views_analyze() { + $messages_count = 0; + + $views = views_get_all_views(); + + if (!empty($views)) { + $analyzer = new Analyzer(); + foreach ($views as $view_name => $view) { + $view = $view->getExecutable(); + $analyzer->setView($view); + if ($messages = $analyzer->getMessages($view)) { + drush_print($view_name); + foreach ($messages as $message) { + $messages_count++; + drush_print($message['type'] .': '. $message['message'], 2); + } + } + } + return drush_log(dt('A total of @total views were analyzed and @messages problems were found.', array('@total' => count($views), '@messages' => $messages_count)), 'ok'); + } + else { + return drush_set_error(dt('There are no views to analyze')); + } +} + +/** + * Drush views enable command. + */ +function drush_views_enable() { + $view_names = func_get_args(); + // Return early if no view names were specified. + if (empty($view_names)) { + return drush_set_error(dt('Please specify a list of view names to enable')); + } + _views_drush_op('enable', $view_names); +} + +/** + * Drush views disable command. + */ +function drush_views_disable() { + $view_names = func_get_args(); + // Return early if no view names were specified. + if (empty($view_names)) { + return drush_set_error(dt('Please specify a list of view names to disable')); + } + _views_drush_op('disable', $view_names); +} + +/** + * Perform operations on view objects. + * + * @param string $op + * The operation to perform. + * @param array $view_names + * An array of view names to load and perform this operation on. + */ +function _views_drush_op($op = '', array $view_names = array()) { + $op_types = _views_drush_op_types(); + if (!in_array($op, array_keys($op_types))) { + return drush_set_error(dt('Invalid op type')); + } + + $view_names = drupal_map_assoc($view_names); + + if ($views = entity_load_multiple('view', $view_names)) { + foreach ($views as $view) { + $tokens = array('@view' => $view->id(), '@action' => $op_types[$op]['action']); + + if ($op_types[$op]['validate']($view)) { + $view->$op(); + drush_log(dt('View: @view has been @action', $tokens), 'success'); + } + else { + drush_log(dt('View: @view is already @action', $tokens), 'notice'); + } + // Remove this view from the viewnames input list. + unset($view_names[$view->id()]); + } + } + else { + drush_set_error(dt('No views have been loaded')); + } + + // If we have some unmatched/leftover view names that weren't loaded. + if (!empty($view_names)) { + foreach ($view_names as $viewname) { + drush_log(dt('View: @view could not be found.', array('@view' => $viewname)), 'error'); + } + } + +} + +/** + * Returns an array of op types that can be performed on views. + * + * @return array + * An associative array keyed by op type => action name. + */ +function _views_drush_op_types() { + return array( + 'enable' => array( + 'action' => dt('enabled'), + 'validate' => '_views_drush_view_is_disabled', + ), + 'disable' => array( + 'action' => dt('disabled'), + 'validate' => '_views_drush_view_is_enabled', + ), + ); +} + +/** + * Returns whether a view is enabled. + * + * @param Drupal\views\ViewStorage $view + * The view object to check. + * + * @return bool + * TRUE if the View is enabled, FALSE otherwise. + */ +function _views_drush_view_is_enabled(ViewStorage $view) { + return $view->isEnabled(); +} + +/** + * Returns whether a view is disabled. + * + * @param Drupal\views\ViewStorage $view + * The view object to check. + * + * @return bool + * TRUE if the View is disabled, FALSE otherwise. + */ +function _views_drush_view_is_disabled(ViewStorage $view) { + return !$view->isEnabled(); +} + +/** + * Adds a cache clear option for views. + */ +function views_drush_cache_clear(&$types) { + $types['views'] = 'views_invalidate_cache'; +} diff --git a/core/modules/views/includes/ajax.inc b/core/modules/views/includes/ajax.inc new file mode 100644 index 0000000..b19d311 --- /dev/null +++ b/core/modules/views/includes/ajax.inc @@ -0,0 +1,373 @@ +get('request'); + $name = $request->request->get('view_name'); + $display_id = $request->request->get('view_display_id'); + if (isset($name) && isset($display_id)) { + $args = $request->request->get('view_args'); + $args = isset($args) && $args !== '' ? explode('/', $args) : array(); + $path = $request->request->get('view_path'); + $dom_id = $request->request->get('view_dom_id'); + $dom_id = isset($dom_id) ? preg_replace('/[^a-zA-Z0-9_-]+/', '-', $dom_id) : NULL; + $pager_element = $request->request->get('pager_element'); + $pager_element = isset($pager_element) ? intval($pager_element) : NULL; + + $commands = array(); + + // Remove all of this stuff from the query of the request so it doesn't end + // up in pagers and tablesort URLs. + foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state') as $key) { + if ($request->query->has($key)) { + $request->query->remove($key); + } + if ($request->request->has($key)) { + $request->request->remove($key); + } + } + + // Load the view. + $view = views_get_view($name); + if ($view && $view->access($display_id)) { + // Fix the current path for paging. + if (!empty($path)) { + _current_path($path); + } + + // Add all $_POST data, because AJAX is always a post and many things, + // such as tablesorts, exposed filters and paging assume $_GET. + $request_all = $request->request->all(); + $query_all = $request->query->all(); + $request->query->replace($request_all + $query_all); + + // Overwrite the destination. + // @see drupal_get_destination() + $origin_destination = $path; + $query = drupal_http_build_query($request->query->all()); + if ($query != '') { + $origin_destination .= '?' . $query; + } + $destination = &drupal_static('drupal_get_destination'); + $destination = array('destination' => $origin_destination); + + // Override the display's pager_element with the one actually used. + if (isset($pager_element)) { + $commands[] = views_ajax_command_scroll_top('.view-dom-id-' . $dom_id); + $view->displayHandlers[$display_id]->setOption('pager_element', $pager_element); + } + // Reuse the same DOM id so it matches that in Drupal.settings. + $view->dom_id = $dom_id; + + $commands[] = ajax_command_replace('.view-dom-id-' . $dom_id, $view->preview($display_id, $args)); + } + drupal_alter('views_ajax_data', $commands, $view); + return array('#type' => 'ajax', '#commands' => $commands); + } +} + +/** + * Creates a Drupal AJAX 'viewsSetForm' command. + * + * @param $output + * The form to display in the modal. + * @param $title + * The title. + * @param $url + * An optional URL. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function views_ajax_command_set_form($output, $title, $url = NULL) { + $command = array( + 'command' => 'viewsSetForm', + 'output' => $output, + 'title' => $title, + ); + if (isset($url)) { + $command['url'] = $url; + } + return $command; +} + +/** + * Creates a Drupal AJAX 'viewsDismissForm' command. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function views_ajax_command_dismiss_form() { + $command = array( + 'command' => 'viewsDismissForm', + ); + return $command; +} + +/** + * Creates a Drupal AJAX 'viewsHilite' command. + * + * @param $selector + * The selector to highlight + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function views_ajax_command_hilite($selector) { + return array( + 'command' => 'viewsHilite', + 'selector' => $selector, + ); +} + +/** + * Creates a Drupal AJAX 'addTab' command. + * + * @param $id + * The DOM ID. + * @param $title + * The title. + * @param $body + * The body. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function views_ajax_command_add_tab($id, $title, $body) { + $command = array( + 'command' => 'viewsAddTab', + 'id' => $id, + 'title' => $title, + 'body' => $body, + ); + return $command; +} + +/** + * Scroll to top of the current view. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function views_ajax_command_scroll_top($selector) { + $command = array( + 'command' => 'viewsScrollTop', + 'selector' => $selector, + ); + return $command; +} + +/** + * Shows Save and Cancel buttons. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function views_ajax_command_show_buttons() { + $command = array( + 'command' => 'viewsShowButtons', + ); + return $command; +} + +/** + * Trigger the Views live preview. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function views_ajax_command_trigger_preview() { + $command = array( + 'command' => 'viewsTriggerPreview', + ); + return $command; +} + +/** + * Replace the page title. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function views_ajax_command_replace_title($title) { + $command = array( + 'command' => 'viewsReplaceTitle', + 'title' => $title, + 'siteName' => config('system.site')->get('name'), + ); + return $command; +} + +/** + * Return an AJAX error. + */ +function views_ajax_error($message) { + $commands = array(); + $commands[] = views_ajax_command_set_form($message, t('Error')); + return $commands; +} + +/** + * Wrapper around drupal_build_form to handle some AJAX stuff automatically. + * This makes some assumptions about the client. + */ +function views_ajax_form_wrapper($form_id, &$form_state) { + // This won't override settings already in. + $form_state += array( + 'rerender' => FALSE, + 'no_redirect' => !empty($form_state['ajax']), + 'no_cache' => TRUE, + 'build_info' => array( + 'args' => array(), + ), + ); + + $form = drupal_build_form($form_id, $form_state); + $output = drupal_render($form); + + // These forms have the title built in, so set the title here: + if (empty($form_state['ajax']) && !empty($form_state['title'])) { + drupal_set_title($form_state['title']); + drupal_add_css(drupal_get_path('module', 'views_ui') . '/css/views-admin.css'); + } + + if (!empty($form_state['ajax']) && (empty($form_state['executed']) || !empty($form_state['rerender']))) { + // If the form didn't execute and we're using ajax, build up a + // Ajax command list to execute. + $commands = array(); + + $display = ''; + if ($messages = theme('status_messages')) { + $display = '
' . $messages . '
'; + } + $display .= $output; + + $title = empty($form_state['title']) ? '' : $form_state['title']; + + $url = empty($form_state['url']) ? url(current_path(), array('absolute' => TRUE)) : $form_state['url']; + + $commands[] = views_ajax_command_set_form($display, $title, $url); + + if (!empty($form_state['#section'])) { + $commands[] = views_ajax_command_hilite('.' . drupal_clean_css_identifier($form_state['#section'])); + } + + return $commands; + } + + // These forms have the title built in, so set the title here: + if (empty($form_state['ajax']) && !empty($form_state['title'])) { + drupal_set_title($form_state['title']); + } + + return $output; +} + +/** + * Page callback for views user autocomplete + */ +function views_ajax_autocomplete_user($string = '') { + // The user enters a comma-separated list of user name. We only autocomplete the last name. + $array = drupal_explode_tags($string); + + // Fetch last name + $last_string = trim(array_pop($array)); + $matches = array(); + if ($last_string != '') { + $prefix = count($array) ? implode(', ', $array) . ', ' : ''; + + if (strpos('anonymous', strtolower($last_string)) !== FALSE) { + $matches[$prefix . 'Anonymous'] = 'Anonymous'; + } + + $result = db_select('users', 'u') + ->fields('u', array('uid', 'name')) + ->condition('u.name', db_like($last_string) . '%', 'LIKE') + ->range(0, 10) + ->execute() + ->fetchAllKeyed(); + + foreach ($result as $account) { + $n = $account; + // Commas and quotes in terms are special cases, so encode 'em. + if (strpos($account, ',') !== FALSE || strpos($account, '"') !== FALSE) { + $n = '"' . str_replace('"', '""', $account) . '"'; + } + $matches[$prefix . $n] = check_plain($account); + } + } + + return new JsonResponse($matches); +} + +/** + * Page callback for views taxonomy autocomplete. + * + * @param $vid + * The vocabulary id of the tags which should be returned. + * + * @param $tags_typed + * The typed string of the user. + * + * @see taxonomy_autocomplete() + */ +function views_ajax_autocomplete_taxonomy($vid, $tags_typed = '') { + // The user enters a comma-separated list of tags. We only autocomplete the last tag. + $tags_typed = drupal_explode_tags($tags_typed); + $tag_last = drupal_strtolower(array_pop($tags_typed)); + + $matches = array(); + if ($tag_last != '') { + + $query = db_select('taxonomy_term_data', 't'); + $query->addTag('translatable'); + $query->addTag('term_access'); + + // Do not select already entered terms. + if (!empty($tags_typed)) { + $query->condition('t.name', $tags_typed, 'NOT IN'); + } + // Select rows that match by term name. + $tags_return = $query + ->fields('t', array('tid', 'name')) + ->condition('t.vid', $vid) + ->condition('t.name', '%' . db_like($tag_last) . '%', 'LIKE') + ->range(0, 10) + ->execute() + ->fetchAllKeyed(); + + $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : ''; + + $term_matches = array(); + foreach ($tags_return as $tid => $name) { + $n = $name; + // Term names containing commas or quotes must be wrapped in quotes. + if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) { + $n = '"' . str_replace('"', '""', $name) . '"'; + } + // Add term name to list of matches. + $term_matches[$prefix . $n] = check_plain($name); + } + } + + return new JsonResponse($term_matches); +} + +/** + * @} + */ diff --git a/core/modules/views/includes/cache.inc b/core/modules/views/includes/cache.inc new file mode 100644 index 0000000..dc75e56 --- /dev/null +++ b/core/modules/views/includes/cache.inc @@ -0,0 +1,170 @@ +data)) { + $cache[$table] = $data->data; + } + else { + // No cache entry, rebuild. + $cache = _views_fetch_data_build(); + $fully_loaded = TRUE; + } + } + if (isset($cache[$table])) { + if (isset($cache[$table]['moved to']) && $move) { + $moved_table = $cache[$table]['moved to']; + if (!empty($recursion_protection[$table])) { + // recursion detected! + return NULL; + } + $recursion_protection[$table] = TRUE; + $data = _views_fetch_data($moved_table); + $recursion_protection = array(); + return $data; + } + return $cache[$table]; + } + } + else { + if (!$fully_loaded) { + $data = views_cache_get('views_data', TRUE); + if (!empty($data->data)) { + $cache = $data->data; + } + + if (empty($cache)) { + $cache = _views_fetch_data_build(); + } + $fully_loaded = TRUE; + } + return $cache; + } + // Return an empty array if there is no match. + return array(); +} + +/** + * Build, set the views data cache if empty and return the views data. + * + * @return array + * The views_data of all tables. + */ +function _views_fetch_data_build() { + $cache = module_invoke_all('views_data'); + foreach (module_implements('views_data_alter') as $module) { + $function = $module . '_views_data_alter'; + $function($cache); + } + _views_data_process_entity_types($cache); + + // Keep a record with all data. + views_cache_set('views_data', $cache, TRUE); + // Save data in seperate cache entries. + foreach ($cache as $key => $data) { + $cid = 'views_data:' . $key; + views_cache_set($cid, $data, TRUE); + } + return $cache; +} + +/** + * Links tables having an 'entity type' specified to the respective generic entity-type tables. + */ +function _views_data_process_entity_types(&$data) { + foreach ($data as $table_name => $table_info) { + // Add in a join from the entity-table if an entity-type is given. + if (!empty($table_info['table']['entity type'])) { + $entity_table = 'views_entity_' . $table_info['table']['entity type']; + + $data[$entity_table]['table']['join'][$table_name] = array( + 'left_table' => $table_name, + ); + $data[$entity_table]['table']['entity type'] = $table_info['table']['entity type']; + // Copy over the default table group if we have none yet. + if (!empty($table_info['table']['group']) && empty($data[$entity_table]['table']['group'])) { + $data[$entity_table]['table']['group'] = $table_info['table']['group']; + } + } + } +} + +/** + * Set a cached item in the views cache. + * + * This is just a convenience wrapper around cache_set(). + * + * @param $cid + * The cache ID of the data to store. + * @param $data + * The data to store in the cache. Complex data types will be automatically serialized before insertion. + * Strings will be stored as plain text and not serialized. + * @param $use_language + * If TRUE, the data will be cached specific to the currently active language. + */ +function views_cache_set($cid, $data, $use_language = FALSE) { + if (config('views.settings')->get('skip_cache')) { + return; + } + if ($use_language) { + $cid .= ':' . language(LANGUAGE_TYPE_INTERFACE)->langcode; + } + + cache('views_info')->set($cid, $data); +} + +/** + * Return data from the persistent views cache. + * + * This is just a convenience wrapper around cache_get(). + * + * @param int $cid + * The cache ID of the data to retrieve. + * @param bool $use_language + * If TRUE, the data will be requested specific to the currently active language. + * + * @return stdClass|bool + * The cache or FALSE on failure. + */ +function views_cache_get($cid, $use_language = FALSE) { + if (config('views.settings')->get('skip_cache')) { + return FALSE; + } + if ($use_language) { + $cid .= ':' . language(LANGUAGE_TYPE_INTERFACE)->langcode; + } + + return cache('views_info')->get($cid); +} diff --git a/core/modules/views/js/ajax.js b/core/modules/views/js/ajax.js new file mode 100644 index 0000000..65ebfa4 --- /dev/null +++ b/core/modules/views/js/ajax.js @@ -0,0 +1,237 @@ +/** + * @file + * Handles AJAX submission and response in Views UI. + */ +(function ($) { + + "use strict"; + + Drupal.ajax.prototype.commands.viewsSetForm = function (ajax, response, status) { + var ajax_title = Drupal.settings.views.ajax.title; + var ajax_body = Drupal.settings.views.ajax.id; + var ajax_popup = Drupal.settings.views.ajax.popup; + $(ajax_title).html(response.title); + $(ajax_body).html(response.output); + $(ajax_popup).dialog('open'); + Drupal.attachBehaviors($(ajax_popup), ajax.settings); + if (response.url) { + // Identify the button that was clicked so that .ajaxSubmit() can use it. + // We need to do this for both .click() and .mousedown() since JavaScript + // code might trigger either behavior. + var $submit_buttons = $('input[type=submit], button', ajax_body); + $submit_buttons.click(function(event) { + this.form.clk = this; + }); + $submit_buttons.mousedown(function(event) { + this.form.clk = this; + }); + + $('form', ajax_body).once('views-ajax-submit-processed').each(function() { + var element_settings = { 'url': response.url, 'event': 'submit', 'progress': { 'type': 'throbber' } }; + var $form = $(this); + var id = $form.attr('id'); + Drupal.ajax[id] = new Drupal.ajax(id, this, element_settings); + Drupal.ajax[id].form = $form; + }); + } + Drupal.viewsUi.resizeModal(); + }; + + Drupal.ajax.prototype.commands.viewsDismissForm = function (ajax, response, status) { + Drupal.ajax.prototype.commands.viewsSetForm({}, {'title': '', 'output': Drupal.settings.views.ajax.defaultForm}); + $(Drupal.settings.views.ajax.popup).dialog('close'); + }; + + Drupal.ajax.prototype.commands.viewsHilite = function (ajax, response, status) { + $('.hilited').removeClass('hilited'); + $(response.selector).addClass('hilited'); + }; + + Drupal.ajax.prototype.commands.viewsAddTab = function (ajax, response, status) { + var id = '#views-tab-' + response.id; + $('#views-tabset').viewsAddTab(id, response.title, 0); + $(id).html(response.body).addClass('views-tab'); + + // Update the preview widget to preview the new tab. + var display_id = id.replace('#views-tab-', ''); + $("#preview-display-id").append(''); + + Drupal.attachBehaviors(id); + var instance = $.viewsUi.tabs.instances[$('#views-tabset').get(0).UI_TABS_UUID]; + $('#views-tabset').viewsClickTab(instance.$tabs.length); + }; + + Drupal.ajax.prototype.commands.viewsShowButtons = function (ajax, response, status) { + $('div.views-edit-view div.form-actions').removeClass('js-hide'); + $('div.views-edit-view div.view-changed.messages').removeClass('js-hide'); + }; + + Drupal.ajax.prototype.commands.viewsTriggerPreview = function (ajax, response, status) { + if ($('input#edit-displays-live-preview').is(':checked')) { + $('#preview-submit').trigger('click'); + } + }; + + Drupal.ajax.prototype.commands.viewsReplaceTitle = function (ajax, response, status) { + // In case we're in the overlay, get a reference to the underlying window. + var doc = parent.document; + // For the element, make a best-effort attempt to replace the page + // title and leave the site name alone. If the theme doesn't use the site + // name in the <title> element, this will fail. + var oldTitle = doc.title; + // Escape the site name, in case it has special characters in it, so we can + // use it in our regex. + var escapedSiteName = response.siteName.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + var re = new RegExp('.+ (.) ' + escapedSiteName); + doc.title = oldTitle.replace(re, response.title + ' $1 ' + response.siteName); + + $('h1.page-title').text(response.title); + $('h1#overlay-title').text(response.title); + }; + + /** + * Get rid of irritating tabledrag messages + */ + Drupal.theme.tableDragChangedWarning = function () { + return []; + }; + + /** + * Trigger preview when the "live preview" checkbox is checked. + */ + Drupal.behaviors.livePreview = { + attach: function (context) { + $('input#edit-displays-live-preview', context).once('views-ajax-processed').click(function() { + if ($(this).is(':checked')) { + $('#preview-submit').click(); + } + }); + } + }; + + /** + * Sync preview display. + */ + Drupal.behaviors.syncPreviewDisplay = { + attach: function (context) { + $("#views-tabset a").once('views-ajax-processed').click(function() { + var href = $(this).attr('href'); + // Cut of #views-tabset. + var display_id = href.substr(11); + // Set the form element. + $("#views-live-preview #preview-display-id").val(display_id); + }).addClass('views-ajax-processed'); + } + }; + + Drupal.behaviors.viewsAjax = { + collapseReplaced: false, + attach: function (context, settings) { + if (!settings.views) { + return; + } + // Create a jQuery UI dialog, but leave it closed. + var dialog_area = $(settings.views.ajax.popup, context); + dialog_area.dialog({ + 'autoOpen': false, + 'dialogClass': 'views-ui-dialog', + 'modal': true, + 'position': 'center', + 'resizable': false, + 'width': 750 + }); + + var base_element_settings = { + 'event': 'click', + 'progress': { 'type': 'throbber' } + }; + // Bind AJAX behaviors to all items showing the class. + $('a.views-ajax-link', context).once('views-ajax-processed').each(function () { + var element_settings = base_element_settings; + // Set the URL to go to the anchor. + if ($(this).attr('href')) { + element_settings.url = $(this).attr('href'); + } + var base = $(this).attr('id'); + Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings); + }); + + $('div#views-live-preview a') + .once('views-ajax-processed').each(function () { + // We don't bind to links without a URL. + if (!$(this).attr('href')) { + return true; + } + + var element_settings = base_element_settings; + // Set the URL to go to the anchor. + element_settings.url = $(this).attr('href'); + if (Drupal.Views.getPath(element_settings.url).substring(0, 21) !== 'admin/structure/views') { + return true; + } + + element_settings.wrapper = 'views-live-preview'; + element_settings.method = 'html'; + var base = $(this).attr('id'); + Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings); + }); + + // Within a live preview, make exposed widget form buttons re-trigger the + // Preview button. + // @todo Revisit this after fixing Views UI to display a Preview outside + // of the main Edit form. + $('div#views-live-preview input[type=submit]') + .once('views-ajax-processed').each(function(event) { + $(this).click(function () { + this.form.clk = this; + return true; + }); + var element_settings = base_element_settings; + // Set the URL to go to the anchor. + element_settings.url = $(this.form).attr('action'); + if (Drupal.Views.getPath(element_settings.url).substring(0, 21) !== 'admin/structure/views') { + return true; + } + + element_settings.wrapper = 'views-live-preview'; + element_settings.method = 'html'; + element_settings.event = 'click'; + + var base = $(this).attr('id'); + Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings); + }); + + if (!this.collapseReplaced && Drupal.collapseScrollIntoView) { + this.collapseReplaced = true; + Drupal.collapseScrollIntoView = function (node) { + for (var $parent = $(node); $parent.get(0) !== document && $parent.size() !== 0; $parent = $parent.parent()) { + if ($parent.css('overflow') === 'scroll' || $parent.css('overflow') === 'auto') { + if (Drupal.viewsUi.resizeModal) { + // If the modal is already at the max height, don't bother with + // this since the only reason to do it is to grow the modal. + if ($('.views-ui-dialog').height() < parseInt($(window).height() * .8)) { + Drupal.viewsUi.resizeModal('', true); + } + } + return; + } + } + + var h = document.documentElement.clientHeight || document.body.clientHeight || 0; + var offset = document.documentElement.scrollTop || document.body.scrollTop || 0; + var posY = $(node).offset().top; + var fudge = 55; + if (posY + node.offsetHeight + fudge > h + offset) { + if (node.offsetHeight > h) { + window.scrollTo(0, posY); + } + else { + window.scrollTo(0, posY + node.offsetHeight - h + fudge); + } + } + }; + } + } + }; + +})(jQuery); diff --git a/core/modules/views/js/ajax_view.js b/core/modules/views/js/ajax_view.js new file mode 100644 index 0000000..dafc886 --- /dev/null +++ b/core/modules/views/js/ajax_view.js @@ -0,0 +1,138 @@ +/** + * @file + * Handles AJAX fetching of views, including filter submission and response. + */ +(function ($) { + +"use strict"; + +/** + * Attaches the AJAX behavior to Views exposed filter forms and key View links. + */ +Drupal.behaviors.ViewsAjaxView = {}; +Drupal.behaviors.ViewsAjaxView.attach = function() { + if (Drupal.settings && Drupal.settings.views && Drupal.settings.views.ajaxViews) { + $.each(Drupal.settings.views.ajaxViews, function(i, settings) { + Drupal.views.instances[i] = new Drupal.views.ajaxView(settings); + }); + } +}; + +Drupal.views = {}; +Drupal.views.instances = {}; + +/** + * Javascript object for a certain view. + */ +Drupal.views.ajaxView = function(settings) { + var selector = '.view-dom-id-' + settings.view_dom_id; + this.$view = $(selector); + + // Retrieve the path to use for views' ajax. + var ajax_path = Drupal.settings.views.ajax_path; + + // If there are multiple views this might've ended up showing up multiple times. + if (ajax_path.constructor.toString().indexOf("Array") !== -1) { + ajax_path = ajax_path[0]; + } + + // Check if there are any GET parameters to send to views. + var queryString = window.location.search || ''; + if (queryString !== '') { + // Remove the question mark and Drupal path component if any. + var queryString = queryString.slice(1).replace(/q=[^&]+&?|&?render=[^&]+/, ''); + if (queryString !== '') { + // If there is a '?' in ajax_path, clean url are on and & should be used to add parameters. + queryString = ((/\?/.test(ajax_path)) ? '&' : '?') + queryString; + } + } + + this.element_settings = { + url: ajax_path + queryString, + submit: settings, + setClick: true, + event: 'click', + selector: selector, + progress: { type: 'throbber' } + }; + + this.settings = settings; + + // Add the ajax to exposed forms. + this.$exposed_form = $('form#views-exposed-form-'+ settings.view_name.replace(/_/g, '-') + '-' + settings.view_display_id.replace(/_/g, '-')); + this.$exposed_form.once(jQuery.proxy(this.attachExposedFormAjax, this)); + + // Add the ajax to pagers. + this.$view + // Don't attach to nested views. Doing so would attach multiple behaviors + // to a given element. + .filter(jQuery.proxy(this.filterNestedViews, this)) + .once(jQuery.proxy(this.attachPagerAjax, this)); +}; + +Drupal.views.ajaxView.prototype.attachExposedFormAjax = function() { + var button = $('input[type=submit], input[type=image]', this.$exposed_form); + button = button[0]; + + this.exposedFormAjax = new Drupal.ajax($(button).attr('id'), button, this.element_settings); +}; + +Drupal.views.ajaxView.prototype.filterNestedViews= function() { + // If there is at least one parent with a view class, this view + // is nested (e.g., an attachment). Bail. + return !this.$view.parents('.view').size(); +}; + +/** + * Attach the ajax behavior to each link. + */ +Drupal.views.ajaxView.prototype.attachPagerAjax = function() { + this.$view.find('ul.pager > li > a, th.views-field a, .attachment .views-summary a') + .each(jQuery.proxy(this.attachPagerLinkAjax, this)); +}; + +/** + * Attach the ajax behavior to a singe link. + */ +Drupal.views.ajaxView.prototype.attachPagerLinkAjax = function(id, link) { + var $link = $(link); + var viewData = {}; + var href = $link.attr('href'); + // Construct an object using the settings defaults and then overriding + // with data specific to the link. + $.extend( + viewData, + this.settings, + Drupal.Views.parseQueryString(href), + // Extract argument data from the URL. + Drupal.Views.parseViewArgs(href, this.settings.view_base_path) + ); + + // For anchor tags, these will go to the target of the anchor rather + // than the usual location. + $.extend(viewData, Drupal.Views.parseViewArgs(href, this.settings.view_base_path)); + + this.element_settings.submit = viewData; + this.pagerAjax = new Drupal.ajax(false, $link, this.element_settings); +}; + +Drupal.ajax.prototype.commands.viewsScrollTop = function (ajax, response, status) { + // Scroll to the top of the view. This will allow users + // to browse newly loaded content after e.g. clicking a pager + // link. + var offset = $(response.selector).offset(); + // We can't guarantee that the scrollable object should be + // the body, as the view could be embedded in something + // more complex such as a modal popup. Recurse up the DOM + // and scroll the first element that has a non-zero top. + var scrollTarget = response.selector; + while ($(scrollTarget).scrollTop() === 0 && $(scrollTarget).parent()) { + scrollTarget = $(scrollTarget).parent(); + } + // Only scroll upward + if (offset.top - 10 < $(scrollTarget).scrollTop()) { + $(scrollTarget).animate({scrollTop: (offset.top - 10)}, 500); + } +}; + +})(jQuery); diff --git a/core/modules/views/js/base.js b/core/modules/views/js/base.js new file mode 100644 index 0000000..90482e4 --- /dev/null +++ b/core/modules/views/js/base.js @@ -0,0 +1,112 @@ +/** + * @file + * Some basic behaviors and utility functions for Views. + */ +(function ($) { + + "use strict"; + + Drupal.Views = {}; + + /** + * jQuery UI tabs, Views integration component + */ + Drupal.behaviors.viewsTabs = { + attach: function (context) { + if ($.viewsUi && $.viewsUi.tabs) { + $('#views-tabset').once('views-processed').viewsTabs({ + selectedClass: 'active' + }); + } + + $('a.views-remove-link').once('views-processed').click(function(event) { + var id = $(this).attr('id').replace('views-remove-link-', ''); + $('#views-row-' + id).hide(); + $('#views-removed-' + id).attr('checked', true); + event.preventDefault(); + }); + /** + * Here is to handle display deletion + * (checking in the hidden checkbox and hiding out the row) + */ + $('a.display-remove-link') + .addClass('display-processed') + .click(function() { + var id = $(this).attr('id').replace('display-remove-link-', ''); + $('#display-row-' + id).hide(); + $('#display-removed-' + id).attr('checked', true); + event.preventDefault(); + }); + } + }; + + /** + * Helper function to parse a querystring. + */ + Drupal.Views.parseQueryString = function (query) { + var args = {}; + var pos = query.indexOf('?'); + if (pos !== -1) { + query = query.substring(pos + 1); + } + var pairs = query.split('&'); + for(var i in pairs) { + if (typeof(pairs[i]) === 'string') { + var pair = pairs[i].split('='); + // Ignore the 'q' path argument, if present. + if (pair[0] !== 'q' && pair[1]) { + args[decodeURIComponent(pair[0].replace(/\+/g, ' '))] = decodeURIComponent(pair[1].replace(/\+/g, ' ')); + } + } + } + return args; + }; + + /** + * Helper function to return a view's arguments based on a path. + */ + Drupal.Views.parseViewArgs = function (href, viewPath) { + var returnObj = {}; + var path = Drupal.Views.getPath(href); + // Ensure we have a correct path. + if (viewPath && path.substring(0, viewPath.length + 1) === viewPath + '/') { + var args = decodeURIComponent(path.substring(viewPath.length + 1, path.length)); + returnObj.view_args = args; + returnObj.view_path = path; + } + return returnObj; + }; + + /** + * Strip off the protocol plus domain from an href. + */ + Drupal.Views.pathPortion = function (href) { + // Remove e.g. http://example.com if present. + var protocol = window.location.protocol; + if (href.substring(0, protocol.length) === protocol) { + // 2 is the length of the '//' that normally follows the protocol + href = href.substring(href.indexOf('/', protocol.length + 2)); + } + return href; + }; + + /** + * Return the Drupal path portion of an href. + */ + Drupal.Views.getPath = function (href) { + href = Drupal.Views.pathPortion(href); + href = href.substring(Drupal.settings.basePath.length, href.length); + // 3 is the length of the '?q=' added to the url without clean urls. + if (href.substring(0, 3) === '?q=') { + href = href.substring(3, href.length); + } + var chars = ['#', '?', '&']; + for (var i in chars) { + if (href.indexOf(chars[i]) > -1) { + href = href.substr(0, href.indexOf(chars[i])); + } + } + return href; + }; + +})(jQuery); diff --git a/core/modules/views/js/jquery.ui.dialog.patch.js b/core/modules/views/js/jquery.ui.dialog.patch.js new file mode 100644 index 0000000..1fb9b0b --- /dev/null +++ b/core/modules/views/js/jquery.ui.dialog.patch.js @@ -0,0 +1,30 @@ +/** + * This is part of a patch to address a jQueryUI bug. The bug is responsible + * for the inability to scroll a page when a modal dialog is active. If the content + * of the dialog extends beyond the bottom of the viewport, the user is only able + * to scroll with a mousewheel or up/down keyboard keys. + * + * @see http://bugs.jqueryui.com/ticket/4671 + * @see https://bugs.webkit.org/show_bug.cgi?id=19033 + * @see views_ui.module + * @see js/jquery.ui.dialog.min.js + * + * This javascript patch overwrites the $.ui.dialog.overlay.events object to remove + * the mousedown, mouseup and click events from the list of events that are bound + * in $.ui.dialog.overlay.create + * + * The original code for this object: + * $.ui.dialog.overlay.events: $.map('focus,mousedown,mouseup,keydown,keypress,click'.split(','), + * function(event) { return event + '.dialog-overlay'; }).join(' '), + * + */ + +(function ($, undefined) { + + "use strict"; + + if ($.ui && $.ui.dialog) { + $.ui.dialog.overlay.events = $.map('focus,keydown,keypress'.split(','), + function(event) { return event + '.dialog-overlay'; }).join(' '); + } +}(jQuery)); diff --git a/core/modules/views/js/views-contextual.js b/core/modules/views/js/views-contextual.js new file mode 100644 index 0000000..3c3ae96 --- /dev/null +++ b/core/modules/views/js/views-contextual.js @@ -0,0 +1,18 @@ +/** + * @file + * Javascript related to contextual links. + */ +(function ($) { + +"use strict"; + +Drupal.behaviors.viewsContextualLinks = { + attach: function (context) { + // If there are views-related contextual links attached to the main page + // content, find the smallest region that encloses both the links and the + // view, and display it as a contextual links region. + $('.views-contextual-links-page', context).closest(':has(.view)').addClass('contextual-region'); + } +}; + +})(jQuery); diff --git a/core/modules/views/lib/Drupal/views/Analyzer.php b/core/modules/views/lib/Drupal/views/Analyzer.php new file mode 100644 index 0000000..8021e0d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Analyzer.php @@ -0,0 +1,131 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Analyzer. + */ + +namespace Drupal\views; + +use Drupal\views\ViewExecutable; + +/** + * This tool is a small plugin manager to perform analysis on a view and + * report results to the user. This tool is meant to let modules that + * provide data to Views also help users properly use that data by + * detecting invalid configurations. Views itself comes with only a + * small amount of analysis tools, but more could easily be added either + * by modules or as patches to Views itself. + */ +class Analyzer { + + /** + * The view to analyze. + * + * @var Drupal\views\ViewExecutable. + */ + protected $view; + + /** + * Constructs the analyzer object. + * + * @param Drupal\views\ViewExecutable $view + * (optional) The view to analyze. + */ + function __construct(ViewExecutable $view = NULL) { + if (isset($view)) { + $this->view = $view; + } + } + + /** + * Sets the view which is analyzed by this analyzer. + * + * @param Drupal\views\ViewExecutable + * The view to analyze. + */ + public function setView(ViewExecutable $view = NULL) { + $this->view = $view; + } + + /** + * Analyzes a review and return the results. + * + * @return array + * An array of analyze results organized into arrays keyed by 'ok', + * 'warning' and 'error'. + */ + public function getMessages() { + $this->view->initDisplay(); + $messages = module_invoke_all('views_analyze', $this->view); + + return $messages; + } + + /** + * Formats the analyze result into a message string. + * + * This is based upon the format of drupal_set_message which uses separate + * boxes for "ok", "warning" and "error". + */ + public function formatMessages(array $messages) { + if (empty($messages)) { + $messages = array($this->formatMessage(t('View analysis can find nothing to report.'), 'ok')); + } + + $types = array('ok' => array(), 'warning' => array(), 'error' => array()); + foreach ($messages as $message) { + if (empty($types[$message['type']])) { + $types[$message['type']] = array(); + } + $types[$message['type']][] = $message['message']; + } + + $output = ''; + foreach ($types as $type => $messages) { + $type .= ' messages'; + $message = ''; + if (count($messages) > 1) { + $message = theme('item_list', array('items' => $messages)); + } + elseif ($messages) { + $message = array_shift($messages); + } + + if ($message) { + $output .= "<div class=\"$type\">$message</div>"; + } + } + + return $output; + } + + /** + * Formats an analysis message. + * + * This tool should be called by any module responding to the analyze hook + * to properly format the message. It is usually used in the form: + * @code + * $ret[] = Analyzer::formatMessage(t('This is the message'), 'ok'); + * @endcode + * + * The 'ok' status should be used to provide information about things + * that are acceptable. In general analysis isn't interested in 'ok' + * messages, but instead the 'warning', which is a category for items + * that may be broken unless the user knows what he or she is doing, + * and 'error' for items that are definitely broken are much more useful. + * + * @param string $message + * @param string $type + * The type of message. This should be "ok", "warning" or "error". Other + * values can be used but how they are treated by the output routine + * is undefined. + * + * @return array + * A single formatted message, consisting of a key message and a key type. + */ + static function formatMessage($message, $type = 'error') { + return array('message' => $message, 'type' => $type); + } + +} diff --git a/core/modules/views/lib/Drupal/views/ManyToOneHelper.php b/core/modules/views/lib/Drupal/views/ManyToOneHelper.php new file mode 100644 index 0000000..18320eb --- /dev/null +++ b/core/modules/views/lib/Drupal/views/ManyToOneHelper.php @@ -0,0 +1,332 @@ +<?php + +/** + * @file + * Definition of Drupal\views\ManyToOneHelper. + */ + +namespace Drupal\views; + +use Drupal\views\Plugin\views\HandlerBase; + +/** + * This many to one helper object is used on both arguments and filters. + * + * @todo This requires extensive documentation on how this class is to + * be used. For now, look at the arguments and filters that use it. Lots + * of stuff is just pass-through but there are definitely some interesting + * areas where they interact. + * + * Any handler that uses this can have the following possibly additional + * definition terms: + * - numeric: If true, treat this field as numeric, using %d instead of %s in + * queries. + * + */ +class ManyToOneHelper { + + function __construct($handler) { + $this->handler = $handler; + } + + public static function defineOptions(&$options) { + $options['reduce_duplicates'] = array('default' => FALSE, 'bool' => TRUE); + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['reduce_duplicates'] = array( + '#type' => 'checkbox', + '#title' => t('Reduce duplicates'), + '#description' => t('This filter can cause items that have more than one of the selected options to appear as duplicate results. If this filter causes duplicate results to occur, this checkbox can reduce those duplicates; however, the more terms it has to search for, the less performant the query will be, so use this with caution. Shouldn\'t be set on single-value fields, as it may cause values to disappear from display, if used on an incompatible field.'), + '#default_value' => !empty($this->handler->options['reduce_duplicates']), + '#weight' => 4, + ); + } + + /** + * Sometimes the handler might want us to use some kind of formula, so give + * it that option. If it wants us to do this, it must set $helper->formula = TRUE + * and implement handler->get_formula(); + */ + public function getField() { + if (!empty($this->formula)) { + return $this->handler->get_formula(); + } + else { + return $this->handler->tableAlias . '.' . $this->handler->realField; + } + } + + /** + * Add a table to the query. + * + * This is an advanced concept; not only does it add a new instance of the table, + * but it follows the relationship path all the way down to the relationship + * link point and adds *that* as a new relationship and then adds the table to + * the relationship, if necessary. + */ + function add_table($join = NULL, $alias = NULL) { + // This is used for lookups in the many_to_one table. + $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field; + + if (empty($join)) { + $join = $this->getJoin(); + } + + // See if there's a chain between us and the base relationship. If so, we need + // to create a new relationship to use. + $relationship = $this->handler->relationship; + + // Determine the primary table to seek + if (empty($this->handler->query->relationships[$relationship])) { + $base_table = $this->handler->query->base_table; + } + else { + $base_table = $this->handler->query->relationships[$relationship]['base']; + } + + // Cycle through the joins. This isn't as error-safe as the normal + // ensure_path logic. Perhaps it should be. + $r_join = clone $join; + while ($r_join->leftTable != $base_table) { + $r_join = HandlerBase::getTableJoin($r_join->leftTable, $base_table); + } + // If we found that there are tables in between, add the relationship. + if ($r_join->table != $join->table) { + $relationship = $this->handler->query->add_relationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship); + } + + // And now add our table, using the new relationship if one was used. + $alias = $this->handler->query->add_table($this->handler->table, $relationship, $join, $alias); + + // Store what values are used by this table chain so that other chains can + // automatically discard those values. + if (empty($this->handler->view->many_to_one_tables[$field])) { + $this->handler->view->many_to_one_tables[$field] = $this->handler->value; + } + else { + $this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value); + } + + return $alias; + } + + public function getJoin() { + return $this->handler->getJoin(); + } + + /** + * Provide the proper join for summary queries. This is important in part because + * it will cooperate with other arguments if possible. + */ + function summary_join() { + $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field; + $join = $this->getJoin(); + + // shortcuts + $options = $this->handler->options; + $view = &$this->handler->view; + $query = &$this->handler->query; + + if (!empty($options['require_value'])) { + $join->type = 'INNER'; + } + + if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) { + return $query->ensure_table($this->handler->table, $this->handler->relationship, $join); + } + else { + if (!empty($view->many_to_one_tables[$field])) { + foreach ($view->many_to_one_tables[$field] as $value) { + $join->extra = array( + array( + 'field' => $this->handler->realField, + 'operator' => '!=', + 'value' => $value, + 'numeric' => !empty($this->definition['numeric']), + ), + ); + } + } + return $this->add_table($join); + } + } + + /** + * Override ensureMyTable so we can control how this joins in. + * The operator actually has influence over joining. + */ + public function ensureMyTable() { + if (!isset($this->handler->tableAlias)) { + // Case 1: Operator is an 'or' and we're not reducing duplicates. + // We hence get the absolute simplest: + $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field; + if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) { + if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) { + // query optimization, INNER joins are slightly faster, so use them + // when we know we can. + $join = $this->getJoin(); + if (isset($join)) { + $join->type = 'INNER'; + } + $this->handler->tableAlias = $this->handler->query->ensure_table($this->handler->table, $this->handler->relationship, $join); + $this->handler->view->many_to_one_tables[$field] = $this->handler->value; + } + else { + $join = $this->getJoin(); + $join->type = 'LEFT'; + if (!empty($this->handler->view->many_to_one_tables[$field])) { + foreach ($this->handler->view->many_to_one_tables[$field] as $value) { + $join->extra = array( + array( + 'field' => $this->handler->realField, + 'operator' => '!=', + 'value' => $value, + 'numeric' => !empty($this->handler->definition['numeric']), + ), + ); + } + } + + $this->handler->tableAlias = $this->add_table($join); + } + + return $this->handler->tableAlias; + } + + // Case 2: it's an 'and' or an 'or'. + // We do one join per selected value. + if ($this->handler->operator != 'not') { + // Clone the join for each table: + $this->handler->tableAliases = array(); + foreach ($this->handler->value as $value) { + $join = $this->getJoin(); + if ($this->handler->operator == 'and') { + $join->type = 'INNER'; + } + $join->extra = array( + array( + 'field' => $this->handler->realField, + 'value' => $value, + 'numeric' => !empty($this->handler->definition['numeric']), + ), + ); + + // The table alias needs to be unique to this value across the + // multiple times the filter or argument is called by the view. + if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) { + if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) { + $this->handler->view->many_to_one_count[$this->handler->table] = 0; + } + $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++); + } + $alias = $this->handler->tableAliases[$value] = $this->add_table($join, $this->handler->view->many_to_one_aliases[$field][$value]); + + // and set table_alias to the first of these. + if (empty($this->handler->tableAlias)) { + $this->handler->tableAlias = $alias; + } + } + } + // Case 3: it's a 'not'. + // We just do one join. We'll add a where clause during + // the query phase to ensure that $table.$field IS NULL. + else { + $join = $this->getJoin(); + $join->type = 'LEFT'; + $join->extra = array(); + $join->extra_type = 'OR'; + foreach ($this->handler->value as $value) { + $join->extra[] = array( + 'field' => $this->handler->realField, + 'value' => $value, + 'numeric' => !empty($this->handler->definition['numeric']), + ); + } + + $this->handler->tableAlias = $this->add_table($join); + } + } + return $this->handler->tableAlias; + } + + /** + * Provides a unique placeholders for handlers. + */ + protected function placeholder() { + return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']); + } + + function add_filter() { + if (empty($this->handler->value)) { + return; + } + $this->handler->ensureMyTable(); + + // Shorten some variables: + $field = $this->getField(); + $options = $this->handler->options; + $operator = $this->handler->operator; + $formula = !empty($this->formula); + $value = $this->handler->value; + if (empty($options['group'])) { + $options['group'] = 0; + } + + // add_condition determines whether a single expression is enough(FALSE) or the + // conditions should be added via an db_or()/db_and() (TRUE). + $add_condition = TRUE; + if ($operator == 'not') { + $value = NULL; + $operator = 'IS NULL'; + $add_condition = FALSE; + } + elseif ($operator == 'or' && empty($options['reduce_duplicates'])) { + if (count($value) > 1) { + $operator = 'IN'; + } + else { + $value = is_array($value) ? array_pop($value) : $value; + $operator = '='; + } + $add_condition = FALSE; + } + + if (!$add_condition) { + if ($formula) { + $placeholder = $this->placeholder(); + if ($operator == 'IN') { + $operator = "$operator IN($placeholder)"; + } + else { + $operator = "$operator $placeholder"; + } + $placeholders = array( + $placeholder => $value, + ) + $this->placeholders; + $this->handler->query->add_where_expression($options['group'], "$field $operator", $placeholders); + } + else { + $placeholder = $this->placeholder(); + if (count($this->handler->value) > 1) { + $this->query->add_where_expression(0, "$field $operator($placeholder)", array($placeholder => $value)); + } + else { + $this->handler->query->add_where_expression(0, "$field $operator $placeholder", array($placeholder => $value)); + } + } + } + + if ($add_condition) { + $field = $this->handler->realField; + $clause = $operator == 'or' ? db_or() : db_and(); + foreach ($this->handler->tableAliases as $value => $alias) { + $clause->condition("$alias.$field", $value); + } + + // implode on either AND or OR. + $this->handler->query->add_where($options['group'], $clause); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/Type/JoinManager.php b/core/modules/views/lib/Drupal/views/Plugin/Type/JoinManager.php new file mode 100644 index 0000000..eac395b --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/Type/JoinManager.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\Type\JoinManager. + */ + +namespace Drupal\views\Plugin\Type; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Component\Plugin\Factory\DefaultFactory; +use Drupal\Core\Plugin\Discovery\AlterDecorator; +use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; +use Drupal\Core\Plugin\Discovery\CacheDecorator; + +class JoinManager extends PluginManagerBase { + + /** + * Constructs a JoinManager object. + */ + public function __construct() { + $this->discovery = new CacheDecorator(new AlterDecorator(new AnnotatedClassDiscovery('views', 'join'), 'views_plugins_join'), 'views:join', 'views_info'); + $this->factory = new DefaultFactory($this); + $this->defaults = array( + 'module' => 'views', + ); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/Type/PluginManager.php b/core/modules/views/lib/Drupal/views/Plugin/Type/PluginManager.php new file mode 100644 index 0000000..f5f6f6e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/Type/PluginManager.php @@ -0,0 +1,46 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\Type\PluginManager. + */ + +namespace Drupal\views\Plugin\Type; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Component\Plugin\Factory\DefaultFactory; +use Drupal\Core\Plugin\Discovery\AlterDecorator; +use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; +use Drupal\Core\Plugin\Discovery\CacheDecorator; + +class PluginManager extends PluginManagerBase { + + /** + * Constructs a PluginManager object. + */ + public function __construct($type) { + $this->discovery = new CacheDecorator(new AlterDecorator(new AnnotatedClassDiscovery('views', $type), 'views_plugins_' . $type), 'views:' . $type, 'views_info'); + $this->factory = new DefaultFactory($this); + $this->defaults += array( + 'parent' => 'parent', + 'plugin_type' => $type, + 'module' => 'views', + ); + } + + /** + * Overrides Drupal\Component\Plugin\PluginManagerBase::processDefinition(). + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + // Setup automatic path/file finding for theme registration. + if ($definition['module'] == 'views' || isset($definition['theme'])) { + $definition += array( + 'theme path' => drupal_get_path('module', 'views') . '/theme', + 'theme file' => 'theme.inc', + ); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/Type/WizardManager.php b/core/modules/views/lib/Drupal/views/Plugin/Type/WizardManager.php new file mode 100644 index 0000000..96b544f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/Type/WizardManager.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\Type\WizardManager. + */ + +namespace Drupal\views\Plugin\Type; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Component\Plugin\Factory\DefaultFactory; +use Drupal\Core\Plugin\Discovery\AlterDecorator; +use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; +use Drupal\Core\Plugin\Discovery\CacheDecorator; + +class WizardManager extends PluginManagerBase { + + /** + * Constructs a WizardManager object. + */ + public function __construct() { + $this->discovery = new CacheDecorator(new AlterDecorator(new AnnotatedClassDiscovery('views', 'wizard'), 'views_plugins_wizard'), 'views:wizard', 'views_info'); + $this->factory = new DefaultFactory($this); + $this->defaults = array( + 'module' => 'views', + ); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/HandlerBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/HandlerBase.php new file mode 100644 index 0000000..08280ac --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/HandlerBase.php @@ -0,0 +1,932 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\HandlerBase. + */ + +namespace Drupal\views\Plugin\views; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\ViewExecutable; +use Drupal\Core\Database\Database; +use DateTimeZone; +use DateTime; + +abstract class HandlerBase extends PluginBase { + + /** + * Where the $query object will reside: + * + * @var Drupal\views\Plugin\views\query\QueryPluginBase + */ + public $query = NULL; + + /** + * The table this handler is attached to. + * + * @var string + */ + public $table; + + /** + * The alias of the table of this handler which is used in the query. + * + * @var string + */ + public $tableAlias; + + /** + * When a table has been moved this property is set. + * + * @var string + */ + public $actualTable; + + /** + * The actual field in the database table, maybe different + * on other kind of query plugins/special handlers. + * + * @var string + */ + public $realField; + + /** + * With field you can override the realField if the real field is not set. + * + * @var string + */ + public $field; + + /** + * When a field has been moved this property is set. + * + * @var string + */ + public $actualField; + + /** + * The relationship used for this field. + * + * @var string + */ + public $relationship = NULL; + + /** + * Constructs a Handler object. + */ + public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) { + parent::__construct($configuration, $plugin_id, $discovery); + $this->is_handler = TRUE; + } + + /** + * Init the handler with necessary data. + * + * @param Drupal\views\ViewExecutable $view + * The $view object this handler is attached to. + * @param array $options + * The item from the database; the actual contents of this will vary + * based upon the type of handler. + */ + public function init(ViewExecutable $view, &$options) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $display_id = $this->view->current_display; + // Check to see if this handler type is defaulted. Note that + // we have to do a lookup because the type is singular but the + // option is stored as the plural. + + // If the 'moved to' keyword moved our handler, let's fix that now. + if (isset($this->actualTable)) { + $options['table'] = $this->actualTable; + } + + if (isset($this->actualField)) { + $options['field'] = $this->actualField; + } + + $types = ViewExecutable::viewsHandlerTypes(); + $plural = $this->definition['plugin_type']; + if (isset($types[$plural]['plural'])) { + $plural = $types[$plural]['plural']; + } + if ($this->view->display_handler->isDefaulted($plural)) { + $display_id = 'default'; + } + + $this->unpackOptions($this->options, $options); + + // This exist on most handlers, but not all. So they are still optional. + if (isset($options['table'])) { + $this->table = $options['table']; + } + + // Allow alliases on both fields and tables. + if (isset($this->definition['real table'])) { + $this->table = $this->definition['real table']; + } + + if (isset($this->definition['real field'])) { + $this->realField = $this->definition['real field']; + } + + if (isset($this->definition['field'])) { + $this->realField = $this->definition['field']; + } + + if (isset($options['field'])) { + $this->field = $options['field']; + if (!isset($this->realField)) { + $this->realField = $options['field']; + } + } + + $this->query = &$view->query; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['id'] = array('default' => ''); + $options['table'] = array('default' => ''); + $options['field'] = array('default' => ''); + $options['relationship'] = array('default' => 'none'); + $options['group_type'] = array('default' => 'group'); + $options['admin_label'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + /** + * Return a string representing this handler's name in the UI. + */ + public function adminLabel($short = FALSE) { + if (!empty($this->options['admin_label'])) { + $title = check_plain($this->options['admin_label']); + return $title; + } + $title = ($short && isset($this->definition['title short'])) ? $this->definition['title short'] : $this->definition['title']; + return t('!group: !title', array('!group' => $this->definition['group'], '!title' => $title)); + } + + /** + * Shortcut to get a handler's raw field value. + * + * This should be overridden for handlers with formulae or other + * non-standard fields. Because this takes an argument, fields + * overriding this can just call return parent::getField($formula) + */ + public function getField($field = NULL) { + if (!isset($field)) { + if (!empty($this->formula)) { + $field = $this->get_formula(); + } + else { + $field = $this->tableAlias . '.' . $this->realField; + } + } + + // If grouping, check to see if the aggregation method needs to modify the field. + if ($this->view->display_handler->useGroupBy()) { + $this->view->initQuery(); + if ($this->query) { + $info = $this->query->get_aggregation_info(); + if (!empty($info[$this->options['group_type']]['method'])) { + $method = $info[$this->options['group_type']]['method']; + if (method_exists($this->query, $method)) { + return $this->query->$method($this->options['group_type'], $field); + } + } + } + } + + return $field; + } + + /** + * Sanitize the value for output. + * + * @param $value + * The value being rendered. + * @param $type + * The type of sanitization needed. If not provided, check_plain() is used. + * + * @return string + * Returns the safe value. + */ + protected function sanitizeValue($value, $type = NULL) { + switch ($type) { + case 'xss': + $value = filter_xss($value); + break; + case 'xss_admin': + $value = filter_xss_admin($value); + break; + case 'url': + $value = check_url($value); + break; + default: + $value = check_plain($value); + break; + } + return $value; + } + + /** + * Transform a string by a certain method. + * + * @param $string + * The input you want to transform. + * @param $option + * How do you want to transform it, possible values: + * - upper: Uppercase the string. + * - lower: lowercase the string. + * - ucfirst: Make the first char uppercase. + * - ucwords: Make each word in the string uppercase. + * + * @return string + * The transformed string. + */ + protected function caseTransform($string, $option) { + global $multibyte; + + switch ($option) { + default: + return $string; + case 'upper': + return drupal_strtoupper($string); + case 'lower': + return drupal_strtolower($string); + case 'ucfirst': + return drupal_strtoupper(drupal_substr($string, 0, 1)) . drupal_substr($string, 1); + case 'ucwords': + if ($multibyte == UNICODE_MULTIBYTE) { + return mb_convert_case($string, MB_CASE_TITLE); + } + else { + return ucwords($string); + } + } + } + + /** + * Validate the options form. + */ + public function validateOptionsForm(&$form, &$form_state) { } + + /** + * Build the options form. + */ + public function buildOptionsForm(&$form, &$form_state) { + // Some form elements belong in a fieldset for presentation, but can't + // be moved into one because of the form_state['values'] hierarchy. Those + // elements can add a #fieldset => 'fieldset_name' property, and they'll + // be moved to their fieldset during pre_render. + $form['#pre_render'][] = 'views_ui_pre_render_add_fieldset_markup'; + + $form['admin_label'] = array( + '#type' => 'textfield', + '#title' => t('Administrative title'), + '#description' => t('This title will be displayed on the views edit page instead of the default one. This might be useful if you have the same item twice.'), + '#default_value' => $this->options['admin_label'], + '#fieldset' => 'more', + ); + + // This form is long and messy enough that the "Administrative title" option + // belongs in a "more options" fieldset at the bottom of the form. + $form['more'] = array( + '#type' => 'fieldset', + '#title' => t('More'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 150, + ); + // Allow to alter the default values brought into the form. + drupal_alter('views_handler_options', $this->options, $view); + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitOptionsForm(&$form, &$form_state) { } + + /** + * Provides the handler some groupby. + */ + public function usesGroupBy() { + return TRUE; + } + /** + * Provide a form for aggregation settings. + */ + public function buildGroupByForm(&$form, &$form_state) { + $view = &$form_state['view']; + $display_id = $form_state['display_id']; + $types = ViewExecutable::viewsHandlerTypes(); + $type = $form_state['type']; + $id = $form_state['id']; + + $form['#title'] = check_plain($view->display[$display_id]['display_title']) . ': '; + $form['#title'] .= t('Configure aggregation settings for @type %item', array('@type' => $types[$type]['lstitle'], '%item' => $this->adminLabel())); + + $form['#section'] = $display_id . '-' . $type . '-' . $id; + + $view->initQuery(); + $info = $view->query->get_aggregation_info(); + foreach ($info as $id => $aggregate) { + $group_types[$id] = $aggregate['title']; + } + + $form['group_type'] = array( + '#type' => 'select', + '#title' => t('Aggregation type'), + '#default_value' => $this->options['group_type'], + '#description' => t('Select the aggregation function to use on this field.'), + '#options' => $group_types, + ); + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitGroupByForm(&$form, &$form_state) { + $item =& $form_state['handler']->options; + + $item['group_type'] = $form_state['values']['options']['group_type']; + } + + /** + * If a handler has 'extra options' it will get a little settings widget and + * another form called extra_options. + */ + public function hasExtraOptions() { return FALSE; } + + /** + * Provide defaults for the handler. + */ + public function defineExtraOptions(&$option) { } + + /** + * Provide a form for setting options. + */ + public function buildExtraOptionsForm(&$form, &$form_state) { } + + /** + * Validate the options form. + */ + public function validateExtraOptionsForm($form, &$form_state) { } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitExtraOptionsForm($form, &$form_state) { } + + /** + * Determine if a handler can be exposed. + */ + public function canExpose() { return FALSE; } + + /** + * Set new exposed option defaults when exposed setting is flipped + * on. + */ + public function defaultExposeOptions() { } + + /** + * Get information about the exposed form for the form renderer. + */ + public function exposedInfo() { } + + /** + * Render our chunk of the exposed handler form when selecting + */ + public function buildExposedForm(&$form, &$form_state) { } + + /** + * Validate the exposed handler form + */ + public function validateExposed(&$form, &$form_state) { } + + /** + * Submit the exposed handler form + */ + public function submitExposed(&$form, &$form_state) { } + + /** + * Form for exposed handler options. + */ + public function buildExposeForm(&$form, &$form_state) { } + + /** + * Validate the options form. + */ + public function validateExposeForm($form, &$form_state) { } + + /** + * Perform any necessary changes to the form exposes prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitExposeForm($form, &$form_state) { } + + /** + * Shortcut to display the expose/hide button. + */ + public function showExposeButton(&$form, &$form_state) { } + + /** + * Shortcut to display the exposed options form. + */ + public function showExposeForm(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + $this->buildExposeForm($form, $form_state); + + // When we click the expose button, we add new gadgets to the form but they + // have no data in $_POST so their defaults get wiped out. This prevents + // these defaults from getting wiped out. This setting will only be TRUE + // during a 2nd pass rerender. + if (!empty($form_state['force_expose_options'])) { + foreach (element_children($form['expose']) as $id) { + if (isset($form['expose'][$id]['#default_value']) && !isset($form['expose'][$id]['#value'])) { + $form['expose'][$id]['#value'] = $form['expose'][$id]['#default_value']; + } + } + } + } + + /** + * Check whether current user has access to this handler. + * + * @return boolean + */ + public function access() { + if (isset($this->definition['access callback']) && function_exists($this->definition['access callback'])) { + if (isset($this->definition['access arguments']) && is_array($this->definition['access arguments'])) { + return call_user_func_array($this->definition['access callback'], $this->definition['access arguments']); + } + return $this->definition['access callback'](); + } + + return TRUE; + } + + /** + * Run before the view is built. + * + * This gives all the handlers some time to set up before any handler has + * been fully run. + */ + public function preQuery() { } + + /** + * Run after the view is executed, before the result is cached. + * + * This gives all the handlers some time to modify values. This is primarily + * used so that handlers that pull up secondary data can put it in the + * $values so that the raw data can be utilized externally. + */ + public function postExecute(&$values) { } + + /** + * Provides a unique placeholders for handlers. + * + * @return string + * A placeholder which contains the table and the fieldname. + */ + protected function placeholder() { + return $this->query->placeholder($this->table . '_' . $this->field); + } + + /** + * Called just prior to query(), this lets a handler set up any relationship + * it needs. + */ + public function setRelationship() { + // Ensure this gets set to something. + $this->relationship = NULL; + + // Don't process non-existant relationships. + if (empty($this->options['relationship']) || $this->options['relationship'] == 'none') { + return; + } + + $relationship = $this->options['relationship']; + + // Ignore missing/broken relationships. + if (empty($this->view->relationship[$relationship])) { + return; + } + + // Check to see if the relationship has already processed. If not, then we + // cannot process it. + if (empty($this->view->relationship[$relationship]->alias)) { + return; + } + + // Finally! + $this->relationship = $this->view->relationship[$relationship]->alias; + } + + /** + * Ensure the main table for this handler is in the query. This is used + * a lot. + */ + public function ensureMyTable() { + if (!isset($this->tableAlias)) { + $this->tableAlias = $this->query->ensure_table($this->table, $this->relationship); + } + return $this->tableAlias; + } + + /** + * Provide text for the administrative summary + */ + public function adminSummary() { } + + /** + * Determine if this item is 'exposed', meaning it provides form elements + * to let users modify the view. + * + * @return TRUE/FALSE + */ + public function isExposed() { + return !empty($this->options['exposed']); + } + + /** + * Returns TRUE if the exposed filter works like a grouped filter. + */ + public function isAGroup() { return FALSE; } + + /** + * Define if the exposed input has to be submitted multiple times. + * This is TRUE when exposed filters grouped are using checkboxes as + * widgets. + */ + public function multipleExposedInput() { return FALSE; } + + /** + * Take input from exposed handlers and assign to this handler, if necessary. + */ + public function acceptExposedInput($input) { return TRUE; } + + /** + * If set to remember exposed input in the session, store it there. + */ + public function storeExposedInput($input, $status) { return TRUE; } + + /** + * Get the join object that should be used for this handler. + * + * This method isn't used a great deal, but it's very handy for easily + * getting the join if it is necessary to make some changes to it, such + * as adding an 'extra'. + */ + public function getJoin() { + // get the join from this table that links back to the base table. + // Determine the primary table to seek + if (empty($this->query->relationships[$this->relationship])) { + $base_table = $this->query->base_table; + } + else { + $base_table = $this->query->relationships[$this->relationship]['base']; + } + + $join = $this->getTableJoin($this->table, $base_table); + if ($join) { + return clone $join; + } + } + + /** + * Validates the handler against the complete View. + * + * This is called when the complete View is being validated. For validating + * the handler options form use validateOptionsForm(). + * + * @see views_handler::validateOptionsForm() + * + * @return + * Empty array if the handler is valid; an array of error strings if it is not. + */ + public function validate() { return array(); } + + /** + * Determine if the handler is considered 'broken', meaning it's a + * a placeholder used when a handler can't be found. + */ + public function broken() { } + + /** + * Creates cross-database SQL date formatting. + * + * @param string $format + * A format string for the result, like 'Y-m-d H:i:s'. + * + * @return string + * An appropriate SQL string for the DB type and field type. + */ + public function getSQLFormat($format) { + $db_type = Database::getConnection()->databaseType(); + $field = $this->getSQLDateField(); + switch ($db_type) { + case 'mysql': + $replace = array( + 'Y' => '%Y', + 'y' => '%y', + 'M' => '%b', + 'm' => '%m', + 'n' => '%c', + 'F' => '%M', + 'D' => '%a', + 'd' => '%d', + 'l' => '%W', + 'j' => '%e', + 'W' => '%v', + 'H' => '%H', + 'h' => '%h', + 'i' => '%i', + 's' => '%s', + 'A' => '%p', + ); + $format = strtr($format, $replace); + return "DATE_FORMAT($field, '$format')"; + case 'pgsql': + $replace = array( + 'Y' => 'YYYY', + 'y' => 'YY', + 'M' => 'Mon', + 'm' => 'MM', + 'n' => 'MM', // no format for Numeric representation of a month, without leading zeros + 'F' => 'Month', + 'D' => 'Dy', + 'd' => 'DD', + 'l' => 'Day', + 'j' => 'DD', // no format for Day of the month without leading zeros + 'W' => 'WW', + 'H' => 'HH24', + 'h' => 'HH12', + 'i' => 'MI', + 's' => 'SS', + 'A' => 'AM', + ); + $format = strtr($format, $replace); + return "TO_CHAR($field, '$format')"; + case 'sqlite': + $replace = array( + 'Y' => '%Y', // 4 digit year number + 'y' => '%Y', // no format for 2 digit year number + 'M' => '%m', // no format for 3 letter month name + 'm' => '%m', // month number with leading zeros + 'n' => '%m', // no format for month number without leading zeros + 'F' => '%m', // no format for full month name + 'D' => '%d', // no format for 3 letter day name + 'd' => '%d', // day of month number with leading zeros + 'l' => '%d', // no format for full day name + 'j' => '%d', // no format for day of month number without leading zeros + 'W' => '%W', // ISO week number + 'H' => '%H', // 24 hour hour with leading zeros + 'h' => '%H', // no format for 12 hour hour with leading zeros + 'i' => '%M', // minutes with leading zeros + 's' => '%S', // seconds with leading zeros + 'A' => '', // no format for AM/PM + ); + $format = strtr($format, $replace); + return "strftime('$format', $field, 'unixepoch')"; + } + } + + /** + * Creates cross-database SQL dates. + * + * @return string + * An appropriate SQL string for the db type and field type. + */ + public function getSQLDateField() { + $field = "$this->tableAlias.$this->realField"; + $db_type = Database::getConnection()->databaseType(); + $offset = $this->getTimezone(); + if (isset($offset) && !is_numeric($offset)) { + $dtz = new DateTimeZone($offset); + $dt = new DateTime('now', $dtz); + $offset_seconds = $dtz->getOffset($dt); + } + + switch ($db_type) { + case 'mysql': + $field = "DATE_ADD('19700101', INTERVAL $field SECOND)"; + if (!empty($offset)) { + $field = "($field + INTERVAL $offset_seconds SECOND)"; + } + return $field; + case 'pgsql': + $field = "TO_TIMESTAMP($field)"; + if (!empty($offset)) { + $field = "($field + INTERVAL '$offset_seconds SECONDS')"; + } + return $field; + case 'sqlite': + if (!empty($offset)) { + $field = "($field + '$offset_seconds')"; + } + return $field; + } + } + + /** + * Figure out what timezone we're in; needed for some date manipulations. + */ + public static function getTimezone() { + global $user; + if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) { + $timezone = $user->timezone; + } + else { + $timezone = variable_get('date_default_timezone', 0); + } + + // set up the database timezone + $db_type = Database::getConnection()->databaseType(); + if (in_array($db_type, array('mysql', 'pgsql'))) { + $offset = '+00:00'; + static $already_set = FALSE; + if (!$already_set) { + if ($db_type == 'pgsql') { + db_query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE"); + } + elseif ($db_type == 'mysql') { + db_query("SET @@session.time_zone = '$offset'"); + } + + $already_set = TRUE; + } + } + + return $timezone; + } + + /** + * Fetches a handler to join one table to a primary table from the data cache. + * + * @param string $table + * The table to join from. + * @param string $base_table + * The table to join to. + * + * @return Drupal\views\Plugin\views\join\JoinPluginBase + */ + public static function getTableJoin($table, $base_table) { + $data = views_fetch_data($table); + if (isset($data['table']['join'][$base_table])) { + $join_info = $data['table']['join'][$base_table]; + if (!empty($join_info['join_id'])) { + $id = $join_info['join_id']; + } + else { + $id = 'standard'; + } + + $configuration = $join_info; + // Fill in some easy defaults. + if (empty($configuration['table'])) { + $configuration['table'] = $table; + } + // If this is empty, it's a direct link. + if (empty($configuration['left_table'])) { + $configuration['left_table'] = $base_table; + } + + if (isset($join_info['arguments'])) { + foreach ($join_info['arguments'] as $key => $argument) { + $configuration[$key] = $argument; + } + } + + $join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $configuration); + + return $join; + } + } + + /** + * Breaks x,y,z and x+y+z into an array. Numeric only. + * + * @param string $str + * The string to parse. + * @param Drupal\views\Plugin\views\HandlerBase|null $handler + * The handler object to use as a base. If not specified one will + * be created. + * + * @return Drupal\views\Plugin\views\HandlerBase|stdClass $handler + * The new handler object. + */ + public static function breakPhrase($str, &$handler = NULL) { + if (!$handler) { + $handler = new \stdClass(); + } + + // Set up defaults: + + if (!isset($handler->value)) { + $handler->value = array(); + } + + if (!isset($handler->operator)) { + $handler->operator = 'or'; + } + + if (empty($str)) { + return $handler; + } + + if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) { + // The '+' character in a query string may be parsed as ' '. + $handler->operator = 'or'; + $handler->value = preg_split('/[+ ]/', $str); + } + elseif (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) { + $handler->operator = 'and'; + $handler->value = explode(',', $str); + } + + // Keep an 'error' value if invalid strings were given. + if (!empty($str) && (empty($handler->value) || !is_array($handler->value))) { + $handler->value = array(-1); + return $handler; + } + + // Doubly ensure that all values are numeric only. + foreach ($handler->value as $id => $value) { + $handler->value[$id] = intval($value); + } + + return $handler; + } + + /** + * Breaks x,y,z and x+y+z into an array. Works for strings. + * + * @param string $str + * The string to parse. + * @param Drupal\views\Plugin\views\HandlerBase|null $handler + * The object to use as a base. If not specified one will + * be created. + * + * @return Drupal\views\Plugin\views\HandlerBase|stdClass $handler + * The new handler object. + */ + public static function breakPhraseString($str, &$handler = NULL) { + if (!$handler) { + $handler = new \stdClass(); + } + + // Set up defaults: + if (!isset($handler->value)) { + $handler->value = array(); + } + + if (!isset($handler->operator)) { + $handler->operator = 'or'; + } + + if ($str == '') { + return $handler; + } + + // Determine if the string has 'or' operators (plus signs) or 'and' operators + // (commas) and split the string accordingly. If we have an 'and' operator, + // spaces are treated as part of the word being split, but otherwise they are + // treated the same as a plus sign. + $or_wildcard = '[^\s+,]'; + $and_wildcard = '[^+,]'; + if (preg_match("/^({$or_wildcard}+[+ ])+{$or_wildcard}+$/", $str)) { + $handler->operator = 'or'; + $handler->value = preg_split('/[+ ]/', $str); + } + elseif (preg_match("/^({$and_wildcard}+,)*{$and_wildcard}+$/", $str)) { + $handler->operator = 'and'; + $handler->value = explode(',', $str); + } + + // Keep an 'error' value if invalid strings were given. + if (!empty($str) && (empty($handler->value) || !is_array($handler->value))) { + $handler->value = array(-1); + return $handler; + } + + // Doubly ensure that all values are strings only. + foreach ($handler->value as $id => $value) { + $handler->value[$id] = (string) $value; + } + + return $handler; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/PluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/PluginBase.php new file mode 100644 index 0000000..118eb1c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/PluginBase.php @@ -0,0 +1,230 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\PluginBase. + */ + +namespace Drupal\views\Plugin\views; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\PluginBase as ComponentPluginBase; + +abstract class PluginBase extends ComponentPluginBase { + + /** + * Options for this plugin will be held here. + * + * @var array + */ + public $options = array(); + + /** + * The top object of a view. + * + * @var Drupal\views\ViewExecutable + */ + public $view = NULL; + + /** + * The display object this plugin is for. + * + * For display plugins this is empty. + * + * @todo find a better description + * + * @var Drupal\views\Plugin\views\display\DisplayPluginBase + */ + public $displayHandler; + + /** + * Plugins's definition + * + * @var array + */ + public $definition; + + /** + * Denotes whether the plugin has an additional options form. + * + * @var bool + */ + protected $usesOptions = FALSE; + + + /** + * Constructs a Plugin object. + */ + public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) { + parent::__construct($configuration, $plugin_id, $discovery); + + $this->definition = $this->discovery->getDefinition($plugin_id) + $configuration; + } + + /** + * Information about options for all kinds of purposes will be held here. + * @code + * 'option_name' => array( + * - 'default' => default value, + * - 'translatable' => (optional) TRUE/FALSE (wrap in t() on export if true), + * - 'contains' => (optional) array of items this contains, with its own + * defaults, etc. If contains is set, the default will be ignored and + * assumed to be array(). + * - 'bool' => (optional) TRUE/FALSE Is the value a boolean value. This will + * change the export format to TRUE/FALSE instead of 1/0. + * ), + * + * @return array + * Returns the options of this handler/plugin. + */ + protected function defineOptions() { return array(); } + + protected function setOptionDefaults(&$storage, $options, $level = 0) { + foreach ($options as $option => $definition) { + if (isset($definition['contains']) && is_array($definition['contains'])) { + $storage[$option] = array(); + $this->setOptionDefaults($storage[$option], $definition['contains'], $level++); + } + elseif (!empty($definition['translatable']) && !empty($definition['default'])) { + $storage[$option] = t($definition['default']); + } + else { + $storage[$option] = isset($definition['default']) ? $definition['default'] : NULL; + } + } + } + + /** + * Unpack options over our existing defaults, drilling down into arrays + * so that defaults don't get totally blown away. + */ + public function unpackOptions(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE) { + if ($check && !is_array($options)) { + return; + } + + if (!isset($definition)) { + $definition = $this->defineOptions(); + } + + foreach ($options as $key => $value) { + if (is_array($value)) { + // Ignore arrays with no definition. + if (!$all && empty($definition[$key])) { + continue; + } + + if (!isset($storage[$key]) || !is_array($storage[$key])) { + $storage[$key] = array(); + } + + // If we're just unpacking our known options, and we're dropping an + // unknown array (as might happen for a dependent plugin fields) go + // ahead and drop that in. + if (!$all && isset($definition[$key]) && !isset($definition[$key]['contains'])) { + $storage[$key] = $value; + continue; + } + + $this->unpackOptions($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : array(), $all, FALSE); + } + else if ($all || !empty($definition[$key])) { + $storage[$key] = $value; + } + } + } + + /** + * Clears a plugin. + */ + public function destroy() { + unset($this->view, $this->display, $this->query); + } + + /** + * Init will be called after construct, when the plugin is attached to a + * view and a display. + */ + + /** + * Provide a form to edit options for this plugin. + */ + public function buildOptionsForm(&$form, &$form_state) { + // Some form elements belong in a fieldset for presentation, but can't + // be moved into one because of the form_state['values'] hierarchy. Those + // elements can add a #fieldset => 'fieldset_name' property, and they'll + // be moved to their fieldset during pre_render. + $form['#pre_render'][] = 'views_ui_pre_render_add_fieldset_markup'; + } + + /** + * Validate the options form. + */ + public function validateOptionsForm(&$form, &$form_state) { } + + /** + * Handle any special handling on the validate form. + */ + public function submitOptionsForm(&$form, &$form_state) { } + + /** + * Add anything to the query that we might need to. + */ + public function query() { } + + /** + * Provide a full list of possible theme templates used by this style. + */ + public function themeFunctions() { + return views_theme_functions($this->definition['theme'], $this->view, $this->view->display_handler->display); + } + + /** + * Provide a list of additional theme functions for the theme information page + */ + public function additionalThemeFunctions() { + $funcs = array(); + if (!empty($this->definition['additional themes'])) { + foreach ($this->definition['additional themes'] as $theme => $type) { + $funcs[] = views_theme_functions($theme, $this->view, $this->view->display_handler->display); + } + } + return $funcs; + } + + /** + * Validate that the plugin is correct and can be saved. + * + * @return + * An array of error strings to tell the user what is wrong with this + * plugin. + */ + public function validate() { return array(); } + + /** + * Returns the summary of the settings in the display. + */ + public function summaryTitle() { + return t('Settings'); + } + + /** + * Return the human readable name of the display. + * + * This appears on the ui beside each plugin and beside the settings link. + */ + public function pluginTitle() { + if (isset($this->definition['short_title'])) { + return check_plain($this->definition['short_title']); + } + return check_plain($this->definition['title']); + } + + /** + * Returns the usesOptions property. + */ + public function usesOptions() { + return $this->usesOptions; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/PluginInterface.php b/core/modules/views/lib/Drupal/views/Plugin/views/PluginInterface.php new file mode 100644 index 0000000..5b18791 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/PluginInterface.php @@ -0,0 +1,14 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\PluginInterface. + */ + +namespace Drupal\views\Plugin\views; + +use Drupal\Component\Plugin\PluginInspectionInterface; + +interface PluginInterface extends PluginInspectionInterface { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/access/AccessPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/access/AccessPluginBase.php new file mode 100644 index 0000000..a253223 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/access/AccessPluginBase.php @@ -0,0 +1,103 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\access\AccessPluginBase. + */ + +namespace Drupal\views\Plugin\views\access; + +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\ViewExecutable; + +/** + * @defgroup views_access_plugins Views access plugins + * @{ + * The base plugin to handle access to a view. + * + * Therefore it primarily has to implement the access and the get_access_callback + * method. + * + * @see hook_views_plugins() + */ + +/** + * The base plugin to handle access control. + */ +abstract class AccessPluginBase extends PluginBase { + + /** + * Initialize the plugin. + * + * @param $view + * The view object. + * @param $display + * The display handler. + */ + public function init(ViewExecutable $view, &$display, $options = NULL) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->displayHandler = &$display; + + $this->unpackOptions($this->options, $options); + } + + /** + * Retrieve the options when this is a new access + * control plugin + */ + protected function defineOptions() { return array(); } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { } + + /** + * Provide the default form form for validating options + */ + public function validateOptionsForm(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + public function submitOptionsForm(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * access control. + */ + public function summaryTitle() { + return t('Unknown'); + } + + /** + * Determine if the current user has access or not. + * + * @param Drupal\user\User $account + * The user who wants to access this view. + * + * @return TRUE + * Returns whether the user has access to the view. + */ + abstract public function access($account); + + /** + * Determine the access callback and arguments. + * + * This information will be embedded in the menu in order to reduce + * performance hits during menu item access testing, which happens + * a lot. + * + * @return array + * The first item of the array should be the function to call,and the + * second item should be an array of arguments. The first item may also be + * TRUE (bool only) which will indicate no access control. + */ + abstract function get_access_callback(); + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/access/None.php b/core/modules/views/lib/Drupal/views/Plugin/views/access/None.php new file mode 100644 index 0000000..923598c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/access/None.php @@ -0,0 +1,46 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\access\None. + */ + +namespace Drupal\views\Plugin\views\access; + +use Drupal\Core\Annotation\Translation; +use Drupal\Core\Annotation\Plugin; + +/** + * Access plugin that provides no access control at all. + * + * @ingroup views_access_plugins + * + * @Plugin( + * id = "none", + * title = @Translation("None"), + * help = @Translation("Will be available to all users.") + * ) + */ +class None extends AccessPluginBase { + + public function summaryTitle() { + return t('Unrestricted'); + } + + /** + * Implements Drupal\views\Plugin\views\access\AccessPluginBase::access(). + */ + public function access($account) { + // No access control. + return TRUE; + } + + /** + * Implements Drupal\views\Plugin\views\access\AccessPluginBase::get_access_callback(). + */ + public function get_access_callback() { + // No access control. + return TRUE; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/access/Permission.php b/core/modules/views/lib/Drupal/views/Plugin/views/access/Permission.php new file mode 100644 index 0000000..4d39fc8 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/access/Permission.php @@ -0,0 +1,80 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\access\Permission. + */ + +namespace Drupal\views\Plugin\views\access; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Access plugin that provides permission-based access control. + * + * @ingroup views_access_plugins + * + * @Plugin( + * id = "perm", + * title = @Translation("Permission"), + * help = @Translation("Access will be granted to users with the specified permission string.") + * ) + */ +class Permission extends AccessPluginBase { + + /** + * Overrides Drupal\views\Plugin\Plugin::$usesOptions. + */ + protected $usesOptions = TRUE; + + public function access($account) { + return views_check_perm($this->options['perm'], $account); + } + + function get_access_callback() { + return array('views_check_perm', array($this->options['perm'])); + } + + public function summaryTitle() { + $permissions = module_invoke_all('permission'); + if (isset($permissions[$this->options['perm']])) { + return $permissions[$this->options['perm']]['title']; + } + + return t($this->options['perm']); + } + + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['perm'] = array('default' => 'access content'); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $perms = array(); + $module_info = system_get_info('module'); + + // Get list of permissions + foreach (module_implements('permission') as $module) { + $permissions = module_invoke($module, 'permission'); + foreach ($permissions as $name => $perm) { + $perms[$module_info[$module]['name']][$name] = strip_tags($perm['title']); + } + } + + ksort($perms); + + $form['perm'] = array( + '#type' => 'select', + '#options' => $perms, + '#title' => t('Permission'), + '#default_value' => $this->options['perm'], + '#description' => t('Only users with the selected permission flag will be able to access this display. Note that users with "access all views" can see any view, regardless of other permissions.'), + ); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/access/Role.php b/core/modules/views/lib/Drupal/views/Plugin/views/access/Role.php new file mode 100644 index 0000000..647d4bf --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/access/Role.php @@ -0,0 +1,84 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\access\Role. + */ + +namespace Drupal\views\Plugin\views\access; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Access plugin that provides role-based access control. + * + * @ingroup views_access_plugins + * + * @Plugin( + * id = "role", + * title = @Translation("Role"), + * help = @Translation("Access will be granted to users with any of the specified roles.") + * ) + */ +class Role extends AccessPluginBase { + + /** + * Overrides Drupal\views\Plugin\Plugin::$usesOptions. + */ + protected $usesOptions = TRUE; + + public function access($account) { + return views_check_roles(array_filter($this->options['role']), $account); + } + + function get_access_callback() { + return array('views_check_roles', array(array_filter($this->options['role']))); + } + + public function summaryTitle() { + $count = count($this->options['role']); + if ($count < 1) { + return t('No role(s) selected'); + } + elseif ($count > 1) { + return t('Multiple roles'); + } + else { + $rids = user_roles(); + $rid = reset($this->options['role']); + return check_plain($rids[$rid]); + } + } + + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['role'] = array('default' => array()); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['role'] = array( + '#type' => 'checkboxes', + '#title' => t('Role'), + '#default_value' => $this->options['role'], + '#options' => array_map('check_plain', $this->getRoles()), + '#description' => t('Only the checked roles will be able to access this display. Note that users with "access all views" can see any view, regardless of role.'), + ); + } + + public function validateOptionsForm(&$form, &$form_state) { + if (!array_filter($form_state['values']['access_options']['role'])) { + form_error($form['role'], t('You must select at least one role if type is "by role"')); + } + } + + public function submitOptionsForm(&$form, &$form_state) { + // I hate checkboxes. + $form_state['values']['access_options']['role'] = array_filter($form_state['values']['access_options']['role']); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/area/AreaPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/area/AreaPluginBase.php new file mode 100644 index 0000000..db0383e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/area/AreaPluginBase.php @@ -0,0 +1,116 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\area\AreaPluginBase. + */ + +namespace Drupal\views\Plugin\views\area; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\Plugin\views\HandlerBase; + +/** + * @defgroup views_area_handlers Views area handlers + * @{ + * Handlers to tell Views what can display in header, footer + * and empty text in a view. + */ + +/** + * Base class for area handlers. + * + * @ingroup views_area_handlers + */ +abstract class AreaPluginBase extends HandlerBase { + + /** + * Overrides Drupal\views\Plugin\views\HandlerBase::init(). + * + * Make sure that no result area handlers are set to be shown when the result + * is empty. + */ + public function init(ViewExecutable $view, &$options) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + parent::init($view, $options); + if ($this->definition['plugin_type'] == 'empty') { + $this->options['empty'] = TRUE; + } + } + + /** + * Get this area's label. + */ + public function label() { + if (!isset($this->options['label'])) { + return $this->adminLabel(); + } + return $this->options['label']; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $this->definition['field'] = !empty($this->definition['field']) ? $this->definition['field'] : ''; + $label = !empty($this->definition['label']) ? $this->definition['label'] : $this->definition['field']; + $options['label'] = array('default' => $label, 'translatable' => TRUE); + $options['empty'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide extra data to the administration form + */ + public function adminSummary() { + return $this->label(); + } + + /** + * Default options form that provides the label widget that all fields + * should have. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#default_value' => isset($this->options['label']) ? $this->options['label'] : '', + '#description' => t('The label for this area that will be displayed only administratively.'), + ); + + + if ($form_state['type'] != 'empty') { + $form['empty'] = array( + '#type' => 'checkbox', + '#title' => t('Display even if view has no result'), + '#default_value' => isset($this->options['empty']) ? $this->options['empty'] : 0, + ); + } + } + + /** + * Don't run a query + */ + public function query() { } + + /** + * Render the area + */ + function render($empty = FALSE) { + return ''; + } + + /** + * Area handlers shouldn't have groupby. + */ + public function usesGroupBy() { + return FALSE; + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/area/Broken.php b/core/modules/views/lib/Drupal/views/Plugin/views/area/Broken.php new file mode 100644 index 0000000..a7d00c9 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/area/Broken.php @@ -0,0 +1,44 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\area\Broken + */ + +namespace Drupal\views\Plugin\views\area; + +use Drupal\Core\Annotation\Plugin; + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_area_handlers + * + * @Plugin( + * id = "broken" + * ) + */ +class Broken extends AreaPluginBase { + + public function adminLabel($short = FALSE) { + return t('Broken/missing handler'); + } + + public function defineOptions() { return array(); } + public function ensureMyTable() { /* No table to ensure! */ } + public function query($group_by = FALSE) { /* No query to run */ } + function render($empty = FALSE) { return ''; } + public function buildOptionsForm(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '<div class="form-item description">' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '</div>', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + public function broken() { + return TRUE; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/area/Result.php b/core/modules/views/lib/Drupal/views/Plugin/views/area/Result.php new file mode 100644 index 0000000..11a5f94 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/area/Result.php @@ -0,0 +1,105 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\area\Result. + */ + +namespace Drupal\views\Plugin\views\area; + +use Drupal\Core\Annotation\Plugin; + +/** + * Views area handler to display some configurable result summary. + * + * @ingroup views_area_handlers + * + * @Plugin( + * id = "result" + * ) + */ +class Result extends AreaPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['content'] = array( + 'default' => 'Displaying @start - @end of @total', + 'translatable' => TRUE, + ); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $variables = array( + 'items' => array( + '@start -- the initial record number in the set', + '@end -- the last record number in the set', + '@total -- the total records in the set', + '@name -- the human-readable name of the view', + '@per_page -- the number of items per page', + '@current_page -- the current page number', + '@current_record_count -- the current page record count', + '@page_count -- the total page count', + ), + ); + $list = theme('item_list', $variables); + $form['content'] = array( + '#title' => t('Display'), + '#type' => 'textarea', + '#rows' => 3, + '#default_value' => $this->options['content'], + '#description' => t('You may use HTML code in this field. The following tokens are supported:') . $list, + ); + } + + + /** + * Find out the information to render. + */ + function render($empty = FALSE) { + // Must have options and does not work on summaries. + if (!isset($this->options['content']) || $this->view->plugin_name == 'default_summary') { + return; + } + $output = ''; + $format = $this->options['content']; + // Calculate the page totals. + $current_page = (int) $this->view->getCurrentPage() + 1; + $per_page = (int) $this->view->getItemsPerPage(); + $count = count($this->view->result); + // @TODO: Maybe use a possible is views empty functionality. + // Not every view has total_rows set, use view->result instead. + $total = isset($this->view->total_rows) ? $this->view->total_rows : count($this->view->result); + $name = check_plain($this->view->storage->getHumanName()); + if ($per_page === 0) { + $page_count = 1; + $start = 1; + $end = $total; + } + else { + $page_count = (int) ceil($total / $per_page); + $total_count = $current_page * $per_page; + if ($total_count > $total) { + $total_count = $total; + } + $start = ($current_page - 1) * $per_page + 1; + $end = $total_count; + } + $current_record_count = ($end - $start) + 1; + // Get the search information. + $items = array('start', 'end', 'total', 'name', 'per_page', 'current_page', 'current_record_count', 'page_count'); + $replacements = array(); + foreach ($items as $item) { + $replacements["@$item"] = ${$item}; + } + // Send the output. + if (!empty($total)) { + $output .= filter_xss_admin(str_replace(array_keys($replacements), array_values($replacements), $format)); + } + return $output; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/area/Text.php b/core/modules/views/lib/Drupal/views/Plugin/views/area/Text.php new file mode 100644 index 0000000..b696414 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/area/Text.php @@ -0,0 +1,119 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\area\Text. + */ + +namespace Drupal\views\Plugin\views\area; + +use Drupal\Core\Annotation\Plugin; + +/** + * Views area text handler. + * + * @ingroup views_area_handlers + * + * @Plugin( + * id = "text" + * ) + */ +class Text extends AreaPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['content'] = array('default' => '', 'translatable' => TRUE, 'format_key' => 'format'); + $options['format'] = array('default' => NULL); + $options['tokenize'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['content'] = array( + '#type' => 'text_format', + '#default_value' => $this->options['content'], + '#rows' => 6, + '#format' => isset($this->options['format']) ? $this->options['format'] : filter_default_format(), + '#wysiwyg' => FALSE, + ); + + + $form['tokenize'] = array( + '#type' => 'checkbox', + '#title' => t('Use replacement tokens from the first row'), + '#default_value' => $this->options['tokenize'], + ); + + // Get a list of the available fields and arguments for token replacement. + $options = array(); + foreach ($this->view->display_handler->getHandlers('field') as $field => $handler) { + $options[t('Fields')]["[$field]"] = $handler->adminLabel(); + } + + $count = 0; // This lets us prepare the key as we want it printed. + foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) { + $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); + $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); + } + + if (!empty($options)) { + $output = '<p>' . t('The following tokens are available. If you would like to have the characters \'[\' and \']\' please use the html entity codes \'%5B\' or \'%5D\' or they will get replaced with empty space.' . '</p>'); + foreach (array_keys($options) as $type) { + if (!empty($options[$type])) { + $items = array(); + foreach ($options[$type] as $key => $value) { + $items[] = $key . ' == ' . $value; + } + $output .= theme('item_list', + array( + 'items' => $items, + 'type' => $type + )); + } + } + + $form['token_help'] = array( + '#type' => 'fieldset', + '#title' => t('Replacement patterns'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#value' => $output, + '#id' => 'edit-options-token-help', + '#states' => array( + 'visible' => array( + ':input[name="options[tokenize]"]' => array('checked' => TRUE), + ), + ), + ); + } + } + + public function submitOptionsForm(&$form, &$form_state) { + $form_state['values']['options']['format'] = $form_state['values']['options']['content']['format']; + $form_state['values']['options']['content'] = $form_state['values']['options']['content']['value']; + parent::submitOptionsForm($form, $form_state); + } + + function render($empty = FALSE) { + $format = isset($this->options['format']) ? $this->options['format'] : filter_default_format(); + if (!$empty || !empty($this->options['empty'])) { + return $this->render_textarea($this->options['content'], $format); + } + return ''; + } + + /** + * Render a text area, using the proper format. + */ + function render_textarea($value, $format) { + if ($value) { + if ($this->options['tokenize']) { + $value = $this->view->style_plugin->tokenize_value($value, 0); + } + return check_markup($value, $format, '', FALSE); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/area/TextCustom.php b/core/modules/views/lib/Drupal/views/Plugin/views/area/TextCustom.php new file mode 100644 index 0000000..f0d410c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/area/TextCustom.php @@ -0,0 +1,62 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\area\TextCustom. + */ + +namespace Drupal\views\Plugin\views\area; + +use Drupal\Core\Annotation\Plugin; + +/** + * Views area text handler. + * + * @ingroup views_area_handlers + * + * @Plugin( + * id = "text_custom" + * ) + */ +class TextCustom extends AreaPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + unset($options['format']); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + // Alter the form element, to be a regular text area. + $form['content']['#type'] = 'textarea'; + unset($form['content']['#format']); + unset($form['content']['#wysiwyg']); + } + + // Empty, so we don't inherit submitOptionsForm from the parent. + public function submitOptionsForm(&$form, &$form_state) { + } + + function render($empty = FALSE) { + if (!$empty || !empty($this->options['empty'])) { + return $this->render_textarea_custom($this->options['content']); + } + + return ''; + } + + /** + * Render a text area with filter_xss_admin. + */ + function render_textarea_custom($value) { + if ($value) { + if ($this->options['tokenize']) { + $value = $this->view->style_plugin->tokenize_value($value, 0); + } + return $this->sanitizeValue($value, 'xss_admin'); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/area/View.php b/core/modules/views/lib/Drupal/views/Plugin/views/area/View.php new file mode 100644 index 0000000..018996c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/area/View.php @@ -0,0 +1,92 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\area\View. + */ + +namespace Drupal\views\Plugin\views\area; + +use Drupal\Core\Annotation\Plugin; + +/** + * Views area handlers. Insert a view inside of an area. + * + * @ingroup views_area_handlers + * + * @Plugin( + * id = "view" + * ) + */ +class View extends AreaPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['view_to_insert'] = array('default' => ''); + $options['inherit_arguments'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Default options form that provides the label widget that all fields + * should have. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $view_display = $this->view->storage->name . ':' . $this->view->current_display; + + $options = array('' => t('-Select-')); + $options += views_get_views_as_options(FALSE, 'all', $view_display, FALSE, TRUE); + $form['view_to_insert'] = array( + '#type' => 'select', + '#title' => t('View to insert'), + '#default_value' => $this->options['view_to_insert'], + '#description' => t('The view to insert into this area.'), + '#options' => $options, + ); + + $form['inherit_arguments'] = array( + '#type' => 'checkbox', + '#title' => t('Inherit contextual filters'), + '#default_value' => $this->options['inherit_arguments'], + '#description' => t('If checked, this view will receive the same contextual filters as its parent.'), + ); + } + + /** + * Render the area + */ + function render($empty = FALSE) { + if (!empty($this->options['view_to_insert'])) { + list($view_name, $display_id) = explode(':', $this->options['view_to_insert']); + + $view = views_get_view($view_name); + if (empty($view) || !$view->access($display_id)) { + return; + } + $view->setDisplay($display_id); + + // Avoid recursion + $view->parent_views += $this->view->parent_views; + $view->parent_views[] = "$view_name:$display_id"; + + // Check if the view is part of the parent views of this view + $search = "$view_name:$display_id"; + if (in_array($search, $this->view->parent_views)) { + drupal_set_message(t("Recursion detected in view @view display @display.", array('@view' => $view_name, '@display' => $display_id)), 'error'); + } + else { + if (!empty($this->options['inherit_arguments']) && !empty($this->view->args)) { + return $view->preview($display_id, $this->view->args); + } + else { + return $view->preview($display_id); + } + } + } + return ''; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/ArgumentPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/ArgumentPluginBase.php new file mode 100644 index 0000000..ef30140 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/ArgumentPluginBase.php @@ -0,0 +1,1121 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\ArgumentPluginBase. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\HandlerBase; + +/** + * @defgroup views_argument_handlers Views argument handlers + * Handlers to tell Views how to contextually filter queries. + * @{ + */ + +/** + * Base class for arguments. + * + * The basic argument works for very simple arguments such as nid and uid + * + * Definition terms for this handler: + * - name field: The field to use for the name to use in the summary, which is + * the displayed output. For example, for the node: nid argument, + * the argument itself is the nid, but node.title is displayed. + * - name table: The table to use for the name, should it not be in the same + * table as the argument. + * - empty field name: For arguments that can have no value, such as taxonomy + * which can have "no term", this is the string which + * will be displayed for this lack of value. Be sure to use + * t(). + * - validate type: A little used string to allow an argument to restrict + * which validator is available to just one. Use the + * validator ID. This probably should not be used at all, + * and may disappear or change. + * - numeric: If set to TRUE this field is numeric and will use %d instead of + * %s in queries. + * + * @ingroup views_argument_handlers + */ +abstract class ArgumentPluginBase extends HandlerBase { + + var $validator = NULL; + var $argument = NULL; + var $value = NULL; + + /** + * The table to use for the name, should it not be in the same table as the argument. + * @var string + */ + var $name_table; + + /** + * The field to use for the name to use in the summary, which is + * the displayed output. For example, for the node: nid argument, + * the argument itself is the nid, but node.title is displayed. + * @var string + */ + var $name_field; + + /** + * Overrides Drupal\views\Plugin\views\HandlerBase:init(). + */ + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + if (!empty($this->definition['name field'])) { + $this->name_field = $this->definition['name field']; + } + if (!empty($this->definition['name table'])) { + $this->name_table = $this->definition['name table']; + } + } + + /** + * Give an argument the opportunity to modify the breadcrumb, if it wants. + * This only gets called on displays where a breadcrumb is actually used. + * + * The breadcrumb will be in the form of an array, with the keys being + * the path and the value being the already sanitized title of the path. + */ + function set_breadcrumb(&$breadcrumb) { } + + /** + * Determine if the argument can generate a breadcrumb + * + * @return TRUE/FALSE + */ + function uses_breadcrumb() { + $info = $this->default_actions($this->options['default_action']); + return !empty($info['breadcrumb']); + } + + function is_exception($arg = NULL) { + if (!isset($arg)) { + $arg = isset($this->argument) ? $this->argument : NULL; + } + return !empty($this->options['exception']['value']) && $this->options['exception']['value'] === $arg; + } + + function exception_title() { + // If title overriding is off for the exception, return the normal title. + if (empty($this->options['exception']['title_enable'])) { + return $this->get_title(); + } + return $this->options['exception']['title']; + } + + /** + * Determine if the argument needs a style plugin. + * + * @return TRUE/FALSE + */ + public function needsStylePlugin() { + $info = $this->default_actions($this->options['default_action']); + $validate_info = $this->default_actions($this->options['validate']['fail']); + return !empty($info['style plugin']) || !empty($validate_info['style plugin']); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['default_action'] = array('default' => 'ignore'); + $options['exception'] = array( + 'contains' => array( + 'value' => array('default' => 'all'), + 'title_enable' => array('default' => FALSE, 'bool' => TRUE), + 'title' => array('default' => 'All', 'translatable' => TRUE), + ), + ); + $options['title_enable'] = array('default' => FALSE, 'bool' => TRUE); + $options['title'] = array('default' => '', 'translatable' => TRUE); + $options['breadcrumb_enable'] = array('default' => FALSE, 'bool' => TRUE); + $options['breadcrumb'] = array('default' => '', 'translatable' => TRUE); + $options['default_argument_type'] = array('default' => 'fixed'); + $options['default_argument_options'] = array('default' => array()); + $options['default_argument_skip_url'] = array('default' => FALSE, 'bool' => TRUE); + $options['summary_options'] = array('default' => array()); + $options['summary'] = array( + 'contains' => array( + 'sort_order' => array('default' => 'asc'), + 'number_of_records' => array('default' => 0), + 'format' => array('default' => 'default_summary'), + ), + ); + $options['specify_validation'] = array('default' => FALSE, 'bool' => TRUE); + $options['validate'] = array( + 'contains' => array( + 'type' => array('default' => 'none'), + 'fail' => array('default' => 'not found'), + ), + ); + $options['validate_options'] = array('default' => array()); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $argument_text = $this->view->display_handler->getArgumentText(); + + $form['#pre_render'][] = 'views_ui_pre_render_move_argument_options'; + + $form['description'] = array( + '#markup' => $argument_text['description'], + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('description')), + ); + + $form['no_argument'] = array( + '#type' => 'fieldset', + '#title' => $argument_text['filter value not present'], + ); + // Everything in the fieldset is floated, so the last element needs to + // clear those floats. + $form['no_argument']['clearfix'] = array( + '#weight' => 1000, + '#markup' => '<div class="clearfix"></div>', + ); + $form['default_action'] = array( + '#type' => 'radios', + '#process' => array(array($this, 'processContainerRadios')), + '#default_value' => $this->options['default_action'], + '#fieldset' => 'no_argument', + ); + + $form['exception'] = array( + '#type' => 'fieldset', + '#title' => t('Exceptions'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#fieldset' => 'no_argument', + ); + $form['exception']['value'] = array( + '#type' => 'textfield', + '#title' => t('Exception value'), + '#size' => 20, + '#default_value' => $this->options['exception']['value'], + '#description' => t('If this value is received, the filter will be ignored; i.e, "all values"'), + ); + $form['exception']['title_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Override title'), + '#default_value' => $this->options['exception']['title_enable'], + ); + $form['exception']['title'] = array( + '#type' => 'textfield', + '#title' => t('Override title'), + '#title_display' => 'invisible', + '#size' => 20, + '#default_value' => $this->options['exception']['title'], + '#description' => t('Override the view and other argument titles. Use "%1" for the first argument, "%2" for the second, etc.'), + '#states' => array( + 'visible' => array( + ':input[name="options[exception][title_enable]"]' => array('checked' => TRUE), + ), + ), + ); + + $options = array(); + $defaults = $this->default_actions(); + $validate_options = array(); + foreach ($defaults as $id => $info) { + $options[$id] = $info['title']; + if (empty($info['default only'])) { + $validate_options[$id] = $info['title']; + } + if (!empty($info['form method'])) { + $this->{$info['form method']}($form, $form_state); + } + } + $form['default_action']['#options'] = $options; + + $form['argument_present'] = array( + '#type' => 'fieldset', + '#title' => $argument_text['filter value present'], + ); + $form['title_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Override title'), + '#default_value' => $this->options['title_enable'], + '#fieldset' => 'argument_present', + ); + $form['title'] = array( + '#type' => 'textfield', + '#title' => t('Provide title'), + '#title_display' => 'invisible', + '#default_value' => $this->options['title'], + '#description' => t('Override the view and other argument titles. Use "%1" for the first argument, "%2" for the second, etc.'), + '#states' => array( + 'visible' => array( + ':input[name="options[title_enable]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'argument_present', + ); + + $form['breadcrumb_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Override breadcrumb'), + '#default_value' => $this->options['breadcrumb_enable'], + '#fieldset' => 'argument_present', + ); + $form['breadcrumb'] = array( + '#type' => 'textfield', + '#title' => t('Provide breadcrumb'), + '#title_display' => 'invisible', + '#default_value' => $this->options['breadcrumb'], + '#description' => t('Enter a breadcrumb name you would like to use. See "Title" for percent substitutions.'), + '#states' => array( + 'visible' => array( + ':input[name="options[breadcrumb_enable]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'argument_present', + ); + + $form['specify_validation'] = array( + '#type' => 'checkbox', + '#title' => t('Specify validation criteria'), + '#default_value' => $this->options['specify_validation'], + '#fieldset' => 'argument_present', + ); + + $form['validate'] = array( + '#type' => 'container', + '#fieldset' => 'argument_present', + ); + $form['validate']['type'] = array( + '#type' => 'select', + '#title' => t('Validator'), + '#default_value' => $this->options['validate']['type'], + '#states' => array( + 'visible' => array( + ':input[name="options[specify_validation]"]' => array('checked' => TRUE), + ), + ), + ); + + $plugins = views_get_plugin_definitions('argument_validator'); + foreach ($plugins as $id => $info) { + if (!empty($info['no_ui'])) { + continue; + } + + $valid = TRUE; + if (!empty($info['type'])) { + $valid = FALSE; + if (empty($this->definition['validate type'])) { + continue; + } + foreach ((array) $info['type'] as $type) { + if ($type == $this->definition['validate type']) { + $valid = TRUE; + break; + } + } + } + + // If we decide this validator is ok, add it to the list. + if ($valid) { + $plugin = $this->get_plugin('argument_validator', $id); + if ($plugin) { + if ($plugin->access() || $this->options['validate']['type'] == $id) { + $form['validate']['options'][$id] = array( + '#prefix' => '<div id="edit-options-validate-options-' . $id . '-wrapper">', + '#suffix' => '</div>', + '#type' => 'item', + // Even if the plugin has no options add the key to the form_state. + '#input' => TRUE, // trick it into checking input to make #process run + '#states' => array( + 'visible' => array( + ':input[name="options[specify_validation]"]' => array('checked' => TRUE), + ':input[name="options[validate][type]"]' => array('value' => $id), + ), + ), + '#id' => 'edit-options-validate-options-' . $id, + ); + $plugin->buildOptionsForm($form['validate']['options'][$id], $form_state); + $validate_types[$id] = $info['title']; + } + } + } + } + + asort($validate_types); + $form['validate']['type']['#options'] = $validate_types; + + $form['validate']['fail'] = array( + '#type' => 'select', + '#title' => t('Action to take if filter value does not validate'), + '#default_value' => $this->options['validate']['fail'], + '#options' => $validate_options, + '#states' => array( + 'visible' => array( + ':input[name="options[specify_validation]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'argument_present', + ); + } + + public function validateOptionsForm(&$form, &$form_state) { + if (empty($form_state['values']['options'])) { + return; + } + + // Let the plugins do validation. + $default_id = $form_state['values']['options']['default_argument_type']; + $plugin = $this->get_plugin('argument_default', $default_id); + if ($plugin) { + $plugin->validateOptionsForm($form['argument_default'][$default_id], $form_state, $form_state['values']['options']['argument_default'][$default_id]); + } + + // summary plugin + $summary_id = $form_state['values']['options']['summary']['format']; + $plugin = $this->get_plugin('style', $summary_id); + if ($plugin) { + $plugin->validateOptionsForm($form['summary']['options'][$summary_id], $form_state, $form_state['values']['options']['summary']['options'][$summary_id]); + } + + $validate_id = $form_state['values']['options']['validate']['type']; + $plugin = $this->get_plugin('argument_validator', $validate_id); + if ($plugin) { + $plugin->validateOptionsForm($form['validate']['options'][$default_id], $form_state, $form_state['values']['options']['validate']['options'][$validate_id]); + } + + } + + public function submitOptionsForm(&$form, &$form_state) { + if (empty($form_state['values']['options'])) { + return; + } + + // Let the plugins make submit modifications if necessary. + $default_id = $form_state['values']['options']['default_argument_type']; + $plugin = $this->get_plugin('argument_default', $default_id); + if ($plugin) { + $options = &$form_state['values']['options']['argument_default'][$default_id]; + $plugin->submitOptionsForm($form['argument_default'][$default_id], $form_state, $options); + // Copy the now submitted options to their final resting place so they get saved. + $form_state['values']['options']['default_argument_options'] = $options; + } + + // summary plugin + $summary_id = $form_state['values']['options']['summary']['format']; + $plugin = $this->get_plugin('style', $summary_id); + if ($plugin) { + $options = &$form_state['values']['options']['summary']['options'][$summary_id]; + $plugin->submitOptionsForm($form['summary']['options'][$summary_id], $form_state, $options); + // Copy the now submitted options to their final resting place so they get saved. + $form_state['values']['options']['summary_options'] = $options; + } + + $validate_id = $form_state['values']['options']['validate']['type']; + $plugin = $this->get_plugin('argument_validator', $validate_id); + if ($plugin) { + $options = &$form_state['values']['options']['validate']['options'][$validate_id]; + $plugin->submitOptionsForm($form['validate']['options'][$validate_id], $form_state, $options); + // Copy the now submitted options to their final resting place so they get saved. + $form_state['values']['options']['validate_options'] = $options; + } + + // Clear out the content of title if it's not enabled. + $options =& $form_state['values']['options']; + if (empty($options['title_enable'])) { + $options['title'] = ''; + } + } + + /** + * Provide a list of default behaviors for this argument if the argument + * is not present. + * + * Override this method to provide additional (or fewer) default behaviors. + */ + function default_actions($which = NULL) { + $defaults = array( + 'ignore' => array( + 'title' => t('Display all results for the specified field'), + 'method' => 'default_ignore', + 'breadcrumb' => TRUE, // generate a breadcrumb to here + ), + 'default' => array( + 'title' => t('Provide default value'), + 'method' => 'default_default', + 'form method' => 'default_argument_form', + 'has default argument' => TRUE, + 'default only' => TRUE, // this can only be used for missing argument, not validation failure + 'breadcrumb' => TRUE, // generate a breadcrumb to here + ), + 'not found' => array( + 'title' => t('Hide view'), + 'method' => 'default_not_found', + 'hard fail' => TRUE, // This is a hard fail condition + ), + 'summary' => array( + 'title' => t('Display a summary'), + 'method' => 'default_summary', + 'form method' => 'default_summary_form', + 'style plugin' => TRUE, + 'breadcrumb' => TRUE, // generate a breadcrumb to here + ), + 'empty' => array( + 'title' => t('Display contents of "No results found"'), + 'method' => 'default_empty', + 'breadcrumb' => TRUE, // generate a breadcrumb to here + ), + 'access denied' => array( + 'title' => t('Display "Access Denied"'), + 'method' => 'default_access_denied', + 'breadcrumb' => FALSE, // generate a breadcrumb to here + ), + ); + + if ($this->view->display_handler->hasPath()) { + $defaults['not found']['title'] = t('Show "Page not found"'); + } + + if ($which) { + if (!empty($defaults[$which])) { + return $defaults[$which]; + } + } + else { + return $defaults; + } + } + + /** + * Provide a form for selecting the default argument when the + * default action is set to provide default argument. + */ + function default_argument_form(&$form, &$form_state) { + $plugins = views_get_plugin_definitions('argument_default'); + $options = array(); + + $form['default_argument_skip_url'] = array( + '#type' => 'checkbox', + '#title' => t('Skip default argument for view URL'), + '#default_value' => $this->options['default_argument_skip_url'], + '#description' => t('Select whether to include this default argument when constructing the URL for this view. Skipping default arguments is useful e.g. in the case of feeds.') + ); + + $form['default_argument_type'] = array( + '#prefix' => '<div id="edit-options-default-argument-type-wrapper">', + '#suffix' => '</div>', + '#type' => 'select', + '#id' => 'edit-options-default-argument-type', + '#title' => t('Type'), + '#default_value' => $this->options['default_argument_type'], + '#states' => array( + 'visible' => array( + ':input[name="options[default_action]"]' => array('value' => 'default'), + ), + ), + // Views custom key, moves this element to the appropriate container + // under the radio button. + '#argument_option' => 'default', + ); + + foreach ($plugins as $id => $info) { + if (!empty($info['no_ui'])) { + continue; + } + $plugin = $this->get_plugin('argument_default', $id); + if ($plugin) { + if ($plugin->access() || $this->options['default_argument_type'] == $id) { + $form['argument_default']['#argument_option'] = 'default'; + $form['argument_default'][$id] = array( + '#prefix' => '<div id="edit-options-argument-default-options-' . $id . '-wrapper">', + '#suffix' => '</div>', + '#id' => 'edit-options-argument-default-options-' . $id, + '#type' => 'item', + // Even if the plugin has no options add the key to the form_state. + '#input' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name="options[default_action]"]' => array('value' => 'default'), + ':input[name="options[default_argument_type]"]' => array('value' => $id), + ), + ), + ); + $options[$id] = $info['title']; + $plugin->buildOptionsForm($form['argument_default'][$id], $form_state); + } + } + } + + asort($options); + $form['default_argument_type']['#options'] = $options; + } + + /** + * Provide a form for selecting further summary options when the + * default action is set to display one. + */ + function default_summary_form(&$form, &$form_state) { + $style_plugins = views_get_plugin_definitions('style'); + $summary_plugins = array(); + $format_options = array(); + foreach ($style_plugins as $key => $plugin) { + if (isset($plugin['type']) && $plugin['type'] == 'summary') { + $summary_plugins[$key] = $plugin; + $format_options[$key] = $plugin['title']; + } + } + + $form['summary'] = array( + // Views custom key, moves this element to the appropriate container + // under the radio button. + '#argument_option' => 'summary', + ); + $form['summary']['sort_order'] = array( + '#type' => 'radios', + '#title' => t('Sort order'), + '#options' => array('asc' => t('Ascending'), 'desc' => t('Descending')), + '#default_value' => $this->options['summary']['sort_order'], + '#states' => array( + 'visible' => array( + ':input[name="options[default_action]"]' => array('value' => 'summary'), + ), + ), + ); + $form['summary']['number_of_records'] = array( + '#type' => 'radios', + '#title' => t('Sort by'), + '#default_value' => $this->options['summary']['number_of_records'], + '#options' => array( + 0 => $this->get_sort_name(), + 1 => t('Number of records') + ), + '#states' => array( + 'visible' => array( + ':input[name="options[default_action]"]' => array('value' => 'summary'), + ), + ), + ); + + $form['summary']['format'] = array( + '#type' => 'radios', + '#title' => t('Format'), + '#options' => $format_options, + '#default_value' => $this->options['summary']['format'], + '#states' => array( + 'visible' => array( + ':input[name="options[default_action]"]' => array('value' => 'summary'), + ), + ), + ); + + foreach ($summary_plugins as $id => $info) { + $plugin = $this->get_plugin('style', $id); + if (!$plugin->usesOptions()) { + continue; + } + if ($plugin) { + $form['summary']['options'][$id] = array( + '#prefix' => '<div id="edit-options-summary-options-' . $id . '-wrapper">', + '#suffix' => '</div>', + '#id' => 'edit-options-summary-options-' . $id, + '#type' => 'item', + '#input' => TRUE, // trick it into checking input to make #process run + '#states' => array( + 'visible' => array( + ':input[name="options[default_action]"]' => array('value' => 'summary'), + ':input[name="options[summary][format]"]' => array('value' => $id), + ), + ), + ); + $options[$id] = $info['title']; + $plugin->buildOptionsForm($form['summary']['options'][$id], $form_state); + } + } + } + + /** + * Handle the default action, which means our argument wasn't present. + * + * Override this method only with extreme care. + * + * @return + * A boolean value; if TRUE, continue building this view. If FALSE, + * building the view will be aborted here. + */ + function default_action($info = NULL) { + if (!isset($info)) { + $info = $this->default_actions($this->options['default_action']); + } + + if (!$info) { + return FALSE; + } + + if (!empty($info['method args'])) { + return call_user_func_array(array(&$this, $info['method']), $info['method args']); + } + else { + return $this->{$info['method']}(); + } + } + + /** + * How to act if validation failes + */ + public function validateFail() { + $info = $this->default_actions($this->options['validate']['fail']); + return $this->default_action($info); + } + /** + * Default action: ignore. + * + * If an argument was expected and was not given, in this case, simply + * ignore the argument entirely. + */ + function default_ignore() { + return TRUE; + } + + /** + * Default action: not found. + * + * If an argument was expected and was not given, in this case, report + * the view as 'not found' or hide it. + */ + function default_not_found() { + // Set a failure condition and let the display manager handle it. + $this->view->build_info['fail'] = TRUE; + return FALSE; + } + + /** + * Default action: access denied. + * + * If an argument was expected and was not given, in this case, report + * the view as 'access denied'. + */ + function default_access_denied() { + $this->view->build_info['denied'] = TRUE; + return FALSE; + } + + /** + * Default action: empty + * + * If an argument was expected and was not given, in this case, display + * the view's empty text + */ + function default_empty() { + // We return with no query; this will force the empty text. + $this->view->built = TRUE; + $this->view->executed = TRUE; + $this->view->result = array(); + return FALSE; + } + + /** + * This just returns true. The view argument builder will know where + * to find the argument from. + */ + function default_default() { + return TRUE; + } + + /** + * Determine if the argument is set to provide a default argument. + */ + function has_default_argument() { + $info = $this->default_actions($this->options['default_action']); + return !empty($info['has default argument']); + } + + /** + * Get a default argument, if available. + */ + function get_default_argument() { + $plugin = $this->get_plugin('argument_default'); + if ($plugin) { + return $plugin->get_argument(); + } + } + + /** + * Process the summary arguments for display. + * + * For example, the validation plugin may want to alter an argument for use in + * the URL. + */ + function process_summary_arguments(&$args) { + if ($this->options['validate']['type'] != 'none') { + if (isset($this->validator) || $this->validator = $this->get_plugin('argument_validator')) { + $this->validator->process_summary_arguments($args); + } + } + } + + /** + * Default action: summary. + * + * If an argument was expected and was not given, in this case, display + * a summary query. + */ + function default_summary() { + $this->view->build_info['summary'] = TRUE; + $this->view->build_info['summary_level'] = $this->options['id']; + + // Change the display style to the summary style for this + // argument. + $this->view->plugin_name = $this->options['summary']['format']; + $this->view->style_options = $this->options['summary_options']; + + // Clear out the normal primary field and whatever else may have + // been added and let the summary do the work. + $this->query->clear_fields(); + $this->summary_query(); + + $by = $this->options['summary']['number_of_records'] ? 'num_records' : NULL; + $this->summary_sort($this->options['summary']['sort_order'], $by); + + // Summaries have their own sorting and fields, so tell the View not + // to build these. + $this->view->build_sort = FALSE; + return TRUE; + } + + /** + * Build the info for the summary query. + * + * This must: + * - add_groupby: group on this field in order to create summaries. + * - add_field: add a 'num_nodes' field for the count. Usually it will + * be a count on $view->base_field + * - set_count_field: Reset the count field so we get the right paging. + * + * @return + * The alias used to get the number of records (count) for this entry. + */ + function summary_query() { + $this->ensureMyTable(); + // Add the field. + $this->base_alias = $this->query->add_field($this->tableAlias, $this->realField); + + $this->summary_name_field(); + return $this->summary_basics(); + } + + /** + * Add the name field, which is the field displayed in summary queries. + * This is often used when the argument is numeric. + */ + function summary_name_field() { + // Add the 'name' field. For example, if this is a uid argument, the + // name field would be 'name' (i.e, the username). + + if (isset($this->name_table)) { + // if the alias is different then we're probably added, not ensured, + // so look up the join and add it instead. + if ($this->tableAlias != $this->name_table) { + $j = HandlerBase::getTableJoin($this->name_table, $this->table); + if ($j) { + $join = clone $j; + $join->leftTable = $this->tableAlias; + $this->name_table_alias = $this->query->add_table($this->name_table, $this->relationship, $join); + } + } + else { + $this->name_table_alias = $this->query->ensure_table($this->name_table, $this->relationship); + } + } + else { + $this->name_table_alias = $this->tableAlias; + } + + if (isset($this->name_field)) { + $this->name_alias = $this->query->add_field($this->name_table_alias, $this->name_field); + } + else { + $this->name_alias = $this->base_alias; + } + } + + /** + * Some basic summary behavior that doesn't need to be repeated as much as + * code that goes into summary_query() + */ + function summary_basics($count_field = TRUE) { + // Add the number of nodes counter + $distinct = ($this->view->display_handler->getOption('distinct') && empty($this->query->no_distinct)); + + $count_alias = $this->query->add_field($this->query->base_table, $this->query->base_field, 'num_records', array('count' => TRUE, 'distinct' => $distinct)); + $this->query->add_groupby($this->name_alias); + + if ($count_field) { + $this->query->set_count_field($this->tableAlias, $this->realField); + } + + $this->count_alias = $count_alias; + } + + /** + * Sorts the summary based upon the user's selection. The base variant of + * this is usually adequte. + * + * @param $order + * The order selected in the UI. + */ + function summary_sort($order, $by = NULL) { + $this->query->add_orderby(NULL, NULL, $order, (!empty($by) ? $by : $this->name_alias)); + } + + /** + * Provide the argument to use to link from the summary to the next level; + * this will be called once per row of a summary, and used as part of + * $view->getUrl(). + * + * @param $data + * The query results for the row. + */ + function summary_argument($data) { + return $data->{$this->base_alias}; + } + + /** + * Provides the name to use for the summary. By default this is just + * the name field. + * + * @param $data + * The query results for the row. + */ + function summary_name($data) { + $value = $data->{$this->name_alias}; + if (empty($value) && !empty($this->definition['empty field name'])) { + $value = $this->definition['empty field name']; + } + return check_plain($value); + } + + /** + * Set up the query for this argument. + * + * The argument sent may be found at $this->argument. + */ + public function query($group_by = FALSE) { + $this->ensureMyTable(); + $this->query->add_where(0, "$this->tableAlias.$this->realField", $this->argument); + } + + /** + * Get the title this argument will assign the view, given the argument. + * + * This usually needs to be overridden to provide a proper title. + */ + function title() { + return check_plain($this->argument); + } + + /** + * Called by the view object to get the title. This may be set by a + * validator so we don't necessarily call through to title(). + */ + function get_title() { + if (isset($this->validated_title)) { + return $this->validated_title; + } + else { + return $this->title(); + } + } + + /** + * Validate that this argument works. By default, all arguments are valid. + */ + public function validateArgument($arg) { + // By using % in URLs, arguments could be validated twice; this eases + // that pain. + if (isset($this->argument_validated)) { + return $this->argument_validated; + } + + if ($this->is_exception($arg)) { + return $this->argument_validated = TRUE; + } + + $plugin = $this->get_plugin('argument_validator'); + return $this->argument_validated = $plugin->validate_argument($arg); + } + + /** + * Called by the menu system to validate an argument. + * + * This checks to see if this is a 'soft fail', which means that if the + * argument fails to validate, but there is an action to take anyway, + * then validation cannot actually fail. + */ + function validate_argument($arg) { + $validate_info = $this->default_actions($this->options['validate']['fail']); + if (empty($validate_info['hard fail'])) { + return TRUE; + } + + $rc = $this->validateArgument($arg); + + // If the validator has changed the validate fail condition to a + // soft fail, deal with that: + $validate_info = $this->default_actions($this->options['validate']['fail']); + if (empty($validate_info['hard fail'])) { + return TRUE; + } + + return $rc; + } + + /** + * Set the input for this argument + * + * @return TRUE if it successfully validates; FALSE if it does not. + */ + function set_argument($arg) { + $this->argument = $arg; + return $this->validateArgument($arg); + } + + /** + * Get the value of this argument. + */ + function get_value() { + // If we already processed this argument, we're done. + if (isset($this->argument)) { + return $this->argument; + } + + // Otherwise, we have to pretend to process ourself to find the value. + $value = NULL; + // Find the position of this argument within the view. + $position = 0; + foreach ($this->view->argument as $id => $argument) { + if ($id == $this->options['id']) { + break; + } + $position++; + } + + $arg = isset($this->view->args[$position]) ? $this->view->args[$position] : NULL; + $this->position = $position; + + // Clone ourselves so that we don't break things when we're really + // processing the arguments. + $argument = clone $this; + if (!isset($arg) && $argument->has_default_argument()) { + $arg = $argument->get_default_argument(); + + // remember that this argument was computed, not passed on the URL. + $this->is_default = TRUE; + } + // Set the argument, which will also validate that the argument can be set. + if ($argument->set_argument($arg)) { + $value = $argument->argument; + } + unset($argument); + return $value; + } + + /** + * Get the display or row plugin, if it exists. + */ + function get_plugin($type = 'argument_default', $name = NULL) { + $options = array(); + switch ($type) { + case 'argument_default': + $plugin_name = $this->options['default_argument_type']; + $options_name = 'default_argument_options'; + break; + case 'argument_validator': + $plugin_name = $this->options['validate']['type']; + $options_name = 'validate_options'; + break; + case 'style': + $plugin_name = $this->options['summary']['format']; + $options_name = 'summary_options'; + } + + if (!$name) { + $name = $plugin_name; + } + + // we only fetch the options if we're fetching the plugin actually + // in use. + if ($name == $plugin_name) { + $options = $this->options[$options_name]; + } + + $plugin = views_get_plugin($type, $name); + if ($plugin) { + // Style plugins expects different parameters as argument related plugins. + if ($type == 'style') { + $plugin->init($this->view, $this->view->display_handler->display, $options); + } + else { + $plugin->init($this->view, $this, $options); + } + return $plugin; + } + } + + /** + * Return a description of how the argument would normally be sorted. + * + * Subclasses should override this to specify what the default sort order of + * their argument is (e.g. alphabetical, numeric, date). + */ + function get_sort_name() { + return t('Default sort', array(), array('context' => 'Sort order')); + } + + /** + * Custom form radios process function. + * + * Roll out a single radios element to a list of radios, using the options + * array as index. While doing that, create a container element underneath + * each option, which contains the settings related to that option. + * + * @see form_process_radios() + */ + public static function processContainerRadios($element) { + if (count($element['#options']) > 0) { + foreach ($element['#options'] as $key => $choice) { + $element += array($key => array()); + // Generate the parents as the autogenerator does, so we will have a + // unique id for each radio button. + $parents_for_id = array_merge($element['#parents'], array($key)); + + $element[$key] += array( + '#type' => 'radio', + '#title' => $choice, + // The key is sanitized in drupal_attributes() during output from the + // theme function. + '#return_value' => $key, + '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : NULL, + '#attributes' => $element['#attributes'], + '#parents' => $element['#parents'], + '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + ); + $element[$key . '_options'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('views-admin-dependent')), + ); + } + } + return $element; + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/Broken.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Broken.php new file mode 100644 index 0000000..d08cda3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Broken.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\Broken. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Annotation\Plugin; + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "broken" + * ) + */ +class Broken extends ArgumentPluginBase { + + public function adminLabel($short = FALSE) { + return t('Broken/missing handler'); + } + + public function defineOptions() { return array(); } + public function ensureMyTable() { /* No table to ensure! */ } + public function query($group_by = FALSE) { /* No query to run */ } + public function buildOptionsForm(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '<div class="form-item description">' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '</div>', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + public function broken() { return TRUE; } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/Date.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Date.php new file mode 100644 index 0000000..ce3f52f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Date.php @@ -0,0 +1,145 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\Date. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Annotation\Plugin; + +/** + * Abstract argument handler for dates. + * + * Adds an option to set a default argument based on the current date. + * + * @param $arg_format + * The format string to use on the current time when + * creating a default date argument. + * + * Definitions terms: + * - many to one: If true, the "many to one" helper will be used. + * - invalid input: A string to give to the user for obviously invalid input. + * This is deprecated in favor of argument validators. + * + * @see Drupal\views\ManyTonOneHelper + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "date" + * ) + */ +class Date extends Formula { + + var $option_name = 'default_argument_date'; + var $arg_format = 'Y-m-d'; + + /** + * Add an option to set the default value to the current date. + */ + function default_argument_form(&$form, &$form_state) { + parent::default_argument_form($form, $form_state); + $form['default_argument_type']['#options'] += array('date' => t('Current date')); + $form['default_argument_type']['#options'] += array('node_created' => t("Current node's creation time")); + $form['default_argument_type']['#options'] += array('node_changed' => t("Current node's update time")); } + + /** + * Set the empty argument value to the current date, + * formatted appropriately for this argument. + */ + function get_default_argument($raw = FALSE) { + if (!$raw && $this->options['default_argument_type'] == 'date') { + return date($this->definition['format'], REQUEST_TIME); + } + elseif (!$raw && in_array($this->options['default_argument_type'], array('node_created', 'node_changed'))) { + foreach (range(1, 3) as $i) { + $node = menu_get_object('node', $i); + if (!empty($node)) { + continue; + } + } + + if (arg(0) == 'node' && is_numeric(arg(1))) { + $node = node_load(arg(1)); + } + + if (empty($node)) { + return parent::get_default_argument(); + } + elseif ($this->options['default_argument_type'] == 'node_created') { + return date($this->definition['format'], $node->created); + } + elseif ($this->options['default_argument_type'] == 'node_changed') { + return date($this->definition['format'], $node->changed); + } + } + + return parent::get_default_argument($raw); + } + + function get_sort_name() { + return t('Date', array(), array('context' => 'Sort order')); + } + + /** + * Creates cross-database SQL date extraction. + * + * @param string $extract_type + * The type of value to extract from the date, like 'MONTH'. + * + * @return string + * An appropriate SQL string for the DB type and field type. + */ + public function extractSQL($extract_type) { + $db_type = Database::getConnection()->databaseType(); + $field = $this->getSQLDateField(); + + // Note there is no space after FROM to avoid db_rewrite problems + // see http://drupal.org/node/79904. + switch ($extract_type) { + case 'DATE': + return $field; + case 'YEAR': + return "EXTRACT(YEAR FROM($field))"; + case 'MONTH': + return "EXTRACT(MONTH FROM($field))"; + case 'DAY': + return "EXTRACT(DAY FROM($field))"; + case 'HOUR': + return "EXTRACT(HOUR FROM($field))"; + case 'MINUTE': + return "EXTRACT(MINUTE FROM($field))"; + case 'SECOND': + return "EXTRACT(SECOND FROM($field))"; + // ISO week number for date + case 'WEEK': + switch ($db_type) { + case 'mysql': + // WEEK using arg 3 in mysql should return the same value as postgres + // EXTRACT. + return "WEEK($field, 3)"; + case 'pgsql': + return "EXTRACT(WEEK FROM($field))"; + } + case 'DOW': + switch ($db_type) { + case 'mysql': + // mysql returns 1 for Sunday through 7 for Saturday php date + // functions and postgres use 0 for Sunday and 6 for Saturday. + return "INTEGER(DAYOFWEEK($field) - 1)"; + case 'pgsql': + return "EXTRACT(DOW FROM($field))"; + } + case 'DOY': + switch ($db_type) { + case 'mysql': + return "DAYOFYEAR($field)"; + case 'pgsql': + return "EXTRACT(DOY FROM($field))"; + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/Formula.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Formula.php new file mode 100644 index 0000000..4c0865f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Formula.php @@ -0,0 +1,75 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\Formula. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Annotation\Plugin; +use Drupal\views\ViewExecutable; + +/** + * Abstract argument handler for simple formulae. + * + * Child classes of this object should implement summary_argument, at least. + * + * Definition terms: + * - formula: The formula to use for this handler. + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "formula" + * ) + */ +class Formula extends ArgumentPluginBase { + + var $formula = NULL; + + /** + * Overrides Drupal\views\Plugin\views\argument\ArgumentPluginBase::init(). + */ + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + if (!empty($this->definition['formula'])) { + $this->formula = $this->definition['formula']; + } + } + + function get_formula() { + return str_replace('***table***', $this->tableAlias, $this->formula); + } + + /** + * Build the summary query based on a formula + */ + function summary_query() { + $this->ensureMyTable(); + // Now that our table is secure, get our formula. + $formula = $this->get_formula(); + + // Add the field. + $this->base_alias = $this->name_alias = $this->query->add_field(NULL, $formula, $this->field); + $this->query->set_count_field(NULL, $formula, $this->field); + + return $this->summary_basics(FALSE); + } + + /** + * Build the query based upon the formula + */ + public function query($group_by = FALSE) { + $this->ensureMyTable(); + // Now that our table is secure, get our formula. + $placeholder = $this->placeholder(); + $formula = $this->get_formula() .' = ' . $placeholder; + $placeholders = array( + $placeholder => $this->argument, + ); + $this->query->add_where(0, $formula, $placeholders, 'formula'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/GroupByNumeric.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/GroupByNumeric.php new file mode 100644 index 0000000..16976d3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/GroupByNumeric.php @@ -0,0 +1,39 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\GroupByNumeric. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Annotation\Plugin; + +/** + * Simple handler for arguments using group by. + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "groupby_numeric" + * ) + */ +class GroupByNumeric extends ArgumentPluginBase { + + public function query($group_by = FALSE) { + $this->ensureMyTable(); + $field = $this->getField(); + $placeholder = $this->placeholder(); + + $this->query->add_having_expression(0, "$field = $placeholder", array($placeholder => $this->argument)); + } + + public function adminLabel($short = FALSE) { + return $this->getField(parent::adminLabel($short)); + } + + function get_sort_name() { + return t('Numerical', array(), array('context' => 'Sort order')); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/ManyToOne.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/ManyToOne.php new file mode 100644 index 0000000..b0807ad --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/ManyToOne.php @@ -0,0 +1,200 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\ManyToOne. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\views\ViewExecutable; +use Drupal\Core\Annotation\Plugin; +use Drupal\views\ManyToOneHelper; + +/** + * An argument handler for use in fields that have a many to one relationship + * with the table(s) to the left. This adds a bunch of options that are + * reasonably common with this type of relationship. + * Definition terms: + * - numeric: If true, the field will be considered numeric. Probably should + * always be set TRUE as views_handler_argument_string has many to one + * capabilities. + * - zero is null: If true, a 0 will be handled as empty, so for example + * a default argument can be provided or a summary can be shown. + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "many_to_one" + * ) + */ +class ManyToOne extends ArgumentPluginBase { + + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + $this->helper = new ManyToOneHelper($this); + + // Ensure defaults for these, during summaries and stuff: + $this->operator = 'or'; + $this->value = array(); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + if (!empty($this->definition['numeric'])) { + $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE); + } + + $options['add_table'] = array('default' => FALSE, 'bool' => TRUE); + $options['require_value'] = array('default' => FALSE, 'bool' => TRUE); + + if (isset($this->helper)) { + $this->helper->defineOptions($options); + } + else { + $helper = new ManyToOneHelper($this); + $helper->defineOptions($options); + } + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + // allow + for or, , for and + $form['break_phrase'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple values'), + '#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'), + '#default_value' => !empty($this->options['break_phrase']), + '#fieldset' => 'more', + ); + + $form['add_table'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple filter values to work together'), + '#description' => t('If selected, multiple instances of this filter can work together, as though multiple values were supplied to the same filter. This setting is not compatible with the "Reduce duplicates" setting.'), + '#default_value' => !empty($this->options['add_table']), + '#fieldset' => 'more', + ); + + $form['require_value'] = array( + '#type' => 'checkbox', + '#title' => t('Do not display items with no value in summary'), + '#default_value' => !empty($this->options['require_value']), + '#fieldset' => 'more', + ); + + $this->helper->buildOptionsForm($form, $form_state); + } + + /** + * Override ensureMyTable so we can control how this joins in. + * The operator actually has influence over joining. + */ + public function ensureMyTable() { + $this->helper->ensureMyTable(); + } + + public function query($group_by = FALSE) { + $empty = FALSE; + if (isset($this->definition['zero is null']) && $this->definition['zero is null']) { + if (empty($this->argument)) { + $empty = TRUE; + } + } + else { + if (!isset($this->argument)) { + $empty = TRUE; + } + } + if ($empty) { + parent::ensureMyTable(); + $this->query->add_where(0, "$this->tableAlias.$this->realField", NULL, 'IS NULL'); + return; + } + + if (!empty($this->options['break_phrase'])) { + if (!empty($this->definition['numeric'])) { + $this->breakPhrase($this->argument, $this); + } + else { + $this->breakPhraseString($this->argument, $this); + } + } + else { + $this->value = array($this->argument); + $this->operator = 'or'; + } + + $this->helper->add_filter(); + } + + function title() { + if (!$this->argument) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if (!empty($this->options['break_phrase'])) { + $this->breakPhrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + $this->operator = 'or'; + } + + // @todo -- both of these should check definition for alternate keywords. + + if (empty($this->value)) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if ($this->value === array(-1)) { + return !empty($this->definition['invalid input']) ? $this->definition['invalid input'] : t('Invalid input'); + } + + return implode($this->operator == 'or' ? ' + ' : ', ', $this->title_query()); + } + + function summary_query() { + $field = $this->table . '.' . $this->field; + $join = $this->getJoin(); + + if (!empty($this->options['require_value'])) { + $join->type = 'INNER'; + } + + if (empty($this->options['add_table']) || empty($this->view->many_to_one_tables[$field])) { + $this->tableAlias = $this->query->ensure_table($this->table, $this->relationship, $join); + } + else { + $this->tableAlias = $this->helper->summary_join(); + } + + // Add the field. + $this->base_alias = $this->query->add_field($this->tableAlias, $this->realField); + + $this->summary_name_field(); + + return $this->summary_basics(); + } + + function summary_argument($data) { + $value = $data->{$this->base_alias}; + if (empty($value)) { + $value = 0; + } + + return $value; + } + + /** + * Override for specific title lookups. + */ + function title_query() { + return $this->value; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/Null.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Null.php new file mode 100644 index 0000000..6097b00 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Null.php @@ -0,0 +1,69 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\Null. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Annotation\Plugin; + +/** + * Argument handler that ignores the argument. + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "null" + * ) + */ +class Null extends ArgumentPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['must_not_be'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Override buildOptionsForm() so that only the relevant options + * are displayed to the user. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['must_not_be'] = array( + '#type' => 'checkbox', + '#title' => t('Fail basic validation if any argument is given'), + '#default_value' => !empty($this->options['must_not_be']), + '#description' => t('By checking this field, you can use this to make sure views with more arguments than necessary fail validation.'), + '#fieldset' => 'more', + ); + + unset($form['exception']); + } + + /** + * Override default_actions() to remove actions that don't + * make sense for a null argument. + */ + function default_actions($which = NULL) { + if ($which) { + if (in_array($which, array('ignore', 'not found', 'empty', 'default'))) { + return parent::default_actions($which); + } + return; + } + $actions = parent::default_actions(); + unset($actions['summary asc']); + unset($actions['summary desc']); + return $actions; + } + + /** + * Override the behavior of query() to prevent the query + * from being changed in any way. + */ + public function query($group_by = FALSE) {} + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/Numeric.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Numeric.php new file mode 100644 index 0000000..3972b4d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Numeric.php @@ -0,0 +1,126 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\Numeric. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Annotation\Plugin; + +/** + * Basic argument handler for arguments that are numeric. Incorporates + * break_phrase. + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "numeric" + * ) + */ +class Numeric extends ArgumentPluginBase { + + /** + * The operator used for the query: or|and. + * @var string + */ + var $operator; + + /** + * The actual value which is used for querying. + * @var array + */ + var $value; + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE); + $options['not'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + // allow + for or, , for and + $form['break_phrase'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple values'), + '#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'), + '#default_value' => !empty($this->options['break_phrase']), + '#fieldset' => 'more', + ); + + $form['not'] = array( + '#type' => 'checkbox', + '#title' => t('Exclude'), + '#description' => t('If selected, the numbers entered for the filter will be excluded rather than limiting the view.'), + '#default_value' => !empty($this->options['not']), + '#fieldset' => 'more', + ); + } + + function title() { + if (!$this->argument) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if (!empty($this->options['break_phrase'])) { + $this->breakPhrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + $this->operator = 'or'; + } + + if (empty($this->value)) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if ($this->value === array(-1)) { + return !empty($this->definition['invalid input']) ? $this->definition['invalid input'] : t('Invalid input'); + } + + return implode($this->operator == 'or' ? ' + ' : ', ', $this->title_query()); + } + + /** + * Override for specific title lookups. + * @return array + * Returns all titles, if it's just one title it's an array with one entry. + */ + function title_query() { + return $this->value; + } + + public function query($group_by = FALSE) { + $this->ensureMyTable(); + + if (!empty($this->options['break_phrase'])) { + $this->breakPhrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + } + + $placeholder = $this->placeholder(); + $null_check = empty($this->options['not']) ? '' : "OR $this->tableAlias.$this->realField IS NULL"; + + if (count($this->value) > 1) { + $operator = empty($this->options['not']) ? 'IN' : 'NOT IN'; + $this->query->add_where_expression(0, "$this->tableAlias.$this->realField $operator($placeholder) $null_check", array($placeholder => $this->value)); + } + else { + $operator = empty($this->options['not']) ? '=' : '!='; + $this->query->add_where_expression(0, "$this->tableAlias.$this->realField $operator $placeholder $null_check", array($placeholder => $this->argument)); + } + } + + function get_sort_name() { + return t('Numerical', array(), array('context' => 'Sort order')); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/Standard.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Standard.php new file mode 100644 index 0000000..d7dafb3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/Standard.php @@ -0,0 +1,23 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\Standard. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Annotation\Plugin; + +/** + * Default implementation of the base argument plugin. + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "standard" + * ) + */ +class Standard extends ArgumentPluginBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument/String.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument/String.php new file mode 100644 index 0000000..59148de --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument/String.php @@ -0,0 +1,289 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument\String. + */ + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\views\ViewExecutable; +use Drupal\views\ManyToOneHelper; +use Drupal\Core\Annotation\Plugin; + +/** + * Basic argument handler to implement string arguments that may have length + * limits. + * + * @ingroup views_argument_handlers + * + * @Plugin( + * id = "string" + * ) + */ +class String extends ArgumentPluginBase { + + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + if (!empty($this->definition['many to one'])) { + $this->helper = new ManyToOneHelper($this); + + // Ensure defaults for these, during summaries and stuff: + $this->operator = 'or'; + $this->value = array(); + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['glossary'] = array('default' => FALSE, 'bool' => TRUE); + $options['limit'] = array('default' => 0); + $options['case'] = array('default' => 'none'); + $options['path_case'] = array('default' => 'none'); + $options['transform_dash'] = array('default' => FALSE, 'bool' => TRUE); + $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE); + + if (!empty($this->definition['many to one'])) { + $options['add_table'] = array('default' => FALSE, 'bool' => TRUE); + $options['require_value'] = array('default' => FALSE, 'bool' => TRUE); + } + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['glossary'] = array( + '#type' => 'checkbox', + '#title' => t('Glossary mode'), + '#description' => t('Glossary mode applies a limit to the number of characters used in the filter value, which allows the summary view to act as a glossary.'), + '#default_value' => $this->options['glossary'], + '#fieldset' => 'more', + ); + + $form['limit'] = array( + '#type' => 'textfield', + '#title' => t('Character limit'), + '#description' => t('How many characters of the filter value to filter against. If set to 1, all fields starting with the first letter in the filter value would be matched.'), + '#default_value' => $this->options['limit'], + '#states' => array( + 'visible' => array( + ':input[name="options[glossary]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'more', + ); + + $form['case'] = array( + '#type' => 'select', + '#title' => t('Case'), + '#description' => t('When printing the title and summary, how to transform the case of the filter value.'), + '#options' => array( + 'none' => t('No transform'), + 'upper' => t('Upper case'), + 'lower' => t('Lower case'), + 'ucfirst' => t('Capitalize first letter'), + 'ucwords' => t('Capitalize each word'), + ), + '#default_value' => $this->options['case'], + '#fieldset' => 'more', + ); + + $form['path_case'] = array( + '#type' => 'select', + '#title' => t('Case in path'), + '#description' => t('When printing url paths, how to transform the case of the filter value. Do not use this unless with Postgres as it uses case sensitive comparisons.'), + '#options' => array( + 'none' => t('No transform'), + 'upper' => t('Upper case'), + 'lower' => t('Lower case'), + 'ucfirst' => t('Capitalize first letter'), + 'ucwords' => t('Capitalize each word'), + ), + '#default_value' => $this->options['path_case'], + '#fieldset' => 'more', + ); + + $form['transform_dash'] = array( + '#type' => 'checkbox', + '#title' => t('Transform spaces to dashes in URL'), + '#default_value' => $this->options['transform_dash'], + '#fieldset' => 'more', + ); + + if (!empty($this->definition['many to one'])) { + $form['add_table'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple filter values to work together'), + '#description' => t('If selected, multiple instances of this filter can work together, as though multiple values were supplied to the same filter. This setting is not compatible with the "Reduce duplicates" setting.'), + '#default_value' => !empty($this->options['add_table']), + '#fieldset' => 'more', + ); + + $form['require_value'] = array( + '#type' => 'checkbox', + '#title' => t('Do not display items with no value in summary'), + '#default_value' => !empty($this->options['require_value']), + '#fieldset' => 'more', + ); + } + + // allow + for or, , for and + $form['break_phrase'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple values'), + '#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'), + '#default_value' => !empty($this->options['break_phrase']), + '#fieldset' => 'more', + ); + } + + /** + * Build the summary query based on a string + */ + function summary_query() { + if (empty($this->definition['many to one'])) { + $this->ensureMyTable(); + } + else { + $this->tableAlias = $this->helper->summary_join(); + } + + if (empty($this->options['glossary'])) { + // Add the field. + $this->base_alias = $this->query->add_field($this->tableAlias, $this->realField); + $this->query->set_count_field($this->tableAlias, $this->realField); + } + else { + // Add the field. + $formula = $this->get_formula(); + $this->base_alias = $this->query->add_field(NULL, $formula, $this->field . '_truncated'); + $this->query->set_count_field(NULL, $formula, $this->field, $this->field . '_truncated'); + } + + $this->summary_name_field(); + return $this->summary_basics(FALSE); + } + + /** + * Get the formula for this argument. + * + * $this->ensureMyTable() MUST have been called prior to this. + */ + function get_formula() { + return "SUBSTRING($this->tableAlias.$this->realField, 1, " . intval($this->options['limit']) . ")"; + } + + /** + * Build the query based upon the formula + */ + public function query($group_by = FALSE) { + $argument = $this->argument; + if (!empty($this->options['transform_dash'])) { + $argument = strtr($argument, '-', ' '); + } + + if (!empty($this->options['break_phrase'])) { + $this->breakPhraseString($argument, $this); + } + else { + $this->value = array($argument); + $this->operator = 'or'; + } + + if (!empty($this->definition['many to one'])) { + if (!empty($this->options['glossary'])) { + $this->helper->formula = TRUE; + } + $this->helper->ensureMyTable(); + $this->helper->add_filter(); + return; + } + + $this->ensureMyTable(); + $formula = FALSE; + if (empty($this->options['glossary'])) { + $field = "$this->tableAlias.$this->realField"; + } + else { + $formula = TRUE; + $field = $this->get_formula(); + } + + if (count($this->value) > 1) { + $operator = 'IN'; + $argument = $this->value; + } + else { + $operator = '='; + } + + if ($formula) { + $placeholder = $this->placeholder(); + if ($operator == 'IN') { + $field .= " IN($placeholder)"; + } + else { + $field .= ' = ' . $placeholder; + } + $placeholders = array( + $placeholder => $argument, + ); + $this->query->add_where_expression(0, $field, $placeholders); + } + else { + $this->query->add_where(0, $field, $argument, $operator); + } + } + + function summary_argument($data) { + $value = $this->caseTransform($data->{$this->base_alias}, $this->options['path_case']); + if (!empty($this->options['transform_dash'])) { + $value = strtr($value, ' ', '-'); + } + return $value; + } + + function get_sort_name() { + return t('Alphabetical', array(), array('context' => 'Sort order')); + } + + function title() { + $this->argument = $this->caseTransform($this->argument, $this->options['case']); + if (!empty($this->options['transform_dash'])) { + $this->argument = strtr($this->argument, '-', ' '); + } + + if (!empty($this->options['break_phrase'])) { + $this->breakPhraseString($this->argument, $this); + } + else { + $this->value = array($this->argument); + $this->operator = 'or'; + } + + if (empty($this->value)) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if ($this->value === array(-1)) { + return !empty($this->definition['invalid input']) ? $this->definition['invalid input'] : t('Invalid input'); + } + + return implode($this->operator == 'or' ? ' + ' : ', ', $this->title_query()); + } + + /** + * Override for specific title lookups. + */ + function title_query() { + return drupal_map_assoc($this->value, 'check_plain'); + } + + function summary_name($data) { + return $this->caseTransform(parent::summary_name($data), $this->options['case']); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/ArgumentDefaultPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/ArgumentDefaultPluginBase.php new file mode 100644 index 0000000..5465f0e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/ArgumentDefaultPluginBase.php @@ -0,0 +1,91 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase. + */ + +namespace Drupal\views\Plugin\views\argument_default; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\PluginBase; + +/** + * @defgroup views_argument_default_plugins Views argument default plugins + * @{ + * Allow specialized methods of filling in arguments when they aren't provided. + * + * @see hook_views_plugins() + */ + +/** + * The fixed argument default handler; also used as the base. + */ +abstract class ArgumentDefaultPluginBase extends PluginBase { + + /** + * Return the default argument. + * + * This needs to be overridden by every default argument handler to properly do what is needed. + */ + function get_argument() { } + + /** + * Initialize this plugin with the view and the argument + * it is linked to. + */ + public function init(ViewExecutable $view, &$argument, $options) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->argument = &$argument; + + $this->unpackOptions($this->options, $options); + } + + /** + * Retrieve the options when this is a new access + * control plugin + */ + protected function defineOptions() { return array(); } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { } + + /** + * Provide the default form form for validating options + */ + public function validateOptionsForm(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + public function submitOptionsForm(&$form, &$form_state, &$options = array()) { } + + /** + * Determine if the administrator has the privileges to use this + * plugin + */ + public function access() { return TRUE; } + + /** + * If we don't have access to the form but are showing it anyway, ensure that + * the form is safe and cannot be changed from user input. + * + * This is only called by child objects if specified in the buildOptionsForm(), + * so it will not always be used. + */ + function check_access(&$form, $option_name) { + if (!$this->access()) { + $form[$option_name]['#disabled'] = TRUE; + $form[$option_name]['#value'] = $form[$this->option_name]['#default_value']; + $form[$option_name]['#description'] .= ' <strong>' . t('Note: you do not have permission to modify this. If you change the default filter type, this setting will be lost and you will NOT be able to get it back.') . '</strong>'; + } + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Fixed.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Fixed.php new file mode 100644 index 0000000..977a01c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Fixed.php @@ -0,0 +1,48 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument_default\Fixed. + */ + +namespace Drupal\views\Plugin\views\argument_default; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The fixed argument default handler. + * + * @ingroup views_argument_default_plugins + * + * @Plugin( + * id = "fixed", + * title = @Translation("Fixed") + * ) + */ +class Fixed extends ArgumentDefaultPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['argument'] = array('default' => ''); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['argument'] = array( + '#type' => 'textfield', + '#title' => t('Fixed value'), + '#default_value' => $this->options['argument'], + ); + } + + /** + * Return the default argument. + */ + function get_argument() { + return $this->options['argument']; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Php.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Php.php new file mode 100644 index 0000000..7dc3a56 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Php.php @@ -0,0 +1,63 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument_default\Php. + */ + +namespace Drupal\views\Plugin\views\argument_default; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Default argument plugin to provide a PHP code block. + * + * @ingroup views_argument_default_plugins + * + * @Plugin( + * id = "php", + * title = @Translation("PHP Code") + * ) + */ +class Php extends ArgumentDefaultPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['code'] = array('default' => ''); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['code'] = array( + '#type' => 'textarea', + '#title' => t('PHP contextual filter code'), + '#default_value' => $this->options['code'], + '#description' => t('Enter PHP code that returns a value to use for this filter. Do not use <?php ?>. You must return only a single value for just this filter. Some variables are available: the view object will be "$view". The argument handler will be "$argument", for example you may change the title used for substitutions for this argument by setting "argument->validated_title"".'), + ); + + // Only do this if using one simple standard form gadget + $this->check_access($form, 'code'); + } + + /** + * Only let users with PHP block visibility permissions set/modify this + * default plugin. + */ + public function access() { + return user_access('use PHP for settings'); + } + + function get_argument() { + // set up variables to make it easier to reference during the argument. + $view = &$this->view; + $argument = &$this->argument; + ob_start(); + $result = eval($this->options['code']); + ob_end_clean(); + return $result; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Raw.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Raw.php new file mode 100644 index 0000000..636e3f3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_default/Raw.php @@ -0,0 +1,62 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument_default\Raw. + */ + +namespace Drupal\views\Plugin\views\argument_default; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Default argument plugin to use the raw value from the URL. + * + * @ingroup views_argument_default_plugins + * + * @Plugin( + * id = "raw", + * title = @Translation("Raw value from URL") + * ) + */ +class Raw extends ArgumentDefaultPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['index'] = array('default' => ''); + $options['use_alias'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + // Using range(1, 10) will create an array keyed 0-9, which allows arg() to + // properly function since it is also zero-based. + $form['index'] = array( + '#type' => 'select', + '#title' => t('Path component'), + '#default_value' => $this->options['index'], + '#options' => range(1, 10), + '#description' => t('The numbering starts from 1, e.g. on the page admin/structure/types, the 3rd path component is "types".'), + ); + $form['use_alias'] = array( + '#type' => 'checkbox', + '#title' => t('Use path alias'), + '#default_value' => $this->options['use_alias'], + '#description' => t('Use path alias instead of internal path.'), + ); + } + + function get_argument() { + $path = NULL; + if ($this->options['use_alias']) { + $path = drupal_get_path_alias(); + } + if ($arg = arg($this->options['index'], $path)) { + return $arg; + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/ArgumentValidatorPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/ArgumentValidatorPluginBase.php new file mode 100644 index 0000000..c14be5f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/ArgumentValidatorPluginBase.php @@ -0,0 +1,95 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument_validator\ArgumentValidatorPluginBase. + */ + +namespace Drupal\views\Plugin\views\argument_validator; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\PluginBase; + +/** + * @defgroup views_argument_validate_plugins Views argument validate plugins + * @{ + * Allow specialized methods of validating arguments. + * + * @see hook_views_plugins() + */ + +/** + * Base argument validator plugin to provide basic functionality. + */ +abstract class ArgumentValidatorPluginBase extends PluginBase { + + /** + * Initialize this plugin with the view and the argument + * it is linked to. + */ + public function init(ViewExecutable $view, &$argument, $options) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->argument = &$argument; + + $this->unpackOptions($this->options, $options); + } + + /** + * Retrieve the options when this is a new access + * control plugin + */ + protected function defineOptions() { return array(); } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { } + + /** + * Provide the default form form for validating options + */ + public function validateOptionsForm(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + public function submitOptionsForm(&$form, &$form_state, &$options = array()) { } + + /** + * Determine if the administrator has the privileges to use this plugin + */ + public function access() { return TRUE; } + + /** + * If we don't have access to the form but are showing it anyway, ensure that + * the form is safe and cannot be changed from user input. + * + * This is only called by child objects if specified in the buildOptionsForm(), + * so it will not always be used. + */ + function check_access(&$form, $option_name) { + if (!$this->access()) { + $form[$option_name]['#disabled'] = TRUE; + $form[$option_name]['#value'] = $form[$this->option_name]['#default_value']; + $form[$option_name]['#description'] .= ' <strong>' . t('Note: you do not have permission to modify this. If you change the default filter type, this setting will be lost and you will NOT be able to get it back.') . '</strong>'; + } + } + + function validate_argument($arg) { return TRUE; } + + /** + * Process the summary arguments for displaying. + * + * Some plugins alter the argument so it uses something else interal. + * For example the user validation set's the argument to the uid, + * for a faster query. But there are use cases where you want to use + * the old value again, for example the summary. + */ + function process_summary_arguments(&$args) { } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/None.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/None.php new file mode 100644 index 0000000..f15fc11 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/None.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument_validator\None. + */ + +namespace Drupal\views\Plugin\views\argument_validator; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Do not validate the argument. + * + * @ingroup views_argument_validate_plugins + * + * @Plugin( + * id = "none", + * title = @Translation(" - Basic validation - ") + * ) + */ +class None extends ArgumentValidatorPluginBase { + + function validate_argument($argument) { + if (!empty($this->argument->options['must_not_be'])) { + return !isset($argument); + } + + if (!isset($argument) || $argument === '') { + return FALSE; + } + + if (!empty($this->argument->definition['numeric']) && !isset($this->argument->options['break_phrase']) && !is_numeric($arg)) { + return FALSE; + } + + return TRUE; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Numeric.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Numeric.php new file mode 100644 index 0000000..fae745d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Numeric.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument_validator\Numeric. + */ + +namespace Drupal\views\Plugin\views\argument_validator; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Validate whether an argument is numeric or not. + * + * @ingroup views_argument_validate_plugins + * + * @Plugin( + * id = "numeric", + * title = @Translation("Numeric") + * ) + */ +class Numeric extends ArgumentValidatorPluginBase { + + function validate_argument($argument) { + return is_numeric($argument); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Php.php b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Php.php new file mode 100644 index 0000000..798533f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/argument_validator/Php.php @@ -0,0 +1,63 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\argument_validator\Php. + */ + +namespace Drupal\views\Plugin\views\argument_validator; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Provide PHP code to validate whether or not an argument is ok. + * + * @ingroup views_argument_validate_plugins + * + * @Plugin( + * id = "php", + * title = @Translation("PHP Code") + * ) + */ +class Php extends ArgumentValidatorPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['code'] = array('default' => ''); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['code'] = array( + '#type' => 'textarea', + '#title' => t('PHP validate code'), + '#default_value' => $this->options['code'], + '#description' => t('Enter PHP code that returns TRUE or FALSE. No return is the same as FALSE, so be SURE to return something if you do not want to declare the argument invalid. Do not use <?php ?>. The argument to validate will be "$argument" and the view will be "$view". You may change the argument by setting "$handler->argument". You may change the title used for substitutions for this argument by setting "$handler->validated_title".'), + ); + + $this->check_access($form, 'code'); + } + + /** + * Only let users with PHP block visibility permissions set/modify this + * validate plugin. + */ + public function access() { + return user_access('use PHP for settings'); + } + + function validate_argument($argument) { + // set up variables to make it easier to reference during the argument. + $view = &$this->view; + $handler = &$this->argument; + + ob_start(); + $result = eval($this->options['code']); + ob_end_clean(); + return $result; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/cache/CachePluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/cache/CachePluginBase.php new file mode 100644 index 0000000..21c7ee2 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/cache/CachePluginBase.php @@ -0,0 +1,363 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\cache\CachePluginBase. + */ + +namespace Drupal\views\Plugin\views\cache; + +use Drupal\views\ViewExecutable; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\views\Plugin\views\PluginBase; +use Drupal\Core\Database\Query\Select; + +/** + * @defgroup views_cache_plugins Views cache plugins + * @{ + * The base plugin to handler caching of a view. + * + * Cache plugins can handle both caching of just the database result and + * the rendered output of the view. + * + * @see hook_views_plugins() + */ + +/** + * The base plugin to handle caching. + */ +abstract class CachePluginBase extends PluginBase { + + /** + * Contains all data that should be written/read from cache. + */ + var $storage = array(); + + /** + * What table to store data in. + */ + var $table = 'views_results'; + + /** + * Stores the cache ID used for the results cache. + * + * The cache ID is stored in generateResultsKey() got executed. + * + * @var string + * + * @see Drupal\views\Plugin\views\cache\CachePluginBase::generateResultsKey() + */ + protected $resultsKey; + + /** + * Stores the cache ID used for the output cache, once generateOutputKey() got + * executed. + * + * @var string + * + * @see Drupal\views\Plugin\views\cache\CachePluginBase::generateOutputKey() + */ + protected $outputKey; + + /** + * Initialize the plugin. + * + * @param $view + * The view object. + * @param $display + * The display handler. + */ + public function init(ViewExecutable $view, &$display, $options = NULL) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->displayHandler = &$display; + + $this->unpackOptions($this->options, $options); + } + + /** + * Returns the outputKey property. + * + * @return string + * The outputKey property. + */ + public function getOutputKey() { + return $this->outputKey; + } + + /** + * Returns the resultsKey property. + * + * @return string + * The resultsKey property. + */ + public function getResultsKey() { + return $this->resultsKey; + } + + /** + * Return a string to display as the clickable title for the + * access control. + */ + public function summaryTitle() { + return t('Unknown'); + } + + /** + * Determine the expiration time of the cache type, or NULL if no expire. + * + * Plugins must override this to implement expiration. + * + * @param $type + * The cache type, either 'query', 'result' or 'output'. + */ + function cache_expire($type) { } + + /** + * Determine expiration time in the cache table of the cache type + * or CACHE_PERMANENT if item shouldn't be removed automatically from cache. + * + * Plugins must override this to implement expiration in the cache table. + * + * @param $type + * The cache type, either 'query', 'result' or 'output'. + */ + function cache_set_expire($type) { + return CacheBackendInterface::CACHE_PERMANENT; + } + + + /** + * Save data to the cache. + * + * A plugin should override this to provide specialized caching behavior. + */ + function cache_set($type) { + switch ($type) { + case 'query': + // Not supported currently, but this is certainly where we'd put it. + break; + case 'results': + $data = array( + 'result' => $this->view->result, + 'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0, + 'current_page' => $this->view->getCurrentPage(), + ); + cache($this->table)->set($this->generateResultsKey(), $data, $this->cache_set_expire($type)); + break; + case 'output': + $this->gather_headers(); + $this->storage['output'] = $this->view->display_handler->output; + cache($this->table)->set($this->generateOutputKey(), $this->storage, $this->cache_set_expire($type)); + break; + } + } + + + /** + * Retrieve data from the cache. + * + * A plugin should override this to provide specialized caching behavior. + */ + function cache_get($type) { + $cutoff = $this->cache_expire($type); + switch ($type) { + case 'query': + // Not supported currently, but this is certainly where we'd put it. + return FALSE; + case 'results': + // Values to set: $view->result, $view->total_rows, $view->execute_time, + // $view->current_page. + if ($cache = cache($this->table)->get($this->generateResultsKey())) { + if (!$cutoff || $cache->created > $cutoff) { + $this->view->result = $cache->data['result']; + $this->view->total_rows = $cache->data['total_rows']; + $this->view->setCurrentPage($cache->data['current_page']); + $this->view->execute_time = 0; + return TRUE; + } + } + return FALSE; + case 'output': + if ($cache = cache($this->table)->get($this->generateOutputKey())) { + if (!$cutoff || $cache->created > $cutoff) { + $this->storage = $cache->data; + $this->view->display_handler->output = $cache->data['output']; + $this->restore_headers(); + return TRUE; + } + } + return FALSE; + } + } + + /** + * Clear out cached data for a view. + * + * We're just going to nuke anything related to the view, regardless of display, + * to be sure that we catch everything. Maybe that's a bad idea. + */ + function cache_flush() { + cache($this->table)->invalidateTags(array($this->view->storage->name => TRUE)); + } + + /** + * Post process any rendered data. + * + * This can be valuable to be able to cache a view and still have some level of + * dynamic output. In an ideal world, the actual output will include HTML + * comment based tokens, and then the post process can replace those tokens. + * + * Example usage. If it is known that the view is a node view and that the + * primary field will be a nid, you can do something like this: + * + * <!--post-FIELD-NID--> + * + * And then in the post render, create an array with the text that should + * go there: + * + * strtr($output, array('<!--post-FIELD-1-->', 'output for FIELD of nid 1'); + * + * All of the cached result data will be available in $view->result, as well, + * so all ids used in the query should be discoverable. + */ + function post_render(&$output) { } + + /** + * Start caching javascript, css and other out of band info. + * + * This takes a snapshot of the current system state so that we don't + * duplicate it. Later on, when gather_headers() is run, this information + * will be removed so that we don't hold onto it. + */ + function cache_start() { + $this->storage['head'] = drupal_add_html_head(); + $this->storage['css'] = drupal_add_css(); + $this->storage['js'] = drupal_add_js(); + } + + /** + * Gather out of band data, compare it to what we started with and store the difference. + */ + function gather_headers() { + // Simple replacement for head + if (isset($this->storage['head'])) { + $this->storage['head'] = str_replace($this->storage['head'], '', drupal_add_html_head()); + } + else { + $this->storage['head'] = ''; + } + + // Slightly less simple for CSS: + $css = drupal_add_css(); + $css_start = isset($this->storage['css']) ? $this->storage['css'] : array(); + $this->storage['css'] = array_diff_assoc($css, $css_start); + + // Get javascript after/before views renders. + $js = drupal_add_js(); + $js_start = isset($this->storage['js']) ? $this->storage['js'] : array(); + // If there are any differences between the old and the new javascript then + // store them to be added later. + $this->storage['js'] = array_diff_assoc($js, $js_start); + + // Special case the settings key and get the difference of the data. + $settings = isset($js['settings']['data']) ? $js['settings']['data'] : array(); + $settings_start = isset($js_start['settings']['data']) ? $js_start['settings']['data'] : array(); + $this->storage['js']['settings'] = array_diff_assoc($settings, $settings_start); + } + + /** + * Restore out of band data saved to cache. Copied from Panels. + */ + function restore_headers() { + if (!empty($this->storage['head'])) { + drupal_add_html_head($this->storage['head']); + } + if (!empty($this->storage['css'])) { + foreach ($this->storage['css'] as $args) { + drupal_add_css($args['data'], $args); + } + } + if (!empty($this->storage['js'])) { + foreach ($this->storage['js'] as $key => $args) { + if ($key !== 'settings') { + drupal_add_js($args['data'], $args); + } + else { + foreach ($args as $setting) { + drupal_add_js($setting, 'setting'); + } + } + } + } + } + + /** + * Calculates and sets a cache ID used for the result cache. + * + * @return string + * The generated cache ID. + */ + public function generateResultsKey() { + global $user; + + if (!isset($this->resultsKey)) { + $build_info = $this->view->build_info; + + foreach (array('query', 'count_query') as $index) { + // If the default query back-end is used generate SQL query strings from + // the query objects. + if ($build_info[$index] instanceof Select) { + $query = clone $build_info[$index]; + $query->preExecute(); + $build_info[$index] = (string) $query; + } + } + $key_data = array( + 'build_info' => $build_info, + 'roles' => array_keys($user->roles), + 'super-user' => $user->uid == 1, // special caching for super user. + 'langcode' => language(LANGUAGE_TYPE_INTERFACE)->langcode, + 'base_url' => $GLOBALS['base_url'], + ); + foreach (array('exposed_info', 'page', 'sort', 'order', 'items_per_page', 'offset') as $key) { + if (isset($_GET[$key])) { + $key_data[$key] = $_GET[$key]; + } + } + + $this->resultsKey = $this->view->storage->name . ':' . $this->displayHandler->display['id'] . ':results:' . md5(serialize($key_data)); + } + + return $this->resultsKey; + } + + /** + * Calculates and sets a cache ID used for the output cache. + * + * @return string + * The generated cache ID. + */ + public function generateOutputKey() { + global $user; + if (!isset($this->outputKey)) { + $key_data = array( + 'result' => $this->view->result, + 'roles' => array_keys($user->roles), + 'super-user' => $user->uid == 1, // special caching for super user. + 'theme' => $GLOBALS['theme'], + 'langcode' => language(LANGUAGE_TYPE_INTERFACE)->langcode, + 'base_url' => $GLOBALS['base_url'], + ); + + $this->outputKey = $this->view->storage->name . ':' . $this->displayHandler->display['id'] . ':output:' . md5(serialize($key_data)); + } + + return $this->outputKey; + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/cache/None.php b/core/modules/views/lib/Drupal/views/Plugin/views/cache/None.php new file mode 100644 index 0000000..89e1703 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/cache/None.php @@ -0,0 +1,38 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\cache\None. + */ + +namespace Drupal\views\Plugin\views\cache; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Caching plugin that provides no caching at all. + * + * @ingroup views_cache_plugins + * + * @Plugin( + * id = "none", + * title = @Translation("None"), + * help = @Translation("No caching of Views data.") + * ) + */ +class None extends CachePluginBase { + + function cache_start() { /* do nothing */ } + + public function summaryTitle() { + return t('None'); + } + + function cache_get($type) { + return FALSE; + } + + function cache_set($type) { } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/cache/Time.php b/core/modules/views/lib/Drupal/views/Plugin/views/cache/Time.php new file mode 100644 index 0000000..aae760d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/cache/Time.php @@ -0,0 +1,131 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\cache\Time. + */ + +namespace Drupal\views\Plugin\views\cache; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Annotation\Translation; + +/** + * Simple caching of query results for Views displays. + * + * @ingroup views_cache_plugins + * + * @Plugin( + * id = "time", + * title = @Translation("Time-based"), + * help = @Translation("Simple time-based caching of data.") + * ) + */ +class Time extends CachePluginBase { + + /** + * Overrides Drupal\views\Plugin\Plugin::$usesOptions. + */ + protected $usesOptions = TRUE; + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['results_lifespan'] = array('default' => 3600); + $options['results_lifespan_custom'] = array('default' => 0); + $options['output_lifespan'] = array('default' => 3600); + $options['output_lifespan_custom'] = array('default' => 0); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $options = array(60, 300, 1800, 3600, 21600, 518400); + $options = drupal_map_assoc($options, 'format_interval'); + $options = array(-1 => t('Never cache')) + $options + array('custom' => t('Custom')); + + $form['results_lifespan'] = array( + '#type' => 'select', + '#title' => t('Query results'), + '#description' => t('The length of time raw query results should be cached.'), + '#options' => $options, + '#default_value' => $this->options['results_lifespan'], + ); + $form['results_lifespan_custom'] = array( + '#type' => 'textfield', + '#title' => t('Seconds'), + '#size' => '25', + '#maxlength' => '30', + '#description' => t('Length of time in seconds raw query results should be cached.'), + '#default_value' => $this->options['results_lifespan_custom'], + '#states' => array( + 'visible' => array( + ':input[name="cache_options[results_lifespan]"]' => array('value' => 'custom'), + ), + ), + ); + $form['output_lifespan'] = array( + '#type' => 'select', + '#title' => t('Rendered output'), + '#description' => t('The length of time rendered HTML output should be cached.'), + '#options' => $options, + '#default_value' => $this->options['output_lifespan'], + ); + $form['output_lifespan_custom'] = array( + '#type' => 'textfield', + '#title' => t('Seconds'), + '#size' => '25', + '#maxlength' => '30', + '#description' => t('Length of time in seconds rendered HTML output should be cached.'), + '#default_value' => $this->options['output_lifespan_custom'], + '#states' => array( + 'visible' => array( + ':input[name="cache_options[output_lifespan]"]' => array('value' => 'custom'), + ), + ), + ); + } + + public function validateOptionsForm(&$form, &$form_state) { + $custom_fields = array('output_lifespan', 'results_lifespan'); + foreach ($custom_fields as $field) { + if ($form_state['values']['cache_options'][$field] == 'custom' && !is_numeric($form_state['values']['cache_options'][$field . '_custom'])) { + form_error($form[$field .'_custom'], t('Custom time values must be numeric.')); + } + } + } + + public function summaryTitle() { + $results_lifespan = $this->get_lifespan('results'); + $output_lifespan = $this->get_lifespan('output'); + return format_interval($results_lifespan, 1) . '/' . format_interval($output_lifespan, 1); + } + + function get_lifespan($type) { + $lifespan = $this->options[$type . '_lifespan'] == 'custom' ? $this->options[$type . '_lifespan_custom'] : $this->options[$type . '_lifespan']; + return $lifespan; + } + + function cache_expire($type) { + $lifespan = $this->get_lifespan($type); + if ($lifespan) { + $cutoff = REQUEST_TIME - $lifespan; + return $cutoff; + } + else { + return FALSE; + } + } + + function cache_set_expire($type) { + $lifespan = $this->get_lifespan($type); + if ($lifespan) { + return time() + $lifespan; + } + else { + return CacheBackendInterface::CACHE_PERMANENT; + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/Attachment.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/Attachment.php new file mode 100644 index 0000000..a0f6a2f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/Attachment.php @@ -0,0 +1,299 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\display\Attachment. + */ + +namespace Drupal\views\Plugin\views\display; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The plugin that handles an attachment display. + * + * Attachment displays are secondary displays that are 'attached' to a primary + * display. Effectively they are a simple way to get multiple views within + * the same view. They can share some information. + * + * @ingroup views_display_plugins + * + * @Plugin( + * id = "attachment", + * title = @Translation("Attachment"), + * help = @Translation("Attachments added to other displays to achieve multiple views in the same view."), + * theme = "views_view", + * contextual_links_locations = {""} + * ) + */ +class Attachment extends DisplayPluginBase { + + /** + * Whether the display allows the use of a pager or not. + * + * @var bool + */ + protected $usesPager = FALSE; + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['displays'] = array('default' => array()); + $options['attachment_position'] = array('default' => 'before'); + $options['inherit_arguments'] = array('default' => TRUE, 'bool' => TRUE); + $options['inherit_exposed_filters'] = array('default' => FALSE, 'bool' => TRUE); + $options['inherit_pager'] = array('default' => FALSE, 'bool' => TRUE); + $options['render_pager'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function execute() { + return $this->view->render($this->display['id']); + } + + public function attachmentPositions($position = NULL) { + $positions = array( + 'before' => t('Before'), + 'after' => t('After'), + 'both' => t('Both'), + ); + + if ($position) { + return $positions[$position]; + } + + return $positions; + } + + /** + * Provide the summary for attachment options in the views UI. + * + * This output is returned as an array. + */ + public function optionsSummary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::optionsSummary($categories, $options); + + $categories['attachment'] = array( + 'title' => t('Attachment settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + $displays = array_filter($this->getOption('displays')); + if (count($displays) > 1) { + $attach_to = t('Multiple displays'); + } + elseif (count($displays) == 1) { + $display = array_shift($displays); + if (!empty($this->view->display[$display])) { + $attach_to = check_plain($this->view->display[$display]['display_title']); + } + } + + if (!isset($attach_to)) { + $attach_to = t('Not defined'); + } + + $options['displays'] = array( + 'category' => 'attachment', + 'title' => t('Attach to'), + 'value' => $attach_to, + ); + + $options['attachment_position'] = array( + 'category' => 'attachment', + 'title' => t('Attachment position'), + 'value' => $this->attachmentPositions($this->getOption('attachment_position')), + ); + + $options['inherit_arguments'] = array( + 'category' => 'attachment', + 'title' => t('Inherit contextual filters'), + 'value' => $this->getOption('inherit_arguments') ? t('Yes') : t('No'), + ); + + $options['inherit_exposed_filters'] = array( + 'category' => 'attachment', + 'title' => t('Inherit exposed filters'), + 'value' => $this->getOption('inherit_exposed_filters') ? t('Yes') : t('No'), + ); + + $options['inherit_pager'] = array( + 'category' => 'pager', + 'title' => t('Inherit pager'), + 'value' => $this->getOption('inherit_pager') ? t('Yes') : t('No'), + ); + + $options['render_pager'] = array( + 'category' => 'pager', + 'title' => t('Render pager'), + 'value' => $this->getOption('render_pager') ? t('Yes') : t('No'), + ); + + } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::buildOptionsForm($form, $form_state); + + switch ($form_state['section']) { + case 'inherit_arguments': + $form['#title'] .= t('Inherit contextual filters'); + $form['inherit_arguments'] = array( + '#type' => 'checkbox', + '#title' => t('Inherit'), + '#description' => t('Should this display inherit its contextual filter values from the parent display to which it is attached?'), + '#default_value' => $this->getOption('inherit_arguments'), + ); + break; + case 'inherit_exposed_filters': + $form['#title'] .= t('Inherit exposed filters'); + $form['inherit_exposed_filters'] = array( + '#type' => 'checkbox', + '#title' => t('Inherit'), + '#description' => t('Should this display inherit its exposed filter values from the parent display to which it is attached?'), + '#default_value' => $this->getOption('inherit_exposed_filters'), + ); + break; + case 'inherit_pager': + $form['#title'] .= t('Inherit pager'); + $form['inherit_pager'] = array( + '#type' => 'checkbox', + '#title' => t('Inherit'), + '#description' => t('Should this display inherit its paging values from the parent display to which it is attached?'), + '#default_value' => $this->getOption('inherit_pager'), + ); + break; + case 'render_pager': + $form['#title'] .= t('Render pager'); + $form['render_pager'] = array( + '#type' => 'checkbox', + '#title' => t('Render'), + '#description' => t('Should this display render the pager values? This is only meaningful if inheriting a pager.'), + '#default_value' => $this->getOption('render_pager'), + ); + break; + case 'attachment_position': + $form['#title'] .= t('Position'); + $form['attachment_position'] = array( + '#type' => 'radios', + '#description' => t('Attach before or after the parent display?'), + '#options' => $this->attachmentPositions(), + '#default_value' => $this->getOption('attachment_position'), + ); + break; + case 'displays': + $form['#title'] .= t('Attach to'); + $displays = array(); + foreach ($this->view->display as $display_id => $display) { + if (!empty($this->view->displayHandlers[$display_id]) && $this->view->displayHandlers[$display_id]->acceptAttachments()) { + $displays[$display_id] = $display['display_title']; + } + } + $form['displays'] = array( + '#type' => 'checkboxes', + '#description' => t('Select which display or displays this should attach to.'), + '#options' => $displays, + '#default_value' => $this->getOption('displays'), + ); + break; + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::submitOptionsForm($form, $form_state); + switch ($form_state['section']) { + case 'inherit_arguments': + case 'inherit_pager': + case 'render_pager': + case 'inherit_exposed_filters': + case 'attachment_position': + case 'displays': + $this->setOption($form_state['section'], $form_state['values'][$form_state['section']]); + break; + } + } + + /** + * Attach to another view. + */ + public function attachTo($display_id) { + $displays = $this->getOption('displays'); + + if (empty($displays[$display_id])) { + return; + } + + if (!$this->access()) { + return; + } + + // Get a fresh view because our current one has a lot of stuff on it because it's + // already been executed. + $view = $this->view->cloneView(); + + $args = $this->getOption('inherit_arguments') ? $this->view->args : array(); + $view->setArguments($args); + $view->setDisplay($this->display['id']); + if ($this->getOption('inherit_pager')) { + $view->display_handler->usesPager = $this->view->display[$display_id]->handler->usesPager(); + $view->display_handler->setOption('pager', $this->view->display[$display_id]->handler->getOption('pager')); + } + + $attachment = $view->executeDisplay($this->display['id'], $args); + + switch ($this->getOption('attachment_position')) { + case 'before': + $this->view->attachment_before .= $attachment; + break; + case 'after': + $this->view->attachment_after .= $attachment; + break; + case 'both': + $this->view->attachment_before .= $attachment; + $this->view->attachment_after .= $attachment; + break; + } + + $view->destroy(); + } + + /** + * Attachment displays only use exposed widgets if + * they are set to inherit the exposed filter settings + * of their parent display. + */ + public function usesExposed() { + if (!empty($this->options['inherit_exposed_filters']) && parent::usesExposed()) { + return TRUE; + } + return FALSE; + } + + /** + * If an attachment is set to inherit the exposed filter + * settings from its parent display, then don't render and + * display a second set of exposed filter widgets. + */ + public function displaysExposed() { + return $this->options['inherit_exposed_filters'] ? FALSE : TRUE; + } + + public function renderPager() { + return $this->usesPager() && $this->getOption('render_pager'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/DefaultDisplay.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/DefaultDisplay.php new file mode 100644 index 0000000..c739973 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/DefaultDisplay.php @@ -0,0 +1,79 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\display\DefaultDisplay. + */ + +namespace Drupal\views\Plugin\views\display; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * A plugin to handle defaults on a view. + * + * @ingroup views_display_plugins + * + * @Plugin( + * id = "default", + * title = @Translation("Master"), + * help = @Translation("Default settings for this view."), + * theme = "views_view", + * no_ui = TRUE + * ) + */ +class DefaultDisplay extends DisplayPluginBase { + + /** + * Whether the display allows attachments. + * + * @var bool + */ + protected $usesAttachments = TRUE; + + /** + * Determine if this display is the 'default' display which contains + * fallback settings + */ + public function isDefaultDisplay() { return TRUE; } + + /** + * The default execute handler fully renders the view. + * + * For the simplest use: + * @code + * $output = $view->executeDisplay('default', $args); + * @endcode + * + * For more complex usages, a view can be partially built: + * @code + * $view->setArguments($args); + * $view->build('default'); // Build the query + * $view->preExecute(); // Pre-execute the query. + * $view->execute(); // Run the query + * $output = $view->render(); // Render the view + * @endcode + * + * If short circuited at any point, look in $view->build_info for + * information about the query. After execute, look in $view->result + * for the array of objects returned from db_query. + * + * You can also do: + * @code + * $view->setArguments($args); + * $output = $view->render('default'); // Render the view + * @endcode + * + * This illustrates that render is smart enough to call build and execute + * if these items have not already been accomplished. + * + * Note that execute also must accomplish other tasks, such + * as setting page titles, breadcrumbs, and generating exposed filter + * data if necessary. + */ + public function execute() { + return $this->view->render($this->display['id']); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php new file mode 100644 index 0000000..e8b2327 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php @@ -0,0 +1,2720 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\display\DisplayPluginBase. + */ + +namespace Drupal\views\Plugin\views\display; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\PluginBase; + +/** + * @defgroup views_display_plugins Views display plugins + * @{ + * Display plugins control how Views interact with the rest of Drupal. + * + * They can handle creating Views from a Drupal page hook; they can + * handle creating Views from a Drupal block hook. They can also + * handle creating Views from an external module source, such as + * a Panels pane, or an insert view, or a CCK field type. + * + * @see hook_views_plugins() + */ + +/** + * The default display plugin handler. Display plugins handle options and + * basic mechanisms for different output methods. + */ +abstract class DisplayPluginBase extends PluginBase { + + /** + * The top object of a view. + * + * @var Drupal\views\ViewExecutable + */ + var $view = NULL; + + var $handlers = array(); + + /** + * An array of instantiated plugins used in this display. + * + * @var array + */ + protected $plugins = array(); + + /** + * Stores all available display extenders. + */ + var $extender = array(); + + /** + * Overrides Drupal\views\Plugin\Plugin::$usesOptions. + */ + protected $usesOptions = TRUE; + + /** + * Stores the rendered output of the display. + * + * @see View::render + * @var string + */ + public $output = NULL; + + /** + * Whether the display allows the use of AJAX or not. + * + * @var bool + */ + protected $usesAJAX = TRUE; + + /** + * Whether the display allows the use of a pager or not. + * + * @var bool + */ + protected $usesPager = TRUE; + + /** + * Whether the display allows the use of a 'more' link or not. + * + * @var bool + */ + protected $usesMore = TRUE; + + /** + * Whether the display allows attachments. + * + * @var bool + * TRUE if the display can use attachments, or FALSE otherwise. + */ + protected $usesAttachments = FALSE; + + public function init(ViewExecutable $view, &$display, $options = NULL) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->display = &$display; + + // Load extenders as soon as possible. + $this->extender = array(); + $extenders = views_get_enabled_display_extenders(); + if (!empty($extenders)) { + foreach ($extenders as $extender) { + $plugin = views_get_plugin('display_extender', $extender); + if ($plugin) { + $plugin->init($this->view, $this); + $this->extender[$extender] = $plugin; + } + } + } + + // Track changes that the user should know about. + $changed = FALSE; + + // Make some modifications: + if (!isset($options) && isset($display['display_options'])) { + $options = $display['display_options']; + } + + if ($this->isDefaultDisplay() && isset($options['defaults'])) { + unset($options['defaults']); + } + + views_include('cache'); + // Cache for unpackOptions, but not if we are in the ui. + static $unpack_options = array(); + if (empty($view->editing)) { + $cid = 'unpackOptions:' . md5(serialize(array($this->options, $options))); + if (empty($unpack_options[$cid])) { + $cache = views_cache_get($cid, TRUE); + if (!empty($cache->data)) { + $this->options = $cache->data; + } + else { + $this->unpackOptions($this->options, $options); + views_cache_set($cid, $this->options, TRUE); + } + $unpack_options[$cid] = $this->options; + } + else { + $this->options = $unpack_options[$cid]; + } + } + else { + $this->unpackOptions($this->options, $options); + } + + // Convert the field_language and field_language_add_to_query settings. + $field_language = $this->getOption('field_language'); + $field_language_add_to_query = $this->getOption('field_language_add_to_query'); + if (isset($field_langcode)) { + $this->setOption('field_langcode', $field_language); + $this->setOption('field_langcode_add_to_query', $field_language_add_to_query); + $changed = TRUE; + } + + // Mark the view as changed so the user has a chance to save it. + if ($changed) { + $this->view->changed = TRUE; + } + } + + public function destroy() { + parent::destroy(); + + foreach ($this->handlers as $type => $handlers) { + foreach ($handlers as $id => $handler) { + if (is_object($handler)) { + $this->handlers[$type][$id]->destroy(); + } + } + } + + if (isset($this->default_display)) { + unset($this->default_display); + } + + foreach ($this->extender as $extender) { + $extender->destroy(); + } + } + + /** + * Determine if this display is the 'default' display which contains + * fallback settings + */ + public function isDefaultDisplay() { return FALSE; } + + /** + * Determine if this display uses exposed filters, so the view + * will know whether or not to build them. + */ + public function usesExposed() { + if (!isset($this->has_exposed)) { + foreach ($this->handlers as $type => $value) { + foreach ($this->view->$type as $id => $handler) { + if ($handler->canExpose() && $handler->isExposed()) { + // one is all we need; if we find it, return true. + $this->has_exposed = TRUE; + return TRUE; + } + } + } + $pager = $this->getPlugin('pager'); + if (isset($pager) && $pager->uses_exposed()) { + $this->has_exposed = TRUE; + return TRUE; + } + $this->has_exposed = FALSE; + } + + return $this->has_exposed; + } + + /** + * Determine if this display should display the exposed + * filters widgets, so the view will know whether or not + * to render them. + * + * Regardless of what this function + * returns, exposed filters will not be used nor + * displayed unless usesExposed() returns TRUE. + */ + public function displaysExposed() { + return TRUE; + } + + /** + * Whether the display allows the use of AJAX or not. + * + * @return bool + */ + public function usesAJAX() { + return $this->usesAJAX; + } + + /** + * Whether the display is actually using AJAX or not. + * + * @return bool + */ + public function isAJAXEnabled() { + if ($this->usesAJAX()) { + return $this->getOption('use_ajax'); + } + return FALSE; + } + + /** + * Whether the display is enabled. + * + * @return bool + * Returns TRUE if the display is marked as enabled, else FALSE. + */ + public function isEnabled() { + return (bool) $this->getOption('enabled'); + } + + /** + * Whether the display allows the use of a pager or not. + * + * @return bool + */ + + public function usesPager() { + return $this->usesPager; + } + + /** + * Whether the display is using a pager or not. + * + * @return bool + */ + public function isPagerEnabled() { + if ($this->usesPager()) { + $pager = $this->getPlugin('pager'); + if ($pager) { + return $pager->use_pager(); + } + } + return FALSE; + } + + /** + * Whether the display allows the use of a 'more' link or not. + * + * @return bool + */ + public function usesMore() { + return $this->usesMore; + } + + /** + * Whether the display is using the 'more' link or not. + * + * @return bool + */ + public function isMoreEnabled() { + if ($this->usesMore()) { + return $this->getOption('use_more'); + } + return FALSE; + } + + /** + * Does the display have groupby enabled? + */ + public function useGroupBy() { + return $this->getOption('group_by'); + } + + /** + * Should the enabled display more link be shown when no more items? + */ + public function useMoreAlways() { + if ($this->usesMore()) { + return $this->getOption('useMoreAlways'); + } + return FALSE; + } + + /** + * Does the display have custom link text? + */ + public function useMoreText() { + if ($this->usesMore()) { + return $this->getOption('useMoreText'); + } + return FALSE; + } + + /** + * Determines whether this display can use attachments. + * + * @return bool + */ + public function acceptAttachments() { + if (!$this->usesAttachments()) { + return FALSE; + } + if (!empty($this->view->argument) && $this->getOption('hide_attachment_summary')) { + foreach ($this->view->argument as $argument_id => $argument) { + if ($argument->needsStylePlugin() && empty($argument->argument_validated)) { + return FALSE; + } + } + } + return TRUE; + } + + /** + * Returns whether the display can use attachments. + * + * @return bool + */ + public function usesAttachments() { + return $this->usesAttachments; + } + + /** + * Allow displays to attach to other views. + */ + public function attachTo($display_id) { } + + /** + * Static member function to list which sections are defaultable + * and what items each section contains. + */ + public function defaultableSections($section = NULL) { + $sections = array( + 'access' => array('access'), + 'cache' => array('cache'), + 'title' => array('title'), + 'css_class' => array('css_class'), + 'use_ajax' => array('use_ajax'), + 'hide_attachment_summary' => array('hide_attachment_summary'), + 'hide_admin_links' => array('hide_admin_links'), + 'group_by' => array('group_by'), + 'query' => array('query'), + 'use_more' => array('use_more', 'use_more_always', 'use_more_text'), + 'use_more_always' => array('use_more', 'use_more_always', 'use_more_text'), + 'use_more_text' => array('use_more', 'use_more_always', 'use_more_text'), + 'link_display' => array('link_display', 'link_url'), + + // Force these to cascade properly. + 'style' => array('style', 'row'), + 'row' => array('style', 'row'), + + 'pager' => array('pager', 'pager_options'), + 'pager_options' => array('pager', 'pager_options'), + + 'exposed_form' => array('exposed_form', 'exposed_form_options'), + 'exposed_form_options' => array('exposed_form', 'exposed_form_options'), + + // These guys are special + 'header' => array('header'), + 'footer' => array('footer'), + 'empty' => array('empty'), + 'relationships' => array('relationships'), + 'fields' => array('fields'), + 'sorts' => array('sorts'), + 'arguments' => array('arguments'), + 'filters' => array('filters', 'filter_groups'), + 'filter_groups' => array('filters', 'filter_groups'), + ); + + // If the display cannot use a pager, then we cannot default it. + if (!$this->usesPager()) { + unset($sections['pager']); + unset($sections['items_per_page']); + } + + foreach ($this->extender as $extender) { + $extender->defaultableSections($sections, $section); + } + + if ($section) { + if (!empty($sections[$section])) { + return $sections[$section]; + } + } + else { + return $sections; + } + } + + protected function defineOptions() { + $options = array( + 'defaults' => array( + 'default' => array( + 'access' => TRUE, + 'cache' => TRUE, + 'query' => TRUE, + 'title' => TRUE, + 'css_class' => TRUE, + + 'display_description' => FALSE, + 'use_ajax' => TRUE, + 'hide_attachment_summary' => TRUE, + 'hide_admin_links' => FALSE, + 'pager' => TRUE, + 'use_more' => TRUE, + 'use_more_always' => TRUE, + 'use_more_text' => TRUE, + 'exposed_form' => TRUE, + + 'link_display' => TRUE, + 'link_url' => '', + 'group_by' => TRUE, + + 'style' => TRUE, + 'row' => TRUE, + + 'header' => TRUE, + 'footer' => TRUE, + 'empty' => TRUE, + + 'relationships' => TRUE, + 'fields' => TRUE, + 'sorts' => TRUE, + 'arguments' => TRUE, + 'filters' => TRUE, + 'filter_groups' => TRUE, + ), + ), + + 'title' => array( + 'default' => '', + 'translatable' => TRUE, + ), + 'enabled' => array( + 'default' => TRUE, + 'translatable' => FALSE, + 'bool' => TRUE, + ), + 'display_comment' => array( + 'default' => '', + ), + 'css_class' => array( + 'default' => '', + 'translatable' => FALSE, + ), + 'display_description' => array( + 'default' => '', + 'translatable' => TRUE, + ), + 'use_ajax' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'hide_attachment_summary' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'hide_admin_links' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'use_more' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'use_more_always' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'use_more_text' => array( + 'default' => 'more', + 'translatable' => TRUE, + ), + 'link_display' => array( + 'default' => '', + ), + 'link_url' => array( + 'default' => '', + ), + 'group_by' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'field_langcode' => array( + 'default' => '***CURRENT_LANGUAGE***', + ), + 'field_langcode_add_to_query' => array( + 'default' => TRUE, + 'bool' => TRUE, + ), + + // These types are all plugins that can have individual settings + // and therefore need special handling. + 'access' => array( + 'contains' => array( + 'type' => array('default' => 'none'), + 'options' => array('default' => array()), + ), + ), + 'cache' => array( + 'contains' => array( + 'type' => array('default' => 'none'), + 'options' => array('default' => array()), + ), + ), + 'query' => array( + 'contains' => array( + 'type' => array('default' => 'views_query'), + 'options' => array('default' => array()), + ), + ), + 'exposed_form' => array( + 'contains' => array( + 'type' => array('default' => 'basic'), + 'options' => array('default' => array()), + ), + ), + 'pager' => array( + 'contains' => array( + 'type' => array('default' => 'full'), + 'options' => array('default' => array()), + ), + ), + 'style' => array( + 'contains' => array( + 'type' => array('default' => 'default'), + 'options' => array('default' => array()), + ), + ), + 'row' => array( + 'contains' => array( + 'type' => array('default' => 'fields'), + 'options' => array('default' => array()), + ), + ), + + 'exposed_block' => array( + 'default' => FALSE, + ), + + 'header' => array( + 'default' => array(), + ), + 'footer' => array( + 'default' => array(), + ), + 'empty' => array( + 'default' => array(), + ), + + // We want these to export last. + // These are the 5 handler types. + 'relationships' => array( + 'default' => array(), + ), + 'fields' => array( + 'default' => array(), + ), + 'sorts' => array( + 'default' => array(), + ), + 'arguments' => array( + 'default' => array(), + ), + 'filter_groups' => array( + 'contains' => array( + 'operator' => array('default' => 'AND'), + 'groups' => array('default' => array(1 => 'AND')), + ), + ), + 'filters' => array( + 'default' => array(), + ), + ); + + if (!$this->usesPager()) { + $options['defaults']['default']['use_pager'] = FALSE; + $options['defaults']['default']['items_per_page'] = FALSE; + $options['defaults']['default']['offset'] = FALSE; + $options['defaults']['default']['pager'] = FALSE; + $options['pager']['contains']['type']['default'] = 'some'; + } + + if ($this->isDefaultDisplay()) { + unset($options['defaults']); + } + + foreach ($this->extender as $extender) { + $extender->defineOptionsAlter($options); + } + + return $options; + } + + /** + * Check to see if the display has a 'path' field. + * + * This is a pure function and not just a setting on the definition + * because some displays (such as a panel pane) may have a path based + * upon configuration. + * + * By default, displays do not have a path. + */ + public function hasPath() { return FALSE; } + + /** + * Check to see if the display has some need to link to another display. + * + * For the most part, displays without a path will use a link display. However, + * sometimes displays that have a path might also need to link to another display. + * This is true for feeds. + */ + public function usesLinkDisplay() { return !$this->hasPath(); } + + /** + * Check to see if the display can put the exposed formin a block. + * + * By default, displays that do not have a path cannot disconnect + * the exposed form and put it in a block, because the form has no + * place to go and Views really wants the forms to go to a specific + * page. + */ + public function usesExposedFormInBlock() { return $this->hasPath(); } + + /** + * Check to see which display to use when creating links within + * a view using this display. + */ + public function getLinkDisplay() { + $display_id = $this->getOption('link_display'); + // If unknown, pick the first one. + if (empty($display_id) || empty($this->view->displayHandlers[$display_id])) { + foreach ($this->view->displayHandlers as $display_id => $display) { + if (!empty($display) && $display->hasPath()) { + return $display_id; + } + } + } + else { + return $display_id; + } + // fall-through returns NULL + } + + /** + * Return the base path to use for this display. + * + * This can be overridden for displays that do strange things + * with the path. + */ + public function getPath() { + if ($this->hasPath()) { + return $this->getOption('path'); + } + + $display_id = $this->getLinkDisplay(); + if ($display_id && !empty($this->view->displayHandlers[$display_id]) && is_object($this->view->displayHandlers[$display_id])) { + return $this->view->displayHandlers[$display_id]->getPath(); + } + } + + public function getUrl() { + return $this->view->getUrl(); + } + + /** + * Check to see if the display needs a breadcrumb + * + * By default, displays do not need breadcrumbs + */ + public function usesBreadcrumb() { return FALSE; } + + /** + * Determine if a given option is set to use the default display or the + * current display + * + * @return + * TRUE for the default display + */ + public function isDefaulted($option) { + return !$this->isDefaultDisplay() && !empty($this->default_display) && !empty($this->options['defaults'][$option]); + } + + /** + * Intelligently get an option either from this display or from the + * default display, if directed to do so. + */ + public function getOption($option) { + if ($this->isDefaulted($option)) { + return $this->default_display->getOption($option); + } + + if (array_key_exists($option, $this->options)) { + return $this->options[$option]; + } + } + + /** + * Determine if the display's style uses fields. + * + * @return bool + */ + public function usesFields() { + return $this->getPlugin('style')->usesFields(); + } + + /** + * Get the instance of a plugin, for example style or row. + * + * @param string $type + * The type of the plugin. + * + * @return Drupal\views\Plugin\views\PluginBase + */ + public function getPlugin($type) { + // Look up the plugin name to use for this instance. + $options = $this->getOption($type); + $name = $options['type']; + + // Query plugins allow specifying a specific query class per base table. + if ($type == 'query') { + $views_data = views_fetch_data($this->view->storage->base_table); + $name = !empty($views_data['table']['base']['query class']) ? $views_data['table']['base']['query class'] : 'views_query'; + } + + // Plugin instances are stored on the display for re-use. + if (!isset($this->plugins[$type][$name])) { + $plugin = drupal_container()->get("plugin.manager.views.$type")->createInstance($name); + + // Initialize the plugin. Query has a unique method signature. + if ($type == 'query') { + $plugin->init($this->view->storage->base_table, $this->view->storage->base_field, $options['options']); + } + else { + $plugin->init($this->view, $this, $options['options']); + } + + $this->plugins[$type][$name] = $plugin; + } + + return $this->plugins[$type][$name]; + } + + /** + * Get the handler object for a single handler. + */ + public function &getHandler($type, $id) { + if (!isset($this->handlers[$type])) { + $this->getHandlers($type); + } + + if (isset($this->handlers[$type][$id])) { + return $this->handlers[$type][$id]; + } + + // So we can return a reference. + $null = NULL; + return $null; + } + + /** + * Get a full array of handlers for $type. This caches them. + */ + public function getHandlers($type) { + if (!isset($this->handlers[$type])) { + $this->handlers[$type] = array(); + $types = ViewExecutable::viewsHandlerTypes(); + $plural = $types[$type]['plural']; + + foreach ($this->getOption($plural) as $id => $info) { + // If this is during form submission and there are temporary options + // which can only appear if the view is in the edit cache, use those + // options instead. This is used for AJAX multi-step stuff. + if (isset($_POST['form_id']) && isset($this->view->temporary_options[$type][$id])) { + $info = $this->view->temporary_options[$type][$id]; + } + + if ($info['id'] != $id) { + $info['id'] = $id; + } + + // If aggregation is on, the group type might override the actual + // handler that is in use. This piece of code checks that and, + // if necessary, sets the override handler. + $override = NULL; + if ($this->useGroupBy() && !empty($info['group_type'])) { + if (empty($this->view->query)) { + $this->view->initQuery(); + } + $aggregate = $this->view->query->get_aggregation_info(); + if (!empty($aggregate[$info['group_type']]['handler'][$type])) { + $override = $aggregate[$info['group_type']]['handler'][$type]; + } + } + + if (!empty($types[$type]['type'])) { + $handler_type = $types[$type]['type']; + } + else { + $handler_type = $type; + } + + $handler = views_get_handler($info['table'], $info['field'], $handler_type, $override); + if ($handler) { + // Special override for area types so they know where they come from. + if ($handler_type == 'area') { + $handler->handler_type = $type; + } + + $handler->init($this->view, $info); + $this->handlers[$type][$id] = &$handler; + } + + // Prevent reference problems. + unset($handler); + } + } + + return $this->handlers[$type]; + } + + /** + * Retrieve a list of fields for the current display with the + * relationship associated if it exists. + * + * @param $groupable_only + * Return only an array of field labels from handler that return TRUE + * from use_string_group_by method. + */ + public function getFieldLabels() { + // Use func_get_arg so the function signature isn't amended + // but we can still pass TRUE into the function to filter + // by groupable handlers. + $args = func_get_args(); + $groupable_only = isset($args[0]) ? $args[0] : FALSE; + + $options = array(); + foreach ($this->getHandlers('relationship') as $relationship => $handler) { + if ($label = $handler->label()) { + $relationships[$relationship] = $label; + } + else { + $relationships[$relationship] = $handler->adminLabel(); + } + } + + foreach ($this->getHandlers('field') as $id => $handler) { + if ($groupable_only && !$handler->use_string_group_by()) { + // Continue to next handler if it's not groupable. + continue; + } + if ($label = $handler->label()) { + $options[$id] = $label; + } + else { + $options[$id] = $handler->adminLabel(); + } + if (!empty($handler->options['relationship']) && !empty($relationships[$handler->options['relationship']])) { + $options[$id] = '(' . $relationships[$handler->options['relationship']] . ') ' . $options[$id]; + } + } + return $options; + } + + /** + * Intelligently set an option either from this display or from the + * default display, if directed to do so. + */ + public function setOption($option, $value) { + if ($this->isDefaulted($option)) { + return $this->default_display->setOption($option, $value); + } + + // Set this in two places: On the handler where we'll notice it + // but also on the display object so it gets saved. This should + // only be a temporary fix. + $this->display['display_options'][$option] = $value; + return $this->options[$option] = $value; + } + + /** + * Set an option and force it to be an override. + */ + public function overrideOption($option, $value) { + $this->setOverride($option, FALSE); + $this->setOption($option, $value); + } + + /** + * Because forms may be split up into sections, this provides + * an easy URL to exactly the right section. Don't override this. + */ + public function optionLink($text, $section, $class = '', $title = '') { + views_add_js('ajax'); + if (!empty($class)) { + $text = '<span>' . $text . '</span>'; + } + + if (!trim($text)) { + $text = t('Broken field'); + } + + if (empty($title)) { + $title = $text; + } + + return l($text, 'admin/structure/views/nojs/display/' . $this->view->storage->name . '/' . $this->display['id'] . '/' . $section, array('attributes' => array('class' => 'views-ajax-link ' . $class, 'title' => $title, 'id' => drupal_html_id('views-' . $this->display['id'] . '-' . $section)), 'html' => TRUE)); + } + + /** + * Returns to tokens for arguments. + * + * This function is similar to views_handler_field::get_render_tokens() + * but without fields tokens. + */ + public function getArgumentsTokens() { + $tokens = array(); + if (!empty($this->view->build_info['substitutions'])) { + $tokens = $this->view->build_info['substitutions']; + } + $count = 0; + foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) { + $token = '%' . ++$count; + if (!isset($tokens[$token])) { + $tokens[$token] = ''; + } + + // Use strip tags as there should never be HTML in the path. + // However, we need to preserve special characters like " that + // were removed by check_plain(). + $tokens['!' . $count] = isset($this->view->args[$count - 1]) ? strip_tags(decode_entities($this->view->args[$count - 1])) : ''; + } + + return $tokens; + } + + /** + * Provide the default summary for options in the views UI. + * + * This output is returned as an array. + */ + public function optionsSummary(&$categories, &$options) { + $categories = array( + 'title' => array( + 'title' => t('Title'), + 'column' => 'first', + ), + 'format' => array( + 'title' => t('Format'), + 'column' => 'first', + ), + 'filters' => array( + 'title' => t('Filters'), + 'column' => 'first', + ), + 'fields' => array( + 'title' => t('Fields'), + 'column' => 'first', + ), + 'pager' => array( + 'title' => t('Pager'), + 'column' => 'second', + ), + 'exposed' => array( + 'title' => t('Exposed form'), + 'column' => 'third', + 'build' => array( + '#weight' => 1, + ), + ), + 'access' => array( + 'title' => '', + 'column' => 'second', + 'build' => array( + '#weight' => -5, + ), + ), + 'other' => array( + 'title' => t('Other'), + 'column' => 'third', + 'build' => array( + '#weight' => 2, + ), + ), + ); + + if ($this->display['id'] != 'default') { + $options['display_id'] = array( + 'category' => 'other', + 'title' => t('Machine Name'), + 'value' => !empty($this->display['new_id']) ? check_plain($this->display['new_id']) : check_plain($this->display['id']), + 'desc' => t('Change the machine name of this display.'), + ); + } + + $display_comment = check_plain(drupal_substr($this->getOption('display_comment'), 0, 10)); + $options['display_comment'] = array( + 'category' => 'other', + 'title' => t('Comment'), + 'value' => !empty($display_comment) ? $display_comment : t('No comment'), + 'desc' => t('Comment or document this display.'), + ); + + $title = strip_tags($this->getOption('title')); + if (!$title) { + $title = t('None'); + } + + $options['title'] = array( + 'category' => 'title', + 'title' => t('Title'), + 'value' => $title, + 'desc' => t('Change the title that this display will use.'), + ); + + $style_plugin_instance = $this->getPlugin('style'); + $style_summary = empty($style_plugin_instance->definition['title']) ? t('Missing style plugin') : $style_plugin_instance->summaryTitle(); + $style_title = empty($style_plugin_instance->definition['title']) ? t('Missing style plugin') : $style_plugin_instance->pluginTitle(); + + $style = ''; + + $options['style'] = array( + 'category' => 'format', + 'title' => t('Format'), + 'value' => $style_title, + 'setting' => $style_summary, + 'desc' => t('Change the way content is formatted.'), + ); + + // This adds a 'Settings' link to the style_options setting if the style has options. + if ($style_plugin_instance->usesOptions()) { + $options['style']['links']['style_options'] = t('Change settings for this format'); + } + + if ($style_plugin_instance->usesRowPlugin()) { + $row_plugin_instance = $this->getPlugin('row'); + $row_summary = empty($row_plugin_instance->definition['title']) ? t('Missing style plugin') : $row_plugin_instance->summaryTitle(); + $row_title = empty($row_plugin_instance->definition['title']) ? t('Missing style plugin') : $row_plugin_instance->pluginTitle(); + + $options['row'] = array( + 'category' => 'format', + 'title' => t('Show'), + 'value' => $row_title, + 'setting' => $row_summary, + 'desc' => t('Change the way each row in the view is styled.'), + ); + // This adds a 'Settings' link to the row_options setting if the row style has options. + if ($row_plugin_instance->usesOptions()) { + $options['row']['links']['row_options'] = t('Change settings for this style'); + } + } + if ($this->usesAJAX()) { + $options['use_ajax'] = array( + 'category' => 'other', + 'title' => t('Use AJAX'), + 'value' => $this->getOption('use_ajax') ? t('Yes') : t('No'), + 'desc' => t('Change whether or not this display will use AJAX.'), + ); + } + if ($this->usesAttachments()) { + $options['hide_attachment_summary'] = array( + 'category' => 'other', + 'title' => t('Hide attachments in summary'), + 'value' => $this->getOption('hide_attachment_summary') ? t('Yes') : t('No'), + 'desc' => t('Change whether or not to display attachments when displaying a contextual filter summary.'), + ); + } + if (!isset($this->definition['contextual links locations']) || !empty($this->definition['contextual links locations'])) { + $options['hide_admin_links'] = array( + 'category' => 'other', + 'title' => t('Hide contextual links'), + 'value' => $this->getOption('hide_admin_links') ? t('Yes') : t('No'), + 'desc' => t('Change whether or not to display contextual links for this view.'), + ); + } + + $pager_plugin = $this->getPlugin('pager'); + if (!$pager_plugin) { + // default to the no pager plugin. + $pager_plugin = views_get_plugin('pager', 'none'); + } + + $pager_str = $pager_plugin->summaryTitle(); + + $options['pager'] = array( + 'category' => 'pager', + 'title' => t('Use pager'), + 'value' => $pager_plugin->pluginTitle(), + 'setting' => $pager_str, + 'desc' => t("Change this display's pager setting."), + ); + + // If pagers aren't allowed, change the text of the item: + if (!$this->usesPager()) { + $options['pager']['title'] = t('Items to display'); + } + + if ($pager_plugin->usesOptions()) { + $options['pager']['links']['pager_options'] = t('Change settings for this pager type.'); + } + + if ($this->usesMore()) { + $options['use_more'] = array( + 'category' => 'pager', + 'title' => t('More link'), + 'value' => $this->getOption('use_more') ? t('Yes') : t('No'), + 'desc' => t('Specify whether this display will provide a "more" link.'), + ); + } + + $this->view->initQuery(); + if ($this->view->query->get_aggregation_info()) { + $options['group_by'] = array( + 'category' => 'other', + 'title' => t('Use aggregation'), + 'value' => $this->getOption('group_by') ? t('Yes') : t('No'), + 'desc' => t('Allow grouping and aggregation (calculation) of fields.'), + ); + } + + $options['query'] = array( + 'category' => 'other', + 'title' => t('Query settings'), + 'value' => t('Settings'), + 'desc' => t('Allow to set some advanced settings for the query plugin'), + ); + + $languages = array( + '***CURRENT_LANGUAGE***' => t("Current user's language"), + '***DEFAULT_LANGUAGE***' => t("Default site language"), + LANGUAGE_NOT_SPECIFIED => t('Language neutral'), + ); + if (module_exists('language')) { + $languages = array_merge($languages, language_list()); + } + $options['field_langcode'] = array( + 'category' => 'other', + 'title' => t('Field Language'), + 'value' => $languages[$this->getOption('field_langcode')], + 'desc' => t('All fields which support translations will be displayed in the selected language.'), + ); + + $access_plugin = $this->getPlugin('access'); + if (!$access_plugin) { + // default to the no access control plugin. + $access_plugin = views_get_plugin('access', 'none'); + } + + $access_str = $access_plugin->summaryTitle(); + + $options['access'] = array( + 'category' => 'access', + 'title' => t('Access'), + 'value' => $access_plugin->pluginTitle(), + 'setting' => $access_str, + 'desc' => t('Specify access control type for this display.'), + ); + + if ($access_plugin->usesOptions()) { + $options['access']['links']['access_options'] = t('Change settings for this access type.'); + } + + $cache_plugin = $this->getPlugin('cache'); + if (!$cache_plugin) { + // default to the no cache control plugin. + $cache_plugin = views_get_plugin('cache', 'none'); + } + + $cache_str = $cache_plugin->summaryTitle(); + + $options['cache'] = array( + 'category' => 'other', + 'title' => t('Caching'), + 'value' => $cache_plugin->pluginTitle(), + 'setting' => $cache_str, + 'desc' => t('Specify caching type for this display.'), + ); + + if ($cache_plugin->usesOptions()) { + $options['cache']['links']['cache_options'] = t('Change settings for this caching type.'); + } + + if ($access_plugin->usesOptions()) { + $options['access']['links']['access_options'] = t('Change settings for this access type.'); + } + + if ($this->usesLinkDisplay()) { + $display_id = $this->getLinkDisplay(); + $link_display = empty($this->view->display[$display_id]) ? t('None') : check_plain($this->view->display[$display_id]['display_title']); + $link_display = $this->getOption('link_display') == 'custom_url' ? t('Custom URL') : $link_display; + $options['link_display'] = array( + 'category' => 'other', + 'title' => t('Link display'), + 'value' => $link_display, + 'desc' => t('Specify which display or custom url this display will link to.'), + ); + } + + if ($this->usesExposedFormInBlock()) { + $options['exposed_block'] = array( + 'category' => 'exposed', + 'title' => t('Exposed form in block'), + 'value' => $this->getOption('exposed_block') ? t('Yes') : t('No'), + 'desc' => t('Allow the exposed form to appear in a block instead of the view.'), + ); + } + + $exposed_form_plugin = $this->getPlugin('exposed_form'); + if (!$exposed_form_plugin) { + // default to the no cache control plugin. + $exposed_form_plugin = views_get_plugin('exposed_form', 'basic'); + } + + $exposed_form_str = $exposed_form_plugin->summaryTitle(); + + $options['exposed_form'] = array( + 'category' => 'exposed', + 'title' => t('Exposed form style'), + 'value' => $exposed_form_plugin->pluginTitle(), + 'setting' => $exposed_form_str, + 'desc' => t('Select the kind of exposed filter to use.'), + ); + + if ($exposed_form_plugin->usesOptions()) { + $options['exposed_form']['links']['exposed_form_options'] = t('Exposed form settings for this exposed form style.'); + } + + $css_class = check_plain(trim($this->getOption('css_class'))); + if (!$css_class) { + $css_class = t('None'); + } + + $options['css_class'] = array( + 'category' => 'other', + 'title' => t('CSS class'), + 'value' => $css_class, + 'desc' => t('Change the CSS class name(s) that will be added to this display.'), + ); + + $options['analyze-theme'] = array( + 'category' => 'other', + 'title' => t('Theme'), + 'value' => t('Information'), + 'desc' => t('Get information on how to theme this display'), + ); + + foreach ($this->extender as $extender) { + $extender->optionsSummary($categories, $options); + } + } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + if ($this->defaultableSections($form_state['section'])) { + views_ui_standard_display_dropdown($form, $form_state, $form_state['section']); + } + $form['#title'] = check_plain($this->display['display_title']) . ': '; + + // Set the 'section' to hilite on the form. + // If it's the item we're looking at is pulling from the default display, + // reflect that. Don't use is_defaulted since we want it to show up even + // on the default display. + if (!empty($this->options['defaults'][$form_state['section']])) { + $form['#section'] = 'default-' . $form_state['section']; + } + else { + $form['#section'] = $this->display['id'] . '-' . $form_state['section']; + } + + switch ($form_state['section']) { + case 'display_id': + $form['#title'] .= t('The machine name of this display'); + $form['display_id'] = array( + '#type' => 'textfield', + '#description' => t('This is machine name of the display.'), + '#default_value' => !empty($this->display['new_id']) ? $this->display['new_id'] : $this->display['id'], + '#required' => TRUE, + '#size' => 64, + ); + break; + case 'display_title': + $form['#title'] .= t('The name and the description of this display'); + $form['display_title'] = array( + '#title' => t('Name'), + '#type' => 'textfield', + '#description' => t('This name will appear only in the administrative interface for the View.'), + '#default_value' => $this->display['display_title'], + ); + $form['display_description'] = array( + '#title' => t('Description'), + '#type' => 'textfield', + '#description' => t('This description will appear only in the administrative interface for the View.'), + '#default_value' => $this->getOption('display_description'), + ); + break; + case 'display_comment': + $form['#title'] .= t("This display's comments"); + $form['display_comment'] = array( + '#type' => 'textarea', + '#description' => t('This value will be seen and used only within the Views UI and can be used to document this display. You can use this to provide notes for other or future maintainers of your site about how or why this display is configured.'), + '#default_value' => $this->getOption('display_comment'), + ); + break; + case 'title': + $form['#title'] .= t('The title of this view'); + $form['title'] = array( + '#type' => 'textfield', + '#description' => t('This title will be displayed with the view, wherever titles are normally displayed; i.e, as the page title, block title, etc.'), + '#default_value' => $this->getOption('title'), + ); + break; + case 'css_class': + $form['#title'] .= t('CSS class'); + $form['css_class'] = array( + '#type' => 'textfield', + '#description' => t('The CSS class names will be added to the view. This enables you to use specific CSS code for each view. You may define multiples classes separated by spaces.'), + '#default_value' => $this->getOption('css_class'), + ); + break; + case 'use_ajax': + $form['#title'] .= t('Use AJAX when available to load this view'); + $form['description'] = array( + '#markup' => '<div class="description form-item">' . t('If set, this view will use an AJAX mechanism for paging, table sorting and exposed filters. This means the entire page will not refresh. It is not recommended that you use this if this view is the main content of the page as it will prevent deep linking to specific pages, but it is very useful for side content.') . '</div>', + ); + $form['use_ajax'] = array( + '#type' => 'radios', + '#options' => array(1 => t('Yes'), 0 => t('No')), + '#default_value' => $this->getOption('use_ajax') ? 1 : 0, + ); + break; + case 'hide_attachment_summary': + $form['#title'] .= t('Hide attachments when displaying a contextual filter summary'); + $form['hide_attachment_summary'] = array( + '#type' => 'radios', + '#options' => array(1 => t('Yes'), 0 => t('No')), + '#default_value' => $this->getOption('hide_attachment_summary') ? 1 : 0, + ); + break; + case 'hide_admin_links': + $form['#title'] .= t('Hide contextual links on this view.'); + $form['hide_admin_links'] = array( + '#type' => 'radios', + '#options' => array(1 => t('Yes'), 0 => t('No')), + '#default_value' => $this->getOption('hide_admin_links') ? 1 : 0, + ); + break; + case 'use_more': + $form['#title'] .= t('Add a more link to the bottom of the display.'); + $form['use_more'] = array( + '#type' => 'checkbox', + '#title' => t('Create more link'), + '#description' => t("This will add a more link to the bottom of this view, which will link to the page view. If you have more than one page view, the link will point to the display specified in 'Link display' section under advanced. You can override the url at the link display setting."), + '#default_value' => $this->getOption('use_more'), + ); + $form['use_more_always'] = array( + '#type' => 'checkbox', + '#title' => t("Display 'more' link only if there is more content"), + '#description' => t("Leave this unchecked to display the 'more' link even if there are no more items to display."), + '#default_value' => !$this->getOption('use_more_always'), + '#states' => array( + 'visible' => array( + ':input[name="use_more"]' => array('checked' => TRUE), + ), + ), + ); + $form['use_more_text'] = array( + '#type' => 'textfield', + '#title' => t('More link text'), + '#description' => t("The text to display for the more link."), + '#default_value' => $this->getOption('useMoreText'), + '#states' => array( + 'visible' => array( + ':input[name="use_more"]' => array('checked' => TRUE), + ), + ), + ); + break; + case 'group_by': + $form['#title'] .= t('Allow grouping and aggregation (calculation) of fields.'); + $form['group_by'] = array( + '#type' => 'checkbox', + '#title' => t('Aggregate'), + '#description' => t('If enabled, some fields may become unavailable. All fields that are selected for grouping will be collapsed to one record per distinct value. Other fields which are selected for aggregation will have the function run on them. For example, you can group nodes on title and count the number of nids in order to get a list of duplicate titles.'), + '#default_value' => $this->getOption('group_by'), + ); + break; + case 'access': + $form['#title'] .= t('Access restrictions'); + $form['access'] = array( + '#prefix' => '<div class="clearfix">', + '#suffix' => '</div>', + '#tree' => TRUE, + ); + + $access = $this->getOption('access'); + $form['access']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('access', NULL, array($this->view->storage->base_table)), + '#default_value' => $access['type'], + ); + + $access_plugin = $this->getPlugin('access'); + if ($access_plugin->usesOptions()) { + $form['markup'] = array( + '#prefix' => '<div class="form-item description">', + '#markup' => t('You may also adjust the !settings for the currently selected access restriction.', array('!settings' => $this->optionLink(t('settings'), 'access_options'))), + '#suffix' => '</div>', + ); + } + + break; + case 'access_options': + $plugin = $this->getPlugin('access'); + $form['#title'] .= t('Access options'); + if ($plugin) { + $form['access_options'] = array( + '#tree' => TRUE, + ); + $plugin->buildOptionsForm($form['access_options'], $form_state); + } + break; + case 'cache': + $form['#title'] .= t('Caching'); + $form['cache'] = array( + '#prefix' => '<div class="clearfix">', + '#suffix' => '</div>', + '#tree' => TRUE, + ); + + $cache = $this->getOption('cache'); + $form['cache']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('cache', NULL, array($this->view->storage->base_table)), + '#default_value' => $cache['type'], + ); + + $cache_plugin = $this->getPlugin('cache'); + if ($cache_plugin->usesOptions()) { + $form['markup'] = array( + '#prefix' => '<div class="form-item description">', + '#suffix' => '</div>', + '#markup' => t('You may also adjust the !settings for the currently selected cache mechanism.', array('!settings' => $this->optionLink(t('settings'), 'cache_options'))), + ); + } + break; + case 'cache_options': + $plugin = $this->getPlugin('cache'); + $form['#title'] .= t('Caching options'); + if ($plugin) { + $form['cache_options'] = array( + '#tree' => TRUE, + ); + $plugin->buildOptionsForm($form['cache_options'], $form_state); + } + break; + case 'query': + $query_options = $this->getOption('query'); + $plugin_name = $query_options['type']; + + $form['#title'] .= t('Query options'); + $this->view->initQuery(); + if ($this->view->query) { + $form['query'] = array( + '#tree' => TRUE, + 'type' => array( + '#type' => 'value', + '#value' => $plugin_name, + ), + 'options' => array( + '#tree' => TRUE, + ), + ); + + $this->view->query->buildOptionsForm($form['query']['options'], $form_state); + } + break; + case 'field_language': + $form['#title'] .= t('Field Language'); + + $entities = entity_get_info(); + $entity_tables = array(); + $has_translation_handlers = FALSE; + foreach ($entities as $type => $entity_info) { + $entity_tables[] = $entity_info['base table']; + + if (!empty($entity_info['translation'])) { + $has_translation_handlers = TRUE; + } + } + + // Doesn't make sense to show a field setting here if we aren't querying + // an entity base table. Also, we make sure that there's at least one + // entity type with a translation handler attached. + if (in_array($this->view->storage->base_table, $entity_tables) && $has_translation_handlers) { + $languages = array( + '***CURRENT_LANGUAGE***' => t("Current user's language"), + '***DEFAULT_LANGUAGE***' => t("Default site language"), + LANGUAGE_NOT_SPECIFIED => t('Language neutral'), + ); + $languages = array_merge($languages, views_language_list()); + + $form['field_langcode'] = array( + '#type' => 'select', + '#title' => t('Field Language'), + '#description' => t('All fields which support translations will be displayed in the selected language.'), + '#options' => $languages, + '#default_value' => $this->getOption('field_langcode'), + ); + $form['field_langcode_add_to_query'] = array( + '#type' => 'checkbox', + '#title' => t('When needed, add the field language condition to the query'), + '#default_value' => $this->getOption('field_langcode_add_to_query'), + ); + } + else { + $form['field_language']['#markup'] = t("You don't have translatable entity types."); + } + break; + case 'style': + $form['#title'] .= t('How should this view be styled'); + $style_plugin = $this->getPlugin('style'); + $form['style'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('style', $this->getStyleType(), array($this->view->storage->base_table)), + '#default_value' => $style_plugin->definition['id'], + '#description' => t('If the style you choose has settings, be sure to click the settings button that will appear next to it in the View summary.'), + ); + + if ($style_plugin->usesOptions()) { + $form['markup'] = array( + '#prefix' => '<div class="form-item description">', + '#suffix' => '</div>', + '#markup' => t('You may also adjust the !settings for the currently selected style.', array('!settings' => $this->optionLink(t('settings'), 'style_options'))), + ); + } + + break; + case 'style_options': + $form['#title'] .= t('Style options'); + $style = TRUE; + $style_plugin = $this->getOption('style'); + $name = $style_plugin['type']; + + case 'row_options': + if (!isset($name)) { + $row_plugin = $this->getOption('row'); + $name = $row_plugin['type']; + } + // if row, $style will be empty. + if (empty($style)) { + $form['#title'] .= t('Row style options'); + } + $plugin = $this->getPlugin(empty($style) ? 'row' : 'style', $name); + if ($plugin) { + $form[$form_state['section']] = array( + '#tree' => TRUE, + ); + $plugin->buildOptionsForm($form[$form_state['section']], $form_state); + } + break; + case 'row': + $form['#title'] .= t('How should each row in this view be styled'); + $row_plugin_instance = $this->getPlugin('row'); + $form['row'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('row', $this->getStyleType(), array($this->view->storage->base_table)), + '#default_value' => $row_plugin_instance->definition['id'], + ); + + if ($row_plugin_instance->usesOptions()) { + $form['markup'] = array( + '#prefix' => '<div class="form-item description">', + '#suffix' => '</div>', + '#markup' => t('You may also adjust the !settings for the currently selected row style.', array('!settings' => $this->optionLink(t('settings'), 'row_options'))), + ); + } + + break; + case 'link_display': + $form['#title'] .= t('Which display to use for path'); + foreach ($this->view->storage->display as $display_id => $display) { + if ($this->view->displayHandlers[$display_id]->hasPath()) { + $options[$display_id] = $display['display_title']; + } + } + $options['custom_url'] = t('Custom URL'); + if (count($options)) { + $form['link_display'] = array( + '#type' => 'radios', + '#options' => $options, + '#description' => t("Which display to use to get this display's path for things like summary links, rss feed links, more links, etc."), + '#default_value' => $this->getOption('link_display'), + ); + } + + $options = array(); + $count = 0; // This lets us prepare the key as we want it printed. + foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) { + $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); + $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); + } + + // Default text. + // We have some options, so make a list. + $output = ''; + if (!empty($options)) { + $output = t('<p>The following tokens are available for this link.</p>'); + foreach (array_keys($options) as $type) { + if (!empty($options[$type])) { + $items = array(); + foreach ($options[$type] as $key => $value) { + $items[] = $key . ' == ' . $value; + } + $output .= theme('item_list', + array( + 'items' => $items, + 'type' => $type + )); + } + } + } + + $form['link_url'] = array( + '#type' => 'textfield', + '#title' => t('Custom URL'), + '#default_value' => $this->getOption('link_url'), + '#description' => t('A Drupal path or external URL the more link will point to. Note that this will override the link display setting above.') . $output, + '#states' => array( + 'visible' => array( + ':input[name="link_display"]' => array('value' => 'custom_url'), + ), + ), + ); + break; + case 'analyze-theme': + $form['#title'] .= t('Theming information'); + if ($theme = drupal_container()->get('request')->request->get('theme')) { + $this->theme = $theme; + } + elseif (empty($this->theme)) { + $this->theme = variable_get('theme_default', 'bartik'); + } + + if (isset($GLOBALS['theme']) && $GLOBALS['theme'] == $this->theme) { + $this->theme_registry = theme_get_registry(); + $theme_engine = $GLOBALS['theme_engine']; + } + else { + $themes = list_themes(); + $theme = $themes[$this->theme]; + + // Find all our ancestor themes and put them in an array. + $base_theme = array(); + $ancestor = $this->theme; + while ($ancestor && isset($themes[$ancestor]->base_theme)) { + $ancestor = $themes[$ancestor]->base_theme; + $base_theme[] = $themes[$ancestor]; + } + + // The base themes should be initialized in the right order. + $base_theme = array_reverse($base_theme); + + // This code is copied directly from _drupal_theme_initialize() + $theme_engine = NULL; + + // Initialize the theme. + if (isset($theme->engine)) { + // Include the engine. + include_once DRUPAL_ROOT . '/' . $theme->owner; + + $theme_engine = $theme->engine; + if (function_exists($theme_engine . '_init')) { + foreach ($base_theme as $base) { + call_user_func($theme_engine . '_init', $base); + } + call_user_func($theme_engine . '_init', $theme); + } + } + else { + // include non-engine theme files + foreach ($base_theme as $base) { + // Include the theme file or the engine. + if (!empty($base->owner)) { + include_once DRUPAL_ROOT . '/' . $base->owner; + } + } + // and our theme gets one too. + if (!empty($theme->owner)) { + include_once DRUPAL_ROOT . '/' . $theme->owner; + } + } + $this->theme_registry = _theme_load_registry($theme, $base_theme, $theme_engine); + } + + // If there's a theme engine involved, we also need to know its extension + // so we can give the proper filename. + $this->theme_extension = '.tpl.php'; + if (isset($theme_engine)) { + $extension_function = $theme_engine . '_extension'; + if (function_exists($extension_function)) { + $this->theme_extension = $extension_function(); + } + } + + $funcs = array(); + // Get theme functions for the display. Note that some displays may + // not have themes. The 'feed' display, for example, completely + // delegates to the style. + if (!empty($this->definition['theme'])) { + $funcs[] = $this->optionLink(t('Display output'), 'analyze-theme-display') . ': ' . $this->formatThemes($this->themeFunctions()); + $themes = $this->additionalThemeFunctions(); + if ($themes) { + foreach ($themes as $theme) { + $funcs[] = $this->optionLink(t('Alternative display output'), 'analyze-theme-display') . ': ' . $this->formatThemes($theme); + } + } + } + + $plugin = $this->getPlugin('style'); + if ($plugin) { + $funcs[] = $this->optionLink(t('Style output'), 'analyze-theme-style') . ': ' . $this->formatThemes($plugin->themeFunctions(), $plugin->additionalThemeFunctions()); + $themes = $plugin->additionalThemeFunctions(); + if ($themes) { + foreach ($themes as $theme) { + $funcs[] = $this->optionLink(t('Alternative style'), 'analyze-theme-style') . ': ' . $this->formatThemes($theme); + } + } + + if ($plugin->usesRowPlugin()) { + $row_plugin = $this->getPlugin('row'); + if ($row_plugin) { + $funcs[] = $this->optionLink(t('Row style output'), 'analyze-theme-row') . ': ' . $this->formatThemes($row_plugin->themeFunctions()); + $themes = $row_plugin->additionalThemeFunctions(); + if ($themes) { + foreach ($themes as $theme) { + $funcs[] = $this->optionLink(t('Alternative row style'), 'analyze-theme-row') . ': ' . $this->formatThemes($theme); + } + } + } + } + + if ($plugin->usesFields()) { + foreach ($this->getHandlers('field') as $id => $handler) { + $funcs[] = $this->optionLink(t('Field @field (ID: @id)', array('@field' => $handler->adminLabel(), '@id' => $id)), 'analyze-theme-field') . ': ' . $this->formatThemes($handler->themeFunctions()); + } + } + } + + $form['important'] = array( + '#markup' => '<div class="form-item description"><p>' . t('This section lists all possible templates for the display plugin and for the style plugins, ordered roughly from the least specific to the most specific. The active template for each plugin -- which is the most specific template found on the system -- is highlighted in bold.') . '</p></div>', + ); + + if (isset($this->view->display_handler->new_id)) { + $form['important']['new_id'] = array( + '#prefix' => '<div class="description">', + '#suffix' => '</div>', + '#value' => t("<strong>Important!</strong> You have changed the display's machine name. Anything that attached to this display specifically, such as theming, may stop working until it is updated. To see theme suggestions for it, you need to save the view."), + ); + } + + foreach (list_themes() as $key => $theme) { + if (!empty($theme->info['hidden'])) { + continue; + } + $options[$key] = $theme->info['name']; + } + + $form['box'] = array( + '#prefix' => '<div class="container-inline">', + '#suffix' => '</div>', + ); + $form['box']['theme'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => $this->theme, + ); + + $form['box']['change'] = array( + '#type' => 'submit', + '#value' => t('Change theme'), + '#submit' => array('views_ui_edit_display_form_change_theme'), + ); + + $form['analysis'] = array( + '#markup' => '<div class="form-item">' . theme('item_list', array('items' => $funcs)) . '</div>', + ); + + $form['rescan_button'] = array( + '#prefix' => '<div class="form-item">', + '#suffix' => '</div>', + ); + $form['rescan_button']['button'] = array( + '#type' => 'submit', + '#value' => t('Rescan template files'), + '#submit' => array('views_ui_config_item_form_rescan'), + ); + $form['rescan_button']['markup'] = array( + '#markup' => '<div class="description">' . t("<strong>Important!</strong> When adding, removing, or renaming template files, it is necessary to make Drupal aware of the changes by making it rescan the files on your system. By clicking this button you clear Drupal's theme registry and thereby trigger this rescanning process. The highlighted templates above will then reflect the new state of your system.") . '</div>', + ); + + $form_state['ok_button'] = TRUE; + break; + case 'analyze-theme-display': + $form['#title'] .= t('Theming information (display)'); + $output = '<p>' . t('Back to !info.', array('!info' => $this->optionLink(t('theming information'), 'analyze-theme'))) . '</p>'; + + if (empty($this->definition['theme'])) { + $output .= t('This display has no theming information'); + } + else { + $output .= '<p>' . t('This is the default theme template used for this display.') . '</p>'; + $output .= '<pre>' . check_plain(file_get_contents('./' . $this->definition['theme path'] . '/' . strtr($this->definition['theme'], '_', '-') . '.tpl.php')) . '</pre>'; + } + + if (!empty($this->definition['additional themes'])) { + foreach ($this->definition['additional themes'] as $theme => $type) { + $output .= '<p>' . t('This is an alternative template for this display.') . '</p>'; + $output .= '<pre>' . check_plain(file_get_contents('./' . $this->definition['theme path'] . '/' . strtr($theme, '_', '-') . '.tpl.php')) . '</pre>'; + } + } + + $form['analysis'] = array( + '#markup' => '<div class="form-item">' . $output . '</div>', + ); + + $form_state['ok_button'] = TRUE; + break; + case 'analyze-theme-style': + $form['#title'] .= t('Theming information (style)'); + $output = '<p>' . t('Back to !info.', array('!info' => $this->optionLink(t('theming information'), 'analyze-theme'))) . '</p>'; + + $plugin = $this->getPlugin('style'); + + if (empty($plugin->definition['theme'])) { + $output .= t('This display has no style theming information'); + } + else { + $output .= '<p>' . t('This is the default theme template used for this style.') . '</p>'; + $output .= '<pre>' . check_plain(file_get_contents('./' . $plugin->definition['theme path'] . '/' . strtr($plugin->definition['theme'], '_', '-') . '.tpl.php')) . '</pre>'; + } + + if (!empty($plugin->definition['additional themes'])) { + foreach ($plugin->definition['additional themes'] as $theme => $type) { + $output .= '<p>' . t('This is an alternative template for this style.') . '</p>'; + $output .= '<pre>' . check_plain(file_get_contents('./' . $plugin->definition['theme path'] . '/' . strtr($theme, '_', '-') . '.tpl.php')) . '</pre>'; + } + } + + $form['analysis'] = array( + '#markup' => '<div class="form-item">' . $output . '</div>', + ); + + $form_state['ok_button'] = TRUE; + break; + case 'analyze-theme-row': + $form['#title'] .= t('Theming information (row style)'); + $output = '<p>' . t('Back to !info.', array('!info' => $this->optionLink(t('theming information'), 'analyze-theme'))) . '</p>'; + + $plugin = $this->getPlugin('row'); + + if (empty($plugin->definition['theme'])) { + $output .= t('This display has no row style theming information'); + } + else { + $output .= '<p>' . t('This is the default theme template used for this row style.') . '</p>'; + $output .= '<pre>' . check_plain(file_get_contents('./' . $plugin->definition['theme path'] . '/' . strtr($plugin->definition['theme'], '_', '-') . '.tpl.php')) . '</pre>'; + } + + if (!empty($plugin->definition['additional themes'])) { + foreach ($plugin->definition['additional themes'] as $theme => $type) { + $output .= '<p>' . t('This is an alternative template for this row style.') . '</p>'; + $output .= '<pre>' . check_plain(file_get_contents('./' . $plugin->definition['theme path'] . '/' . strtr($theme, '_', '-') . '.tpl.php')) . '</pre>'; + } + } + + $form['analysis'] = array( + '#markup' => '<div class="form-item">' . $output . '</div>', + ); + + $form_state['ok_button'] = TRUE; + break; + case 'analyze-theme-field': + $form['#title'] .= t('Theming information (row style)'); + $output = '<p>' . t('Back to !info.', array('!info' => $this->optionLink(t('theming information'), 'analyze-theme'))) . '</p>'; + + $output .= '<p>' . t('This is the default theme template used for this row style.') . '</p>'; + + // Field templates aren't registered the normal way...and they're always + // this one, anyhow. + $output .= '<pre>' . check_plain(file_get_contents(drupal_get_path('module', 'views') . '/theme/views-view-field.tpl.php')) . '</pre>'; + + $form['analysis'] = array( + '#markup' => '<div class="form-item">' . $output . '</div>', + ); + $form_state['ok_button'] = TRUE; + break; + + case 'exposed_block': + $form['#title'] .= t('Put the exposed form in a block'); + $form['description'] = array( + '#markup' => '<div class="description form-item">' . t('If set, any exposed widgets will not appear with this view. Instead, a block will be made available to the Drupal block administration system, and the exposed form will appear there. Note that this block must be enabled manually, Views will not enable it for you.') . '</div>', + ); + $form['exposed_block'] = array( + '#type' => 'radios', + '#options' => array(1 => t('Yes'), 0 => t('No')), + '#default_value' => $this->getOption('exposed_block') ? 1 : 0, + ); + break; + case 'exposed_form': + $form['#title'] .= t('Exposed Form'); + $form['exposed_form'] = array( + '#prefix' => '<div class="clearfix">', + '#suffix' => '</div>', + '#tree' => TRUE, + ); + + $exposed_form = $this->getOption('exposed_form'); + $form['exposed_form']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('exposed_form', NULL, array($this->view->storage->base_table)), + '#default_value' => $exposed_form['type'], + ); + + $exposed_form_plugin = $this->getPlugin('exposed_form'); + if ($exposed_form_plugin->usesOptions()) { + $form['markup'] = array( + '#prefix' => '<div class="form-item description">', + '#suffix' => '</div>', + '#markup' => t('You may also adjust the !settings for the currently selected style.', array('!settings' => $this->optionLink(t('settings'), 'exposed_form_options'))), + ); + } + break; + case 'exposed_form_options': + $plugin = $this->getPlugin('exposed_form'); + $form['#title'] .= t('Exposed form options'); + if ($plugin) { + $form['exposed_form_options'] = array( + '#tree' => TRUE, + ); + $plugin->buildOptionsForm($form['exposed_form_options'], $form_state); + } + break; + case 'pager': + $form['#title'] .= t('Select which pager, if any, to use for this view'); + $form['pager'] = array( + '#prefix' => '<div class="clearfix">', + '#suffix' => '</div>', + '#tree' => TRUE, + ); + + $pager = $this->getOption('pager'); + $form['pager']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('pager', !$this->usesPager() ? 'basic' : NULL, array($this->view->storage->base_table)), + '#default_value' => $pager['type'], + ); + + $pager_plugin = $this->getPlugin('pager'); + if ($pager_plugin->usesOptions()) { + $form['markup'] = array( + '#prefix' => '<div class="form-item description">', + '#suffix' => '</div>', + '#markup' => t('You may also adjust the !settings for the currently selected pager.', array('!settings' => $this->optionLink(t('settings'), 'pager_options'))), + ); + } + + break; + case 'pager_options': + $plugin = $this->getPlugin('pager'); + $form['#title'] .= t('Pager options'); + if ($plugin) { + $form['pager_options'] = array( + '#tree' => TRUE, + ); + $plugin->buildOptionsForm($form['pager_options'], $form_state); + } + break; + } + + foreach ($this->extender as $extender) { + $extender->buildOptionsForm($form, $form_state); + } + } + + /** + * Format a list of theme templates for output by the theme info helper. + */ + protected function formatThemes($themes) { + $registry = $this->theme_registry; + $extension = $this->theme_extension; + + $output = ''; + $picked = FALSE; + foreach ($themes as $theme) { + $template = strtr($theme, '_', '-') . $extension; + if (!$picked && !empty($registry[$theme])) { + $template_path = isset($registry[$theme]['path']) ? $registry[$theme]['path'] . '/' : './'; + if (file_exists($template_path . $template)) { + $hint = t('File found in folder @template-path', array('@template-path' => $template_path)); + $template = '<strong title="'. $hint .'">' . $template . '</strong>'; + } + else { + $template = '<strong class="error">' . $template . ' ' . t('(File not found, in folder @template-path)', array('@template-path' => $template_path)) . '</strong>'; + } + $picked = TRUE; + } + $fixed[] = $template; + } + + return theme('item_list', array('items' => array_reverse($fixed))); + } + + /** + * Validate the options form. + */ + public function validateOptionsForm(&$form, &$form_state) { + switch ($form_state['section']) { + case 'display_title': + if (empty($form_state['values']['display_title'])) { + form_error($form['display_title'], t('Display title may not be empty.')); + } + break; + case 'css_class': + $css_class = $form_state['values']['css_class']; + if (preg_match('/[^a-zA-Z0-9-_ ]/', $css_class)) { + form_error($form['css_class'], t('CSS classes must be alphanumeric or dashes only.')); + } + break; + case 'display_id': + if ($form_state['values']['display_id']) { + if (preg_match('/[^a-z0-9_]/', $form_state['values']['display_id'])) { + form_error($form['display_id'], t('Display name must be letters, numbers, or underscores only.')); + } + + foreach ($this->view->display as $id => $display) { + if ($id != $this->view->current_display && ($form_state['values']['display_id'] == $id || (isset($display->new_id) && $form_state['values']['display_id'] == $display->new_id))) { + form_error($form['display_id'], t('Display id should be unique.')); + } + } + } + break; + case 'style_options': + $style = TRUE; + case 'row_options': + // if row, $style will be empty. + $plugin = $this->getPlugin(empty($style) ? 'row' : 'style'); + if ($plugin) { + $plugin->validateOptionsForm($form[$form_state['section']], $form_state); + } + break; + case 'access_options': + $plugin = $this->getPlugin('access'); + if ($plugin) { + $plugin->validateOptionsForm($form['access_options'], $form_state); + } + break; + case 'query': + if ($this->view->query) { + $this->view->query->validateOptionsForm($form['query'], $form_state); + } + break; + case 'cache_options': + $plugin = $this->getPlugin('cache'); + if ($plugin) { + $plugin->validateOptionsForm($form['cache_options'], $form_state); + } + break; + case 'exposed_form_options': + $plugin = $this->getPlugin('exposed_form'); + if ($plugin) { + $plugin->validateOptionsForm($form['exposed_form_options'], $form_state); + } + break; + case 'pager_options': + $plugin = $this->getPlugin('pager'); + if ($plugin) { + $plugin->validateOptionsForm($form['pager_options'], $form_state); + } + break; + } + + foreach ($this->extender as $extender) { + $extender->validateOptionsForm($form, $form_state); + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitOptionsForm(&$form, &$form_state) { + // Not sure I like this being here, but it seems (?) like a logical place. + $cache_plugin = $this->getPlugin('cache'); + if ($cache_plugin) { + $cache_plugin->cache_flush(); + } + + $section = $form_state['section']; + switch ($section) { + case 'display_id': + if (isset($form_state['values']['display_id'])) { + $this->display['new_id'] = $form_state['values']['display_id']; + } + break; + case 'display_title': + $this->display['display_title'] = $form_state['values']['display_title']; + $this->setOption('display_description', $form_state['values']['display_description']); + break; + case 'access': + $access = $this->getOption('access'); + if ($access['type'] != $form_state['values']['access']['type']) { + $plugin = views_get_plugin('access', $form_state['values']['access']['type']); + if ($plugin) { + $access = array('type' => $form_state['values']['access']['type']); + $this->setOption('access', $access); + if ($plugin->usesOptions()) { + $this->view->addFormToStack('display', $this->display['id'], array('access_options')); + } + } + } + + break; + case 'access_options': + $plugin = $this->getPlugin('access'); + if ($plugin) { + $access = $this->getOption('access'); + $plugin->submitOptionsForm($form['access_options'], $form_state); + $access['options'] = $form_state['values'][$section]; + $this->setOption('access', $access); + } + break; + case 'cache': + $cache = $this->getOption('cache'); + if ($cache['type'] != $form_state['values']['cache']['type']) { + $plugin = views_get_plugin('cache', $form_state['values']['cache']['type']); + if ($plugin) { + $cache = array('type' => $form_state['values']['cache']['type']); + $this->setOption('cache', $cache); + if ($plugin->usesOptions()) { + $this->view->addFormToStack('display', $this->display['id'], array('cache_options')); + } + } + } + + break; + case 'cache_options': + $plugin = $this->getPlugin('cache'); + if ($plugin) { + $cache = $this->getOption('cache'); + $plugin->submitOptionsForm($form['cache_options'], $form_state); + $cache['options'] = $form_state['values'][$section]; + $this->setOption('cache', $cache); + } + break; + case 'query': + $plugin = $this->get_plugin('query'); + if ($plugin) { + $plugin->submitOptionsForm($form['query']['options'], $form_state); + $this->setOption('query', $form_state['values'][$section]); + } + break; + + case 'link_display': + $this->setOption('link_url', $form_state['values']['link_url']); + case 'title': + case 'css_class': + case 'display_comment': + $this->setOption($section, $form_state['values'][$section]); + break; + case 'field_language': + $this->setOption('field_langcode', $form_state['values']['field_langcode']); + $this->setOption('field_langcode_add_to_query', $form_state['values']['field_langcode_add_to_query']); + break; + case 'use_ajax': + case 'hide_attachment_summary': + case 'hide_admin_links': + $this->setOption($section, (bool)$form_state['values'][$section]); + break; + case 'use_more': + $this->setOption($section, intval($form_state['values'][$section])); + $this->setOption('use_more_always', !intval($form_state['values']['use_more_always'])); + $this->setOption('use_more_text', $form_state['values']['use_more_text']); + case 'distinct': + $this->setOption($section, $form_state['values'][$section]); + break; + case 'group_by': + $this->setOption($section, $form_state['values'][$section]); + break; + case 'row': + // This if prevents resetting options to default if they don't change + // the plugin. + $row = $this->getOption('row'); + if ($row['type'] != $form_state['values'][$section]) { + $plugin = views_get_plugin('row', $form_state['values'][$section]); + if ($plugin) { + $row = array('type' => $form_state['values'][$section]); + $this->setOption($section, $row); + + // send ajax form to options page if we use it. + if ($plugin->usesOptions()) { + $this->view->addFormToStack('display', $this->display['id'], array('row_options')); + } + } + } + break; + case 'style': + // This if prevents resetting options to default if they don't change + // the plugin. + $style = $this->getOption('style'); + if ($style['type'] != $form_state['values'][$section]) { + $plugin = views_get_plugin('style', $form_state['values'][$section]); + if ($plugin) { + $row = array('type' => $form_state['values'][$section]); + $this->setOption($section, $row); + // send ajax form to options page if we use it. + if ($plugin->usesOptions()) { + $this->view->addFormToStack('display', $this->display['id'], array('style_options')); + } + } + } + break; + case 'style_options': + $plugin = $this->getPlugin('style'); + if ($plugin) { + $style = $this->getOption('style'); + $plugin->submitOptionsForm($form['style_options'], $form_state); + $style['options'] = $form_state['values'][$section]; + $this->setOption('style', $style); + } + break; + case 'row_options': + $plugin = $this->getPlugin('row'); + if ($plugin) { + $row = $this->getOption('row'); + $plugin->submitOptionsForm($form['row_options'], $form_state); + $row['options'] = $form_state['values'][$section]; + $this->setOption('row', $row); + } + break; + case 'exposed_block': + $this->setOption($section, (bool) $form_state['values'][$section]); + break; + case 'exposed_form': + $exposed_form = $this->getOption('exposed_form'); + if ($exposed_form['type'] != $form_state['values']['exposed_form']['type']) { + $plugin = views_get_plugin('exposed_form', $form_state['values']['exposed_form']['type']); + if ($plugin) { + $exposed_form = array('type' => $form_state['values']['exposed_form']['type'], 'options' => array()); + $this->setOption('exposed_form', $exposed_form); + if ($plugin->usesOptions()) { + $this->view->addFormToStack('display', $this->display['id'], array('exposed_form_options')); + } + } + } + + break; + case 'exposed_form_options': + $plugin = $this->getPlugin('exposed_form'); + if ($plugin) { + $exposed_form = $this->getOption('exposed_form'); + $plugin->submitOptionsForm($form['exposed_form_options'], $form_state); + $exposed_form['options'] = $form_state['values'][$section]; + $this->setOption('exposed_form', $exposed_form); + } + break; + case 'pager': + $pager = $this->getOption('pager'); + if ($pager['type'] != $form_state['values']['pager']['type']) { + $plugin = views_get_plugin('pager', $form_state['values']['pager']['type']); + if ($plugin) { + // Because pagers have very similar options, let's allow pagers to + // try to carry the options over. + $plugin->init($this->view, $this->display, $pager['options']); + + $pager = array('type' => $form_state['values']['pager']['type'], 'options' => $plugin->options); + $this->setOption('pager', $pager); + if ($plugin->usesOptions()) { + $this->view->addFormToStack('display', $this->display['id'], array('pager_options')); + } + } + } + + break; + case 'pager_options': + $plugin = $this->getPlugin('pager'); + if ($plugin) { + $pager = $this->getOption('pager'); + $plugin->submitOptionsForm($form['pager_options'], $form_state); + $pager['options'] = $form_state['values'][$section]; + $this->setOption('pager', $pager); + } + break; + } + + foreach ($this->extender as $extender) { + $extender->submitOptionsForm($form, $form_state); + } + } + + /** + * If override/revert was clicked, perform the proper toggle. + */ + public function optionsOverride($form, &$form_state) { + $this->setOverride($form_state['section']); + } + + /** + * Flip the override setting for the given section. + * + * @param string $section + * Which option should be marked as overridden, for example "filters". + * @param bool $new_state + * Select the new state of the option. + * - TRUE: Revert to default. + * - FALSE: Mark it as overridden. + */ + public function setOverride($section, $new_state = NULL) { + $options = $this->defaultableSections($section); + if (!$options) { + return; + } + + if (!isset($new_state)) { + $new_state = empty($this->options['defaults'][$section]); + } + + // For each option that is part of this group, fix our settings. + foreach ($options as $option) { + if ($new_state) { + // Revert to defaults. + unset($this->options[$option]); + unset($this->display['display_options'][$option]); + } + else { + // copy existing values into our display. + $this->options[$option] = $this->getOption($option); + $this->display['display_options'][$option] = $this->options[$option]; + } + $this->options['defaults'][$option] = $new_state; + $this->display['display_options']['defaults'][$option] = $new_state; + } + } + + /** + * Inject anything into the query that the display handler needs. + */ + public function query() { + foreach ($this->extender as $extender) { + $extender->query(); + } + } + + /** + * Not all display plugins will support filtering + * + * @todo this doesn't seems to be used + */ + public function renderFilters() { } + + /** + * Not all display plugins will suppert pager rendering. + */ + public function renderPager() { + return TRUE; + } + + /** + * Render the 'more' link + */ + public function renderMoreLink() { + if ($this->usesMore() && ($this->useMoreAlways() || (!empty($this->view->pager) && $this->view->pager->has_more_records()))) { + $path = $this->getPath(); + + if ($this->getOption('link_display') == 'custom_url' && $override_path = $this->getOption('link_url')) { + $tokens = $this->getArgumentsTokens(); + $path = strtr($override_path, $tokens); + } + + if ($path) { + if (empty($override_path)) { + $path = $this->view->getUrl(NULL, $path); + } + $url_options = array(); + if (!empty($this->view->exposed_raw_input)) { + $url_options['query'] = $this->view->exposed_raw_input; + } + $theme = views_theme_functions('views_more', $this->view, $this->view->display_handler->display); + $path = check_url(url($path, $url_options)); + + return theme($theme, array('more_url' => $path, 'link_text' => check_plain($this->useMoreText()), 'view' => $this->view)); + } + } + } + + + /** + * Legacy functions. + */ + + /** + * Render the header of the view. + */ + public function renderHeader() { + $empty = !empty($this->view->result); + return $this->renderArea('header', $empty); + } + + /** + * Render the footer of the view. + */ + public function renderFooter() { + $empty = !empty($this->view->result); + return $this->renderArea('footer', $empty); + } + + public function renderEmpty() { + return $this->renderArea('empty'); + } + + /** + * If this display creates a block, implement one of these. + */ + public function hookBlockList($delta = 0, $edit = array()) { return array(); } + + /** + * If this display creates a page with a menu item, implement it here. + */ + public function hookMenu() { return array(); } + + /** + * Render this display. + */ + public function render() { + return theme($this->themeFunctions(), array('view' => $this->view)); + } + + public function renderArea($area, $empty = FALSE) { + $return = ''; + foreach ($this->getHandlers($area) as $area) { + $return .= $area->render($empty); + } + return $return; + } + + + /** + * Determine if the user has access to this display of the view. + */ + public function access($account = NULL) { + if (!isset($account)) { + global $user; + $account = $user; + } + + // Full override. + if (user_access('access all views', $account)) { + return TRUE; + } + + $plugin = $this->getPlugin('access'); + if ($plugin) { + return $plugin->access($account); + } + + // fallback to all access if no plugin. + return TRUE; + } + + /** + * Set up any variables on the view prior to execution. These are separated + * from execute because they are extremely common and unlikely to be + * overridden on an individual display. + */ + public function preExecute() { + $this->view->setUseAJAX($this->isAJAXEnabled()); + if ($this->usesMore() && !$this->useMoreAlways()) { + $this->view->get_total_rows = TRUE; + } + $this->view->initHandlers(); + if ($this->usesExposed()) { + $exposed_form = $this->getPlugin('exposed_form'); + $exposed_form->pre_execute(); + } + + foreach ($this->extender as $extender) { + $extender->pre_execute(); + } + + if ($this->getOption('hide_admin_links')) { + $this->view->hide_admin_links = TRUE; + } + } + + /** + * When used externally, this is how a view gets run and returns + * data in the format required. + * + * The base class cannot be executed. + */ + public function execute() { } + + /** + * Fully render the display for the purposes of a live preview or + * some other AJAXy reason. + */ + function preview() { return $this->view->render(); } + + /** + * Displays can require a certain type of style plugin. By default, they will + * be 'normal'. + */ + protected function getStyleType() { return 'normal'; } + + /** + * Make sure the display and all associated handlers are valid. + * + * @return + * Empty array if the display is valid; an array of error strings if it is not. + */ + public function validate() { + $errors = array(); + // Make sure displays that use fields HAVE fields. + if ($this->usesFields()) { + $fields = FALSE; + foreach ($this->getHandlers('field') as $field) { + if (empty($field->options['exclude'])) { + $fields = TRUE; + } + } + + if (!$fields) { + $errors[] = t('Display "@display" uses fields but there are none defined for it or all are excluded.', array('@display' => $this->display['display_title'])); + } + } + + if ($this->hasPath() && !$this->getOption('path')) { + $errors[] = t('Display "@display" uses a path but the path is undefined.', array('@display' => $this->display['display_title'])); + } + + // Validate style plugin + $style = $this->getPlugin('style'); + if (empty($style)) { + $errors[] = t('Display "@display" has an invalid style plugin.', array('@display' => $this->display['display_title'])); + } + else { + $result = $style->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + } + } + + // Validate query plugin. + $query = $this->getPlugin('query'); + $result = $query->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + } + + // Validate handlers + foreach (ViewExecutable::viewsHandlerTypes() as $type => $info) { + foreach ($this->getHandlers($type) as $handler) { + $result = $handler->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + } + } + } + + return $errors; + } + + /** + * Check if the provided identifier is unique. + * + * @param string $id + * The id of the handler which is checked. + * @param string $identifier + * The actual get identifier configured in the exposed settings. + * + * @return bool + * Returns whether the identifier is unique on all handlers. + * + */ + public function isIdentifierUnique($id, $identifier) { + foreach (ViewExecutable::viewsHandlerTypes() as $type => $info) { + foreach ($this->getHandlers($type) as $key => $handler) { + if ($handler->canExpose() && $handler->isExposed()) { + if ($handler->isAGroup()) { + if ($id != $key && $identifier == $handler->options['group_info']['identifier']) { + return FALSE; + } + } + else { + if ($id != $key && $identifier == $handler->options['expose']['identifier']) { + return FALSE; + } + } + } + } + } + return TRUE; + } + + /** + * Provide the block system with any exposed widget blocks for this display. + */ + public function getSpecialBlocks() { + $blocks = array(); + + if ($this->usesExposedFormInBlock()) { + $delta = '-exp-' . $this->view->storage->name . '-' . $this->display['id']; + $desc = t('Exposed form: @view-@display_id', array('@view' => $this->view->storage->name, '@display_id' => $this->display['id'])); + + $blocks[$delta] = array( + 'info' => $desc, + 'cache' => DRUPAL_NO_CACHE, + ); + } + + return $blocks; + } + + /** + * Render any special blocks provided for this display. + */ + public function viewSpecialBlocks($type) { + if ($type == '-exp') { + // avoid interfering with the admin forms. + if (arg(0) == 'admin' && arg(1) == 'structure' && arg(2) == 'views') { + return; + } + $this->view->initHandlers(); + + if ($this->usesExposed() && $this->getOption('exposed_block')) { + $exposed_form = $this->getPlugin('exposed_form'); + return array( + 'content' => $exposed_form->render_exposed_form(TRUE), + ); + } + } + } + + /** + * Provide some helpful text for the arguments. + * The result should contain of an array with + * - filter value present: The title of the fieldset in the argument + * where you can configure what should be done with a given argument. + * - filter value not present: The tiel of the fieldset in the argument + * where you can configure what should be done if the argument does not + * exist. + * - description: A description about how arguments comes to the display. + * For example blocks don't get it from url. + */ + public function getArgumentText() { + return array( + 'filter value not present' => t('When the filter value is <em>NOT</em> available'), + 'filter value present' => t('When the filter value <em>IS</em> available or a default is provided'), + 'description' => t("This display does not have a source for contextual filters, so no contextual filter value will be available unless you select 'Provide default'."), + ); + } + + /** + * Provide some helpful text for pagers. + * + * The result should contain of an array within + * - items per page title + */ + public function getPagerText() { + return array( + 'items per page title' => t('Items to display'), + 'items per page description' => t('The number of items to display. Enter 0 for no limit.') + ); + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/Embed.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/Embed.php new file mode 100644 index 0000000..bb35995 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/Embed.php @@ -0,0 +1,33 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\display\Embed. + */ + +namespace Drupal\views\Plugin\views\display; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The plugin that handles an embed display. + * + * @ingroup views_display_plugins + * + * @todo: Wait until annotations/plugins support access mehtods. + * no_ui => !config('views.settings')->get('ui.show.display_embed'), + * + * @Plugin( + * id = "embed", + * title = @Translation("Embed"), + * help = @Translation("Provide a display which can be embedded using the views api."), + * theme = "views_view", + * uses_hook_menu = FALSE + * ) + */ +class Embed extends DisplayPluginBase { + + // This display plugin does nothing apart from exist. + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/Feed.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/Feed.php new file mode 100644 index 0000000..1af8deb --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/Feed.php @@ -0,0 +1,260 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\display\Feed. + */ + +namespace Drupal\views\Plugin\views\display; + +use Drupal\views\ViewExecutable; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The plugin that handles a feed, such as RSS or atom. + * + * For the most part, feeds are page displays but with some subtle differences. + * + * @ingroup views_display_plugins + * + * @Plugin( + * id = "feed", + * title = @Translation("Feed"), + * help = @Translation("Display the view as a feed, such as an RSS feed."), + * uses_hook_menu = TRUE, + * admin = @Translation("Feed") + * ) + */ +class Feed extends Page { + + /** + * Whether the display allows the use of AJAX or not. + * + * @var bool + */ + protected $usesAJAX = FALSE; + + /** + * Whether the display allows the use of a pager or not. + * + * @var bool + */ + protected $usesPager = FALSE; + + public function init(ViewExecutable $view, &$display, $options = NULL) { + parent::init($view, $display, $options); + + // Set the default row style. Ideally this would be part of the option + // definition, but in this case it's dependent on the view's base table, + // which we don't know until init(). + $row_plugins = views_fetch_plugin_names('row', $this->getStyleType(), array($view->storage->base_table)); + $default_row_plugin = key($row_plugins); + if (empty($this->options['row']['type'])) { + $this->options['row']['type'] = $default_row_plugin; + } + } + + public function usesBreadcrumb() { return FALSE; } + protected function getStyleType() { return 'feed'; } + + /** + * Feeds do not go through the normal page theming mechanism. Instead, they + * go through their own little theme function and then return NULL so that + * Drupal believes that the page has already rendered itself...which it has. + */ + public function execute() { + $output = $this->view->render(); + if (empty($output)) { + throw new NotFoundHttpException(); + } + + $response = $this->view->getResponse(); + $response->setContent($output); + return $response; + } + + public function preview() { + if (!empty($this->view->live_preview)) { + return '<pre>' . check_plain($this->view->render()) . '</pre>'; + } + return $this->view->render(); + } + + /** + * Instead of going through the standard views_view.tpl.php, delegate this + * to the style handler. + */ + public function render() { + return $this->view->style_plugin->render($this->view->result); + } + + public function defaultableSections($section = NULL) { + if (in_array($section, array('style', 'row'))) { + return FALSE; + } + + $sections = parent::defaultableSections($section); + + // Tell views our sitename_title option belongs in the title section. + if ($section == 'title') { + $sections[] = 'sitename_title'; + } + elseif (!$section) { + $sections['title'][] = 'sitename_title'; + } + return $sections; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['displays'] = array('default' => array()); + + // Overrides for standard stuff: + $options['style']['contains']['type']['default'] = 'rss'; + $options['style']['contains']['options']['default'] = array('description' => ''); + $options['sitename_title']['default'] = FALSE; + $options['row']['contains']['type']['default'] = ''; + $options['defaults']['default']['style'] = FALSE; + $options['defaults']['default']['row'] = FALSE; + + return $options; + } + + public function optionsSummary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::optionsSummary($categories, $options); + + // Since we're childing off the 'page' type, we'll still *call* our + // category 'page' but let's override it so it says feed settings. + $categories['page'] = array( + 'title' => t('Feed settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + if ($this->getOption('sitename_title')) { + $options['title']['value'] = t('Using the site name'); + } + + // I don't think we want to give feeds menus directly. + unset($options['menu']); + + $displays = array_filter($this->getOption('displays')); + if (count($displays) > 1) { + $attach_to = t('Multiple displays'); + } + elseif (count($displays) == 1) { + $display = array_shift($displays); + if (!empty($this->view->display[$display])) { + $attach_to = check_plain($this->view->display[$display]['display_title']); + } + } + + if (!isset($attach_to)) { + $attach_to = t('None'); + } + + $options['displays'] = array( + 'category' => 'page', + 'title' => t('Attach to'), + 'value' => $attach_to, + ); + } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here. + parent::buildOptionsForm($form, $form_state); + + switch ($form_state['section']) { + case 'title': + $title = $form['title']; + // A little juggling to move the 'title' field beyond our checkbox. + unset($form['title']); + $form['sitename_title'] = array( + '#type' => 'checkbox', + '#title' => t('Use the site name for the title'), + '#default_value' => $this->getOption('sitename_title'), + ); + $form['title'] = $title; + $form['title']['#states'] = array( + 'visible' => array( + ':input[name="sitename_title"]' => array('checked' => FALSE), + ), + ); + break; + case 'displays': + $form['#title'] .= t('Attach to'); + $displays = array(); + foreach ($this->view->storage->display as $display_id => $display) { + // @todo The display plugin should have display_title and id as well. + if (!empty($this->view->displayHandlers[$display_id]) && $this->view->displayHandlers[$display_id]->acceptAttachments()) { + $displays[$display_id] = $display['display_title']; + } + } + $form['displays'] = array( + '#type' => 'checkboxes', + '#description' => t('The feed icon will be available only to the selected displays.'), + '#options' => $displays, + '#default_value' => $this->getOption('displays'), + ); + break; + case 'path': + $form['path']['#description'] = t('This view will be displayed by visiting this path on your site. It is recommended that the path be something like "path/%/%/feed" or "path/%/%/rss.xml", putting one % in the path for each contextual filter you have defined in the view.'); + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::submitOptionsForm($form, $form_state); + switch ($form_state['section']) { + case 'title': + $this->setOption('sitename_title', $form_state['values']['sitename_title']); + break; + case 'displays': + $this->setOption($form_state['section'], $form_state['values'][$form_state['section']]); + break; + } + } + + /** + * Attach to another view. + */ + public function attachTo($display_id) { + $displays = $this->getOption('displays'); + if (empty($displays[$display_id])) { + return; + } + + // Defer to the feed style; it may put in meta information, and/or + // attach a feed icon. + $plugin = $this->getPlugin('style'); + if ($plugin) { + $clone = $this->view->cloneView(); + $clone->setDisplay($this->display['id']); + $clone->buildTitle(); + $plugin->attach_to($display_id, $this->getPath(), $clone->getTitle()); + + // Clean up + $clone->destroy(); + unset($clone); + } + } + + public function usesLinkDisplay() { + return TRUE; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php new file mode 100644 index 0000000..9bc4346 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/Page.php @@ -0,0 +1,654 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\display\Page. + */ + +namespace Drupal\views\Plugin\views\display; + +use Drupal\Core\Annotation\Plugin; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Drupal\Core\Annotation\Translation; + +/** + * The plugin that handles a full page. + * + * @ingroup views_display_plugins + * + * @Plugin( + * id = "page", + * title = @Translation("Page"), + * help = @Translation("Display the view as a page, with a URL and menu links."), + * uses_hook_menu = TRUE, + * contextual_links_locations = {"page"}, + * theme = "views_view", + * admin = @Translation("Page") + * ) + */ +class Page extends DisplayPluginBase { + + /** + * Whether the display allows attachments. + * + * @var bool + */ + protected $usesAttachments = TRUE; + + /** + * The page display has a path. + */ + public function hasPath() { return TRUE; } + public function usesBreadcrumb() { return TRUE; } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['path'] = array('default' => ''); + $options['menu'] = array( + 'contains' => array( + 'type' => array('default' => 'none'), + // Do not translate menu and title as menu system will. + 'title' => array('default' => '', 'translatable' => FALSE), + 'description' => array('default' => '', 'translatable' => FALSE), + 'weight' => array('default' => 0), + 'name' => array('default' => variable_get('menu_default_node_menu', 'navigation')), + 'context' => array('default' => ''), + ), + ); + $options['tab_options'] = array( + 'contains' => array( + 'type' => array('default' => 'none'), + // Do not translate menu and title as menu system will. + 'title' => array('default' => '', 'translatable' => FALSE), + 'description' => array('default' => '', 'translatable' => FALSE), + 'weight' => array('default' => 0), + 'name' => array('default' => 'navigation'), + ), + ); + + return $options; + } + + /** + * Add this display's path information to Drupal's menu system. + */ + public function executeHookMenu($callbacks) { + $items = array(); + // Replace % with the link to our standard views argument loader + // views_arg_load -- which lives in views.module + + $bits = explode('/', $this->getOption('path')); + $page_arguments = array($this->view->storage->name, $this->display['id']); + $this->view->initHandlers(); + $view_arguments = $this->view->argument; + + // Replace % with %views_arg for menu autoloading and add to the + // page arguments so the argument actually comes through. + foreach ($bits as $pos => $bit) { + if ($bit == '%') { + $argument = array_shift($view_arguments); + if (!empty($argument->options['specify_validation']) && $argument->options['validate']['type'] != 'none') { + $bits[$pos] = '%views_arg'; + } + $page_arguments[] = $pos; + } + } + + $path = implode('/', $bits); + + $access_plugin = $this->getPlugin('access'); + if (!isset($access_plugin)) { + $access_plugin = views_get_plugin('access', 'none'); + } + + // Get access callback might return an array of the callback + the dynamic arguments. + $access_plugin_callback = $access_plugin->get_access_callback(); + + if (is_array($access_plugin_callback)) { + $access_arguments = array(); + + // Find the plugin arguments. + $access_plugin_method = array_shift($access_plugin_callback); + $access_plugin_arguments = array_shift($access_plugin_callback); + if (!is_array($access_plugin_arguments)) { + $access_plugin_arguments = array(); + } + + $access_arguments[0] = array($access_plugin_method, &$access_plugin_arguments); + + // Move the plugin arguments to the access arguments array. + $i = 1; + foreach ($access_plugin_arguments as $key => $value) { + if (is_int($value)) { + $access_arguments[$i] = $value; + $access_plugin_arguments[$key] = $i; + $i++; + } + } + } + else { + $access_arguments = array($access_plugin_callback); + } + + if ($path) { + $items[$path] = array( + // default views page entry + 'page callback' => 'views_page', + 'page arguments' => $page_arguments, + // Default access check (per display) + 'access callback' => 'views_access', + 'access arguments' => $access_arguments, + // Identify URL embedded arguments and correlate them to a handler + 'load arguments' => array($this->view->storage->name, $this->display['id'], '%index'), + ); + $menu = $this->getOption('menu'); + if (empty($menu)) { + $menu = array('type' => 'none'); + } + // Set the title and description if we have one. + if ($menu['type'] != 'none') { + $items[$path]['title'] = $menu['title']; + $items[$path]['description'] = $menu['description']; + } + + if (isset($menu['weight'])) { + $items[$path]['weight'] = intval($menu['weight']); + } + + switch ($menu['type']) { + case 'none': + default: + $items[$path]['type'] = MENU_CALLBACK; + break; + case 'normal': + $items[$path]['type'] = MENU_NORMAL_ITEM; + // Insert item into the proper menu + $items[$path]['menu_name'] = $menu['name']; + break; + case 'tab': + $items[$path]['type'] = MENU_LOCAL_TASK; + break; + case 'default tab': + $items[$path]['type'] = MENU_DEFAULT_LOCAL_TASK; + break; + } + + // Add context for contextual links. + // @see menu_contextual_links() + if (!empty($menu['context'])) { + $items[$path]['context'] = MENU_CONTEXT_INLINE; + } + + // If this is a 'default' tab, check to see if we have to create teh + // parent menu item. + if ($menu['type'] == 'default tab') { + $tab_options = $this->getOption('tab_options'); + if (!empty($tab_options['type']) && $tab_options['type'] != 'none') { + $bits = explode('/', $path); + // Remove the last piece. + $bit = array_pop($bits); + + // we can't do this if they tried to make the last path bit variable. + // @todo: We can validate this. + if ($bit != '%views_arg' && !empty($bits)) { + $default_path = implode('/', $bits); + $items[$default_path] = array( + // default views page entry + 'page callback' => 'views_page', + 'page arguments' => $page_arguments, + // Default access check (per display) + 'access callback' => 'views_access', + 'access arguments' => $access_arguments, + // Identify URL embedded arguments and correlate them to a handler + 'load arguments' => array($this->view->storage->name, $this->display['id'], '%index'), + 'title' => $tab_options['title'], + 'description' => $tab_options['description'], + 'menu_name' => $tab_options['name'], + ); + switch ($tab_options['type']) { + default: + case 'normal': + $items[$default_path]['type'] = MENU_NORMAL_ITEM; + break; + case 'tab': + $items[$default_path]['type'] = MENU_LOCAL_TASK; + break; + } + if (isset($tab_options['weight'])) { + $items[$default_path]['weight'] = intval($tab_options['weight']); + } + } + } + } + } + + return $items; + } + + /** + * The display page handler returns a normal view, but it also does + * a drupal_set_title for the page, and does a views_set_page_view + * on the view. + */ + public function execute() { + // Let the world know that this is the page view we're using. + views_set_page_view($this->view); + + // Prior to this being called, the $view should already be set to this + // display, and arguments should be set on the view. + $this->view->build(); + if (!empty($this->view->build_info['fail'])) { + throw new NotFoundHttpException(); + } + + if (!empty($this->view->build_info['denied'])) { + throw new AccessDeniedHttpException(); + } + + $this->view->getBreadcrumb(TRUE); + + + // And now render the view. + $render = $this->view->render(); + + // First execute the view so it's possible to get tokens for the title. + // And the title, which is much easier. + drupal_set_title(filter_xss_admin($this->view->getTitle()), PASS_THROUGH); + return $render; + } + + /** + * Provide the summary for page options in the views UI. + * + * This output is returned as an array. + */ + public function optionsSummary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::optionsSummary($categories, $options); + + $categories['page'] = array( + 'title' => t('Page settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + $path = strip_tags('/' . $this->getOption('path')); + if (empty($path)) { + $path = t('None'); + } + + $options['path'] = array( + 'category' => 'page', + 'title' => t('Path'), + 'value' => views_ui_truncate($path, 24), + ); + + $menu = $this->getOption('menu'); + if (!is_array($menu)) { + $menu = array('type' => 'none'); + } + switch ($menu['type']) { + case 'none': + default: + $menu_str = t('No menu'); + break; + case 'normal': + $menu_str = t('Normal: @title', array('@title' => $menu['title'])); + break; + case 'tab': + case 'default tab': + $menu_str = t('Tab: @title', array('@title' => $menu['title'])); + break; + } + + $options['menu'] = array( + 'category' => 'page', + 'title' => t('Menu'), + 'value' => views_ui_truncate($menu_str, 24), + ); + + // This adds a 'Settings' link to the style_options setting if the style has options. + if ($menu['type'] == 'default tab') { + $options['menu']['setting'] = t('Parent menu item'); + $options['menu']['links']['tab_options'] = t('Change settings for the parent menu'); + } + } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::buildOptionsForm($form, $form_state); + + switch ($form_state['section']) { + case 'path': + $form['#title'] .= t('The menu path or URL of this view'); + $form['path'] = array( + '#type' => 'textfield', + '#description' => t('This view will be displayed by visiting this path on your site. You may use "%" in your URL to represent values that will be used for contextual filters: For example, "node/%/feed".'), + '#default_value' => $this->getOption('path'), + '#field_prefix' => '<span dir="ltr">' . url(NULL, array('absolute' => TRUE)), + '#field_suffix' => '</span>‎', + '#attributes' => array('dir' => 'ltr'), + ); + break; + case 'menu': + $form['#title'] .= t('Menu item entry'); + $form['menu'] = array( + '#prefix' => '<div class="clearfix">', + '#suffix' => '</div>', + '#tree' => TRUE, + ); + $menu = $this->getOption('menu'); + if (empty($menu)) { + $menu = array('type' => 'none', 'title' => '', 'weight' => 0); + } + $form['menu']['type'] = array( + '#prefix' => '<div class="views-left-30">', + '#suffix' => '</div>', + '#title' => t('Type'), + '#type' => 'radios', + '#options' => array( + 'none' => t('No menu entry'), + 'normal' => t('Normal menu entry'), + 'tab' => t('Menu tab'), + 'default tab' => t('Default menu tab') + ), + '#default_value' => $menu['type'], + ); + + $form['menu']['title'] = array( + '#prefix' => '<div class="views-left-50">', + '#title' => t('Title'), + '#type' => 'textfield', + '#default_value' => $menu['title'], + '#description' => t('If set to normal or tab, enter the text to use for the menu item.'), + '#states' => array( + 'visible' => array( + array( + ':input[name="menu[type]"]' => array('value' => 'normal'), + ), + array( + ':input[name="menu[type]"]' => array('value' => 'tab'), + ), + array( + ':input[name="menu[type]"]' => array('value' => 'default tab'), + ), + ), + ), + ); + $form['menu']['description'] = array( + '#title' => t('Description'), + '#type' => 'textfield', + '#default_value' => $menu['description'], + '#description' => t("If set to normal or tab, enter the text to use for the menu item's description."), + '#states' => array( + 'visible' => array( + array( + ':input[name="menu[type]"]' => array('value' => 'normal'), + ), + array( + ':input[name="menu[type]"]' => array('value' => 'tab'), + ), + array( + ':input[name="menu[type]"]' => array('value' => 'default tab'), + ), + ), + ), + ); + + // Only display the menu selector if menu module is enabled. + if (module_exists('menu')) { + $form['menu']['name'] = array( + '#title' => t('Menu'), + '#type' => 'select', + '#options' => menu_get_menus(), + '#default_value' => $menu['name'], + '#description' => t('Insert item into an available menu.'), + '#states' => array( + 'visible' => array( + array( + ':input[name="menu[type]"]' => array('value' => 'normal'), + ), + array( + ':input[name="menu[type]"]' => array('value' => 'tab'), + ), + ), + ), + ); + } + else { + $form['menu']['name'] = array( + '#type' => 'value', + '#value' => $menu['name'], + ); + $form['menu']['markup'] = array( + '#markup' => t('Menu selection requires the activation of menu module.'), + ); + } + $form['menu']['weight'] = array( + '#title' => t('Weight'), + '#type' => 'textfield', + '#default_value' => isset($menu['weight']) ? $menu['weight'] : 0, + '#description' => t('The lower the weight the higher/further left it will appear.'), + '#states' => array( + 'visible' => array( + array( + ':input[name="menu[type]"]' => array('value' => 'normal'), + ), + array( + ':input[name="menu[type]"]' => array('value' => 'tab'), + ), + array( + ':input[name="menu[type]"]' => array('value' => 'default tab'), + ), + ), + ), + ); + $form['menu']['context'] = array( + '#title' => t('Context'), + '#suffix' => '</div>', + '#type' => 'checkbox', + '#default_value' => !empty($menu['context']), + '#description' => t('Displays the link in contextual links'), + '#states' => array( + 'visible' => array( + ':input[name="menu[type]"]' => array('value' => 'tab'), + ), + ), + ); + break; + case 'tab_options': + $form['#title'] .= t('Default tab options'); + $tab_options = $this->getOption('tab_options'); + if (empty($tab_options)) { + $tab_options = array('type' => 'none', 'title' => '', 'weight' => 0); + } + + $form['tab_markup'] = array( + '#markup' => '<div class="form-item description">' . t('When providing a menu item as a tab, Drupal needs to know what the parent menu item of that tab will be. Sometimes the parent will already exist, but other times you will need to have one created. The path of a parent item will always be the same path with the last part left off. i.e, if the path to this view is <em>foo/bar/baz</em>, the parent path would be <em>foo/bar</em>.') . '</div>', + ); + + $form['tab_options'] = array( + '#prefix' => '<div class="clearfix">', + '#suffix' => '</div>', + '#tree' => TRUE, + ); + $form['tab_options']['type'] = array( + '#prefix' => '<div class="views-left-25">', + '#suffix' => '</div>', + '#title' => t('Parent menu item'), + '#type' => 'radios', + '#options' => array('none' => t('Already exists'), 'normal' => t('Normal menu item'), 'tab' => t('Menu tab')), + '#default_value' => $tab_options['type'], + ); + $form['tab_options']['title'] = array( + '#prefix' => '<div class="views-left-75">', + '#title' => t('Title'), + '#type' => 'textfield', + '#default_value' => $tab_options['title'], + '#description' => t('If creating a parent menu item, enter the title of the item.'), + '#states' => array( + 'visible' => array( + array( + ':input[name="tab_options[type]"]' => array('value' => 'normal'), + ), + array( + ':input[name="tab_options[type]"]' => array('value' => 'tab'), + ), + ), + ), + ); + $form['tab_options']['description'] = array( + '#title' => t('Description'), + '#type' => 'textfield', + '#default_value' => $tab_options['description'], + '#description' => t('If creating a parent menu item, enter the description of the item.'), + '#states' => array( + 'visible' => array( + array( + ':input[name="tab_options[type]"]' => array('value' => 'normal'), + ), + array( + ':input[name="tab_options[type]"]' => array('value' => 'tab'), + ), + ), + ), + ); + // Only display the menu selector if menu module is enabled. + if (module_exists('menu')) { + $form['tab_options']['name'] = array( + '#title' => t('Menu'), + '#type' => 'select', + '#options' => menu_get_menus(), + '#default_value' => $tab_options['name'], + '#description' => t('Insert item into an available menu.'), + '#states' => array( + 'visible' => array( + ':input[name="tab_options[type]"]' => array('value' => 'normal'), + ), + ), + ); + } + else { + $form['tab_options']['name'] = array( + '#type' => 'value', + '#value' => $tab_options['name'], + ); + $form['tab_options']['markup'] = array( + '#markup' => t('Menu selection requires the activation of menu module.'), + ); + } + $form['tab_options']['weight'] = array( + '#suffix' => '</div>', + '#title' => t('Tab weight'), + '#type' => 'textfield', + '#default_value' => $tab_options['weight'], + '#size' => 5, + '#description' => t('If the parent menu item is a tab, enter the weight of the tab. The lower the number, the more to the left it will be.'), + '#states' => array( + 'visible' => array( + ':input[name="tab_options[type]"]' => array('value' => 'tab'), + ), + ), + ); + break; + } + } + + public function validateOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::validateOptionsForm($form, $form_state); + switch ($form_state['section']) { + case 'path': + if (strpos($form_state['values']['path'], '$arg') !== FALSE) { + form_error($form['path'], t('"$arg" is no longer supported. Use % instead.')); + } + + if (strpos($form_state['values']['path'], '%') === 0) { + form_error($form['path'], t('"%" may not be used for the first segment of a path.')); + } + + // automatically remove '/' and trailing whitespace from path. + $form_state['values']['path'] = trim($form_state['values']['path'], '/ '); + break; + case 'menu': + $path = $this->getOption('path'); + if ($form_state['values']['menu']['type'] == 'normal' && strpos($path, '%') !== FALSE) { + form_error($form['menu']['type'], t('Views cannot create normal menu items for paths with a % in them.')); + } + + if ($form_state['values']['menu']['type'] == 'default tab' || $form_state['values']['menu']['type'] == 'tab') { + $bits = explode('/', $path); + $last = array_pop($bits); + if ($last == '%') { + form_error($form['menu']['type'], t('A display whose path ends with a % cannot be a tab.')); + } + } + + if ($form_state['values']['menu']['type'] != 'none' && empty($form_state['values']['menu']['title'])) { + form_error($form['menu']['title'], t('Title is required for this menu type.')); + } + break; + } + } + + public function submitOptionsForm(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::submitOptionsForm($form, $form_state); + switch ($form_state['section']) { + case 'path': + $this->setOption('path', $form_state['values']['path']); + break; + case 'menu': + $this->setOption('menu', $form_state['values']['menu']); + // send ajax form to options page if we use it. + if ($form_state['values']['menu']['type'] == 'default tab') { + $this->view->addFormToStack('display', $this->display['id'], array('tab_options')); + } + break; + case 'tab_options': + $this->setOption('tab_options', $form_state['values']['tab_options']); + break; + } + } + + public function validate() { + $errors = parent::validate(); + + $menu = $this->getOption('menu'); + if (!empty($menu['type']) && $menu['type'] != 'none' && empty($menu['title'])) { + $errors[] = t('Display @display is set to use a menu but the menu link text is not set.', array('@display' => $this->display['display_title'])); + } + + if ($menu['type'] == 'default tab') { + $tab_options = $this->getOption('tab_options'); + if (!empty($tab_options['type']) && $tab_options['type'] != 'none' && empty($tab_options['title'])) { + $errors[] = t('Display @display is set to use a parent menu but the parent menu link text is not set.', array('@display' => $this->display['display_title'])); + } + } + + return $errors; + } + + public function getArgumentText() { + return array( + 'filter value not present' => t('When the filter value is <em>NOT</em> in the URL'), + 'filter value present' => t('When the filter value <em>IS</em> in the URL or a default is provided'), + 'description' => t('The contextual filter values is provided by the URL.'), + ); + } + + public function getPagerText() { + return array( + 'items per page title' => t('Items per page'), + 'items per page description' => t('The number of items to display per page. Enter 0 for no limit.') + ); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display_extender/DefaultDisplayExtender.php b/core/modules/views/lib/Drupal/views/Plugin/views/display_extender/DefaultDisplayExtender.php new file mode 100644 index 0000000..b8faf98 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display_extender/DefaultDisplayExtender.php @@ -0,0 +1,26 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\display_extender\DefaultDisplayExtender. + */ + +namespace Drupal\views\Plugin\views\display_extender; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * @todo + * + * @Plugin( + * id = "default", + * title = @Translation("Empty display extender"), + * help = @Translation("Default settings for this view."), + * enabled = FALSE, + * no_ui = TRUE + * ) + */ +class DefaultDisplayExtender extends DisplayExtenderPluginBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display_extender/DisplayExtenderPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/display_extender/DisplayExtenderPluginBase.php new file mode 100644 index 0000000..0647022 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display_extender/DisplayExtenderPluginBase.php @@ -0,0 +1,70 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase. + */ + +namespace Drupal\views\Plugin\views\display_extender; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\PluginBase; +use Drupal\Core\Annotation\Translation; + +/** + * @todo. + * + * @ingroup views_display_plugins + */ +abstract class DisplayExtenderPluginBase extends PluginBase { + + public function init(ViewExecutable $view, &$display) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = $view; + $this->displayHandler = $display; + } + + /** + * Provide a form to edit options for this plugin. + */ + public function defineOptionsAlter(&$options) { } + + /** + * Provide a form to edit options for this plugin. + */ + public function buildOptionsForm(&$form, &$form_state) { } + + /** + * Validate the options form. + */ + public function validateOptionsForm(&$form, &$form_state) { } + + /** + * Handle any special handling on the validate form. + */ + public function submitOptionsForm(&$form, &$form_state) { } + + /** + * Set up any variables on the view prior to execution. + */ + public function pre_execute() { } + + /** + * Inject anything into the query that the display_extender handler needs. + */ + public function query() { } + + /** + * Provide the default summary for options in the views UI. + * + * This output is returned as an array. + */ + public function optionsSummary(&$categories, &$options) { } + + /** + * Static member function to list which sections are defaultable + * and what items each section contains. + */ + public function defaultableSections(&$sections, $section = NULL) { } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/Basic.php b/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/Basic.php new file mode 100644 index 0000000..f850d89 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/Basic.php @@ -0,0 +1,26 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\exposed_form\Basic. + */ + +namespace Drupal\views\Plugin\views\exposed_form; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Exposed form plugin that provides a basic exposed form. + * + * @ingroup views_exposed_form_plugins + * + * @Plugin( + * id = "basic", + * title = @Translation("Basic"), + * help = @Translation("Basic exposed form") + * ) + */ +class Basic extends ExposedFormPluginBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/ExposedFormPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/ExposedFormPluginBase.php new file mode 100644 index 0000000..c583d65 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/ExposedFormPluginBase.php @@ -0,0 +1,324 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase. + */ + +namespace Drupal\views\Plugin\views\exposed_form; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\PluginBase; + +/** + * @defgroup views_exposed_form_plugins Views exposed form plugins + * @{ + * Plugins that handle the validation/submission and rendering of exposed forms. + * + * If needed, it is possible to use them to add additional form elements. + * + * @see hook_views_plugins() + */ + +/** + * The base plugin to handle exposed filter forms. + */ +abstract class ExposedFormPluginBase extends PluginBase { + + /** + * Overrides Drupal\views\Plugin\Plugin::$usesOptions. + */ + protected $usesOptions = TRUE; + + /** + * Initialize the plugin. + * + * @param $view + * The view object. + * @param $display + * The display handler. + */ + public function init(ViewExecutable $view, &$display, $options = array()) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->displayHandler = &$display; + + $this->unpackOptions($this->options, $options); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['submit_button'] = array('default' => 'Apply', 'translatable' => TRUE); + $options['reset_button'] = array('default' => FALSE, 'bool' => TRUE); + $options['reset_button_label'] = array('default' => 'Reset', 'translatable' => TRUE); + $options['exposed_sorts_label'] = array('default' => 'Sort by', 'translatable' => TRUE); + $options['expose_sort_order'] = array('default' => TRUE, 'bool' => TRUE); + $options['sort_asc_label'] = array('default' => 'Asc', 'translatable' => TRUE); + $options['sort_desc_label'] = array('default' => 'Desc', 'translatable' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['submit_button'] = array( + '#type' => 'textfield', + '#title' => t('Submit button text'), + '#description' => t('Text to display in the submit button of the exposed form.'), + '#default_value' => $this->options['submit_button'], + '#required' => TRUE, + ); + + $form['reset_button'] = array( + '#type' => 'checkbox', + '#title' => t('Include reset button'), + '#description' => t('If checked the exposed form will provide a button to reset all the applied exposed filters'), + '#default_value' => $this->options['reset_button'], + ); + + $form['reset_button_label'] = array( + '#type' => 'textfield', + '#title' => t('Reset button label'), + '#description' => t('Text to display in the reset button of the exposed form.'), + '#default_value' => $this->options['reset_button_label'], + '#required' => TRUE, + '#states' => array( + 'invisible' => array( + 'input[name="exposed_form_options[reset_button]"]' => array('checked' => FALSE), + ), + ), + ); + + $form['exposed_sorts_label'] = array( + '#type' => 'textfield', + '#title' => t('Exposed sorts label'), + '#description' => t('Text to display as the label of the exposed sort select box.'), + '#default_value' => $this->options['exposed_sorts_label'], + '#required' => TRUE, + ); + + $form['expose_sort_order'] = array( + '#type' => 'checkbox', + '#title' => t('Expose sort order'), + '#description' => t('Allow the user to choose the sort order. If sort order is not exposed, the sort criteria settings for each sort will determine its order.'), + '#default_value' => $this->options['expose_sort_order'], + ); + + $form['sort_asc_label'] = array( + '#type' => 'textfield', + '#title' => t('Ascending'), + '#description' => t('Text to use when exposed sort is ordered ascending.'), + '#default_value' => $this->options['sort_asc_label'], + '#required' => TRUE, + '#states' => array( + 'visible' => array( + 'input[name="exposed_form_options[expose_sort_order]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['sort_desc_label'] = array( + '#type' => 'textfield', + '#title' => t('Descending'), + '#description' => t('Text to use when exposed sort is ordered descending.'), + '#default_value' => $this->options['sort_desc_label'], + '#required' => TRUE, + '#states' => array( + 'visible' => array( + 'input[name="exposed_form_options[expose_sort_order]"]' => array('checked' => TRUE), + ), + ), + ); + } + + /** + * Render the exposed filter form. + * + * This actually does more than that; because it's using FAPI, the form will + * also assign data to the appropriate handlers for use in building the + * query. + */ + function render_exposed_form($block = FALSE) { + // Deal with any exposed filters we may have, before building. + $form_state = array( + 'view' => &$this->view, + 'display' => &$this->display, + 'method' => 'get', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + 'always_process' => TRUE, + ); + + // Some types of displays (eg. attachments) may wish to use the exposed + // filters of their parent displays instead of showing an additional + // exposed filter form for the attachment as well as that for the parent. + if (!$this->view->display_handler->displaysExposed() || (!$block && $this->view->display_handler->getOption('exposed_block'))) { + unset($form_state['rerender']); + } + + if (!empty($this->ajax)) { + $form_state['ajax'] = TRUE; + } + + $form_state['exposed_form_plugin'] = $this; + $form = drupal_build_form('views_exposed_form', $form_state); + $output = drupal_render($form); + + if (!$this->view->display_handler->displaysExposed() || (!$block && $this->view->display_handler->getOption('exposed_block'))) { + return ""; + } + else { + return $output; + } + } + + public function query() { + $view = $this->view; + $exposed_data = isset($view->exposed_data) ? $view->exposed_data : array(); + $sort_by = isset($exposed_data['sort_by']) ? $exposed_data['sort_by'] : NULL; + if (!empty($sort_by)) { + // Make sure the original order of sorts is preserved + // (e.g. a sticky sort is often first) + if (isset($view->sort[$sort_by])) { + $view->query->orderby = array(); + foreach ($view->sort as $key => $sort) { + if (!$sort->isExposed()) { + $sort->query(); + } + elseif ($key == $sort_by) { + if (isset($exposed_data['sort_order']) && in_array($exposed_data['sort_order'], array('ASC', 'DESC'))) { + $sort->options['order'] = $exposed_data['sort_order']; + } + $sort->setRelationship(); + $sort->query(); + } + } + } + } + } + + function pre_render($values) { } + + function post_render(&$output) { } + + function pre_execute() { } + + public function postExecute() { } + + function exposed_form_alter(&$form, &$form_state) { + if (!empty($this->options['reset_button'])) { + $form['reset'] = array( + '#value' => $this->options['reset_button_label'], + '#type' => 'submit', + ); + } + + $form['submit']['#value'] = $this->options['submit_button']; + // Check if there is exposed sorts for this view + $exposed_sorts = array(); + foreach ($this->view->sort as $id => $handler) { + if ($handler->canExpose() && $handler->isExposed()) { + $exposed_sorts[$id] = check_plain($handler->options['expose']['label']); + } + } + + if (count($exposed_sorts)) { + $form['sort_by'] = array( + '#type' => 'select', + '#options' => $exposed_sorts, + '#title' => $this->options['exposed_sorts_label'], + ); + $sort_order = array( + 'ASC' => $this->options['sort_asc_label'], + 'DESC' => $this->options['sort_desc_label'], + ); + if (isset($form_state['input']['sort_by']) && isset($this->view->sort[$form_state['input']['sort_by']])) { + $default_sort_order = $this->view->sort[$form_state['input']['sort_by']]->options['order']; + } + else { + $first_sort = reset($this->view->sort); + $default_sort_order = $first_sort->options['order']; + } + + if (!isset($form_state['input']['sort_by'])) { + $keys = array_keys($exposed_sorts); + $form_state['input']['sort_by'] = array_shift($keys); + } + + if ($this->options['expose_sort_order']) { + $form['sort_order'] = array( + '#type' => 'select', + '#options' => $sort_order, + '#title' => t('Order'), + '#default_value' => $default_sort_order, + ); + } + $form['submit']['#weight'] = 10; + if (isset($form['reset'])) { + $form['reset']['#weight'] = 10; + } + } + + $pager = $this->view->display_handler->getPlugin('pager'); + if ($pager) { + $pager->exposed_form_alter($form, $form_state); + $form_state['pager_plugin'] = $pager; + } + } + + function exposed_form_validate(&$form, &$form_state) { + if (isset($form_state['pager_plugin'])) { + $form_state['pager_plugin']->exposed_form_validate($form, $form_state); + } + } + + /** + * This function is executed when exposed form is submited. + * + * @param $form + * Nested array of form elements that comprise the form. + * @param $form_state + * A keyed array containing the current state of the form. + * @param $exclude + * Nested array of keys to exclude of insert into + * $view->exposed_raw_input + */ + function exposed_form_submit(&$form, &$form_state, &$exclude) { + if (!empty($form_state['values']['op']) && $form_state['values']['op'] == $this->options['reset_button_label']) { + $this->reset_form($form, $form_state); + } + if (isset($form_state['pager_plugin'])) { + $form_state['pager_plugin']->exposed_form_submit($form, $form_state, $exclude); + $exclude[] = 'pager_plugin'; + } + } + + function reset_form(&$form, &$form_state) { + // _SESSION is not defined for users who are not logged in. + + // If filters are not overridden, store the 'remember' settings on the + // default display. If they are, store them on this display. This way, + // multiple displays in the same view can share the same filters and + // remember settings. + $display_id = ($this->view->display_handler->isDefaulted('filters')) ? 'default' : $this->view->current_display; + + if (isset($_SESSION['views'][$this->view->storage->name][$display_id])) { + unset($_SESSION['views'][$this->view->storage->name][$display_id]); + } + + // Set the form to allow redirect. + if (empty($this->view->live_preview)) { + $form_state['no_redirect'] = FALSE; + } + else { + $form_state['rebuild'] = TRUE; + $this->view->exposed_data = array(); + } + + $form_state['values'] = array(); + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/InputRequired.php b/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/InputRequired.php new file mode 100644 index 0000000..3baebe0 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/InputRequired.php @@ -0,0 +1,107 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\exposed_form\InputRequired. + */ + +namespace Drupal\views\Plugin\views\exposed_form; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Exposed form plugin that provides an exposed form with required input. + * + * @ingroup views_exposed_form_plugins + * + * @Plugin( + * id = "input_required", + * title = @Translation("Input required"), + * help = @Translation("An exposed form that only renders a view if the form contains user input.") + * ) + */ +class InputRequired extends ExposedFormPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['text_input_required'] = array('default' => 'Select any filter and click on Apply to see results', 'translatable' => TRUE); + $options['text_input_required_format'] = array('default' => NULL); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['text_input_required'] = array( + '#type' => 'text_format', + '#title' => t('Text on demand'), + '#description' => t('Text to display instead of results until the user selects and applies an exposed filter.'), + '#default_value' => $this->options['text_input_required'], + '#format' => isset($this->options['text_input_required_format']) ? $this->options['text_input_required_format'] : filter_default_format(), + '#wysiwyg' => FALSE, + ); + } + + public function submitOptionsForm(&$form, &$form_state) { + $form_state['values']['exposed_form_options']['text_input_required_format'] = $form_state['values']['exposed_form_options']['text_input_required']['format']; + $form_state['values']['exposed_form_options']['text_input_required'] = $form_state['values']['exposed_form_options']['text_input_required']['value']; + parent::submitOptionsForm($form, $form_state); + } + + function exposed_filter_applied() { + static $cache = NULL; + if (!isset($cache)) { + $view = $this->view; + if (is_array($view->filter) && count($view->filter)) { + foreach ($view->filter as $filter_id => $filter) { + if ($filter->isExposed()) { + $identifier = $filter->options['expose']['identifier']; + if (isset($view->exposed_input[$identifier])) { + $cache = TRUE; + return $cache; + } + } + } + } + $cache = FALSE; + } + + return $cache; + } + + function pre_render($values) { + if (!$this->exposed_filter_applied()) { + $options = array( + 'id' => 'area', + 'table' => 'views', + 'field' => 'area', + 'label' => '', + 'relationship' => 'none', + 'group_type' => 'group', + 'content' => $this->options['text_input_required'], + 'format' => $this->options['text_input_required_format'], + ); + $handler = views_get_handler('views', 'area', 'area'); + $handler->init($this->view, $options); + $this->displayHandler->handlers['empty'] = array( + 'area' => $handler, + ); + $this->displayHandler->setOption('empty', array('text' => $options)); + } + } + + public function query() { + if (!$this->exposed_filter_applied()) { + // We return with no query; this will force the empty text. + $this->view->built = TRUE; + $this->view->executed = TRUE; + $this->view->result = array(); + } + else { + parent::query(); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Boolean.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Boolean.php new file mode 100644 index 0000000..5a9ae65 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Boolean.php @@ -0,0 +1,92 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Boolean. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\views\ViewExecutable; +use Drupal\Core\Annotation\Plugin; + +/** + * A handler to provide proper displays for booleans. + * + * Allows for display of true/false, yes/no, on/off, enabled/disabled. + * + * Definition terms: + * - output formats: An array where the first entry is displayed on boolean true + * and the second is displayed on boolean false. An example for sticky is: + * @code + * 'output formats' => array( + * 'sticky' => array(t('Sticky'), ''), + * ), + * @endcode + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "boolean" + * ) + */ +class Boolean extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['type'] = array('default' => 'yes-no'); + $options['not'] = array('definition bool' => 'reverse'); + + return $options; + } + + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + $default_formats = array( + 'yes-no' => array(t('Yes'), t('No')), + 'true-false' => array(t('True'), t('False')), + 'on-off' => array(t('On'), t('Off')), + 'enabled-disabled' => array(t('Enabled'), t('Disabled')), + 'boolean' => array(1, 0), + 'unicode-yes-no' => array('✔', '✖'), + ); + $output_formats = isset($this->definition['output formats']) ? $this->definition['output formats'] : array(); + $this->formats = array_merge($default_formats, $output_formats); + } + + public function buildOptionsForm(&$form, &$form_state) { + foreach ($this->formats as $key => $item) { + $options[$key] = implode('/', $item); + } + + $form['type'] = array( + '#type' => 'select', + '#title' => t('Output format'), + '#options' => $options, + '#default_value' => $this->options['type'], + ); + $form['not'] = array( + '#type' => 'checkbox', + '#title' => t('Reverse'), + '#description' => t('If checked, true will be displayed as false.'), + '#default_value' => $this->options['not'], + ); + parent::buildOptionsForm($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + if (!empty($this->options['not'])) { + $value = !$value; + } + + if (isset($this->formats[$this->options['type']])) { + return $value ? $this->formats[$this->options['type']][0] : $this->formats[$this->options['type']][1]; + } + else { + return $value ? $this->formats['yes-no'][0] : $this->formats['yes-no'][1]; + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Broken.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Broken.php new file mode 100644 index 0000000..f131f25 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Broken.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Broken. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "broken" + * ) + */ +class Broken extends FieldPluginBase { + + public function adminLabel($short = FALSE) { + return t('Broken/missing handler'); + } + + public function defineOptions() { return array(); } + public function ensureMyTable() { /* No table to ensure! */ } + public function query($group_by = FALSE) { /* No query to run */ } + public function buildOptionsForm(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '<div class="form-item description">' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '</div>', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + public function broken() { return TRUE; } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/ContextualLinks.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/ContextualLinks.php new file mode 100644 index 0000000..ce7d957 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/ContextualLinks.php @@ -0,0 +1,116 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\ContextualLinks. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Provides a handler that adds contextual links. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "contextual_links" + * ) + */ +class ContextualLinks extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['fields'] = array('default' => array()); + $options['destination'] = array('default' => 1); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $all_fields = $this->view->display_handler->getFieldLabels(); + // Offer to include only those fields that follow this one. + $field_options = array_slice($all_fields, 0, array_search($this->options['id'], array_keys($all_fields))); + $form['fields'] = array( + '#type' => 'checkboxes', + '#title' => t('Fields'), + '#description' => t('Fields to be included as contextual links.'), + '#options' => $field_options, + '#default_value' => $this->options['fields'], + ); + $form['destination'] = array( + '#type' => 'select', + '#title' => t('Include destination'), + '#description' => t('Include a "destination" parameter in the link to return the user to the original view upon completing the contextual action.'), + '#options' => array( + '0' => t('No'), + '1' => t('Yes'), + ), + '#default_value' => $this->options['destination'], + ); + } + + function pre_render(&$values) { + // Add a row plugin css class for the contextual link. + $class = 'contextual-region'; + if (!empty($this->view->style_plugin->options['row_class'])) { + $this->view->style_plugin->options['row_class'] .= " $class"; + } + else { + $this->view->style_plugin->options['row_class'] = $class; + } + } + + /** + * Render the contextual fields. + */ + function render($values) { + $links = array(); + foreach ($this->options['fields'] as $field) { + if (empty($this->view->style_plugin->rendered_fields[$this->view->row_index][$field])) { + continue; + } + $title = $this->view->field[$field]->last_render_text; + $path = ''; + if (!empty($this->view->field[$field]->options['alter']['path'])) { + $path = $this->view->field[$field]->options['alter']['path']; + } + if (!empty($title) && !empty($path)) { + // Make sure that tokens are replaced for this paths as well. + $tokens = $this->get_render_tokens(array()); + $path = strip_tags(decode_entities(strtr($path, $tokens))); + + $links[$field] = array( + 'href' => $path, + 'title' => $title, + ); + if (!empty($this->options['destination'])) { + $links[$field]['query'] = drupal_get_destination(); + } + } + } + + if (!empty($links)) { + $build = array( + '#prefix' => '<div class="contextual">', + '#suffix' => '</div>', + '#theme' => 'links__contextual', + '#links' => $links, + '#attributes' => array('class' => array('contextual-links')), + '#attached' => array( + 'library' => array(array('contextual', 'contextual-links')), + ), + '#access' => user_access('access contextual links'), + ); + return drupal_render($build); + } + else { + return ''; + } + } + + public function query() { } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Counter.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Counter.php new file mode 100644 index 0000000..9d27f06 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Counter.php @@ -0,0 +1,63 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Counter. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Field handler to show a counter of the current row. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "counter" + * ) + */ +class Counter extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['counter_start'] = array('default' => 1); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['counter_start'] = array( + '#type' => 'textfield', + '#title' => t('Starting value'), + '#default_value' => $this->options['counter_start'], + '#description' => t('Specify the number the counter should start at.'), + '#size' => 2, + ); + + parent::buildOptionsForm($form, $form_state); + } + + public function query() { + // do nothing -- to override the parent query. + } + + /** + * Overrides Drupal\views\Plugin\views\field\FieldPluginBas::get_value() + */ + public function get_value($values, $field = NULL) { + // Note: 1 is subtracted from the counter start value below because the + // counter value is incremented by 1 at the end of this function. + $count = is_numeric($this->options['counter_start']) ? $this->options['counter_start'] - 1 : 0; + $pager = $this->view->pager; + // Get the base count of the pager. + if ($pager->use_pager()) { + $count += ($pager->get_items_per_page() * $pager->get_current_page() + $pager->set_offset()); + } + // Add the counter for the current site. + $count += $this->view->row_index + 1; + + return $count; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Custom.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Custom.php new file mode 100644 index 0000000..6c10baa --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Custom.php @@ -0,0 +1,51 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Custom. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * A handler to provide a field that is completely custom by the administrator. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "custom" + * ) + */ +class Custom extends FieldPluginBase { + + public function query() { + // do nothing -- to override the parent query. + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + // Override the alter text option to always alter the text. + $options['alter']['contains']['alter_text'] = array('default' => TRUE, 'bool' => TRUE); + $options['hide_alter_empty'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + // Remove the checkbox + unset($form['alter']['alter_text']); + unset($form['alter']['text']['#states']); + unset($form['alter']['help']['#states']); + $form['#pre_render'][] = 'views_handler_field_custom_pre_render_move_text'; + } + + function render($values) { + // Return the text, so the code never thinks the value is empty. + return $this->options['alter']['text']; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Date.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Date.php new file mode 100644 index 0000000..dd5f2fa --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Date.php @@ -0,0 +1,120 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Date. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * A handler to provide proper displays for dates. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "date" + * ) + */ +class Date extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['date_format'] = array('default' => 'small'); + $options['custom_date_format'] = array('default' => ''); + $options['timezone'] = array('default' => ''); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + + $date_formats = array(); + $date_types = system_get_date_types(); + foreach ($date_types as $key => $value) { + $date_formats[$value['type']] = check_plain(t($value['title'] . ' format')) . ': ' . format_date(REQUEST_TIME, $value['type']); + } + + $form['date_format'] = array( + '#type' => 'select', + '#title' => t('Date format'), + '#options' => $date_formats + array( + 'custom' => t('Custom'), + 'raw time ago' => t('Time ago'), + 'time ago' => t('Time ago (with "ago" appended)'), + 'raw time hence' => t('Time hence'), + 'time hence' => t('Time hence (with "hence" appended)'), + 'raw time span' => t('Time span (future dates have "-" prepended)'), + 'inverse time span' => t('Time span (past dates have "-" prepended)'), + 'time span' => t('Time span (with "ago/hence" appended)'), + ), + '#default_value' => isset($this->options['date_format']) ? $this->options['date_format'] : 'small', + ); + $form['custom_date_format'] = array( + '#type' => 'textfield', + '#title' => t('Custom date format'), + '#description' => t('If "Custom", see <a href="http://us.php.net/manual/en/function.date.php" target="_blank">the PHP docs</a> for date formats. Otherwise, enter the number of different time units to display, which defaults to 2.'), + '#default_value' => isset($this->options['custom_date_format']) ? $this->options['custom_date_format'] : '', + ); + // Setup #states for all possible date_formats on the custom_date_format form element. + foreach (array('custom', 'raw time ago', 'time ago', 'raw time hence', 'time hence', 'raw time span', 'time span', 'raw time span', 'inverse time span', 'time span') as $custom_date_possible) { + $form['custom_date_format']['#states']['visible'][] = array( + ':input[name="options[date_format]"]' => array('value' => $custom_date_possible), + ); + } + $form['timezone'] = array( + '#type' => 'select', + '#title' => t('Timezone'), + '#description' => t('Timezone to be used for date output.'), + '#options' => array('' => t('- Default site/user timezone -')) + system_time_zones(FALSE), + '#default_value' => $this->options['timezone'], + ); + foreach (array_merge(array('custom'), array_keys($date_formats)) as $timezone_date_formats) { + $form['timezone']['#states']['visible'][] = array( + ':input[name="options[date_format]"]' => array('value' => $timezone_date_formats), + ); + } + + parent::buildOptionsForm($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + $format = $this->options['date_format']; + if (in_array($format, array('custom', 'raw time ago', 'time ago', 'raw time hence', 'time hence', 'raw time span', 'time span', 'raw time span', 'inverse time span', 'time span'))) { + $custom_format = $this->options['custom_date_format']; + } + + if ($value) { + $timezone = !empty($this->options['timezone']) ? $this->options['timezone'] : NULL; + $time_diff = REQUEST_TIME - $value; // will be positive for a datetime in the past (ago), and negative for a datetime in the future (hence) + switch ($format) { + case 'raw time ago': + return format_interval($time_diff, is_numeric($custom_format) ? $custom_format : 2); + case 'time ago': + return t('%time ago', array('%time' => format_interval($time_diff, is_numeric($custom_format) ? $custom_format : 2))); + case 'raw time hence': + return format_interval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2); + case 'time hence': + return t('%time hence', array('%time' => format_interval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2))); + case 'raw time span': + return ($time_diff < 0 ? '-' : '') . format_interval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2); + case 'inverse time span': + return ($time_diff > 0 ? '-' : '') . format_interval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2); + case 'time span': + return t(($time_diff < 0 ? '%time hence' : '%time ago'), array('%time' => format_interval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2))); + case 'custom': + if ($custom_format == 'r') { + return format_date($value, $format, $custom_format, $timezone, 'en'); + } + return format_date($value, $format, $custom_format, $timezone); + default: + return format_date($value, $format, '', $timezone); + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/FieldPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/FieldPluginBase.php new file mode 100644 index 0000000..d73eafa --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/FieldPluginBase.php @@ -0,0 +1,1628 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\FieldPluginBase. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\views\Plugin\views\HandlerBase; +use Drupal\views\ViewExecutable; +use Drupal\Core\Annotation\Plugin; + +/** + * @defgroup views_field_handlers Views field handlers + * @{ + * Handlers to tell Views how to build and display fields. + * + */ + +/** + * Indicator of the render_text() method for rendering a single item. + * (If no render_item() is present). + */ +define('VIEWS_HANDLER_RENDER_TEXT_PHASE_SINGLE_ITEM', 0); + +/** + * Indicator of the render_text() method for rendering the whole element. + * (if no render_item() method is available). + */ +define('VIEWS_HANDLER_RENDER_TEXT_PHASE_COMPLETELY', 1); + +/** + * Indicator of the render_text() method for rendering the empty text. + */ +define('VIEWS_HANDLER_RENDER_TEXT_PHASE_EMPTY', 2); + +/** + * Base field handler that has no options and renders an unformatted field. + * + * Definition terms: + * - additional fields: An array of fields that should be added to the query + * for some purpose. The array is in the form of: + * array('identifier' => array('table' => tablename, + * 'field' => fieldname); as many fields as are necessary + * may be in this array. + * - click sortable: If TRUE, this field may be click sorted. + * + * @ingroup views_field_handlers + */ +abstract class FieldPluginBase extends HandlerBase { + + var $field_alias = 'unknown'; + var $aliases = array(); + + /** + * The field value prior to any rewriting. + * + * @var mixed + */ + public $original_value = NULL; + + /** + * @var array + * Stores additional fields which get's added to the query. + * The generated aliases are stored in $aliases. + */ + var $additional_fields = array(); + + /** + * Overrides Drupal\views\Plugin\views\HandlerBase::init(). + */ + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + $this->additional_fields = array(); + if (!empty($this->definition['additional fields'])) { + $this->additional_fields = $this->definition['additional fields']; + } + + if (!isset($this->options['exclude'])) { + $this->options['exclude'] = ''; + } + } + + /** + * Determine if this field can allow advanced rendering. + * + * Fields can set this to FALSE if they do not wish to allow + * token based rewriting or link-making. + */ + function allow_advanced_render() { + return TRUE; + } + + /** + * Called to add the field to a query. + */ + public function query() { + $this->ensureMyTable(); + // Add the field. + $params = $this->options['group_type'] != 'group' ? array('function' => $this->options['group_type']) : array(); + $this->field_alias = $this->query->add_field($this->tableAlias, $this->realField, NULL, $params); + + $this->add_additional_fields(); + } + + /** + * Add 'additional' fields to the query. + * + * @param $fields + * An array of fields. The key is an identifier used to later find the + * field alias used. The value is either a string in which case it's + * assumed to be a field on this handler's table; or it's an array in the + * form of + * @code array('table' => $tablename, 'field' => $fieldname) @endcode + */ + function add_additional_fields($fields = NULL) { + if (!isset($fields)) { + // notice check + if (empty($this->additional_fields)) { + return; + } + $fields = $this->additional_fields; + } + + $group_params = array(); + if ($this->options['group_type'] != 'group') { + $group_params = array( + 'function' => $this->options['group_type'], + ); + } + + if (!empty($fields) && is_array($fields)) { + foreach ($fields as $identifier => $info) { + if (is_array($info)) { + if (isset($info['table'])) { + $table_alias = $this->query->ensure_table($info['table'], $this->relationship); + } + else { + $table_alias = $this->tableAlias; + } + + if (empty($table_alias)) { + debug(t('Handler @handler tried to add additional_field @identifier but @table could not be added!', array('@handler' => $this->definition['handler'], '@identifier' => $identifier, '@table' => $info['table']))); + $this->aliases[$identifier] = 'broken'; + continue; + } + + $params = array(); + if (!empty($info['params'])) { + $params = $info['params']; + } + + $params += $group_params; + $this->aliases[$identifier] = $this->query->add_field($table_alias, $info['field'], NULL, $params); + } + else { + $this->aliases[$info] = $this->query->add_field($this->tableAlias, $info, NULL, $group_params); + } + } + } + } + + /** + * Called to determine what to tell the clicksorter. + */ + function click_sort($order) { + if (isset($this->field_alias)) { + // Since fields should always have themselves already added, just + // add a sort on the field. + $params = $this->options['group_type'] != 'group' ? array('function' => $this->options['group_type']) : array(); + $this->query->add_orderby(NULL, NULL, $order, $this->field_alias, $params); + } + } + + /** + * Determine if this field is click sortable. + */ + function click_sortable() { + return !empty($this->definition['click sortable']); + } + + /** + * Get this field's label. + */ + public function label() { + if (!isset($this->options['label'])) { + return ''; + } + return $this->options['label']; + } + + /** + * Return an HTML element based upon the field's element type. + */ + function element_type($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) { + if ($none_supported) { + if ($this->options['element_type'] === '0') { + return ''; + } + } + if ($this->options['element_type']) { + return check_plain($this->options['element_type']); + } + + if ($default_empty) { + return ''; + } + + if ($inline) { + return 'span'; + } + + if (isset($this->definition['element type'])) { + return $this->definition['element type']; + } + + return 'span'; + } + + /** + * Return an HTML element for the label based upon the field's element type. + */ + function element_label_type($none_supported = FALSE, $default_empty = FALSE) { + if ($none_supported) { + if ($this->options['element_label_type'] === '0') { + return ''; + } + } + if ($this->options['element_label_type']) { + return check_plain($this->options['element_label_type']); + } + + if ($default_empty) { + return ''; + } + + return 'span'; + } + + /** + * Return an HTML element for the wrapper based upon the field's element type. + */ + function element_wrapper_type($none_supported = FALSE, $default_empty = FALSE) { + if ($none_supported) { + if ($this->options['element_wrapper_type'] === '0') { + return 0; + } + } + if ($this->options['element_wrapper_type']) { + return check_plain($this->options['element_wrapper_type']); + } + + if ($default_empty) { + return ''; + } + + return 'div'; + } + + /** + * Provide a list of elements valid for field HTML. + * + * This function can be overridden by fields that want more or fewer + * elements available, though this seems like it would be an incredibly + * rare occurence. + */ + function get_elements() { + static $elements = NULL; + if (!isset($elements)) { + // @todo Add possible html5 elements. + $elements = array( + '' => t(' - Use default -'), + '0' => t('- None -') + ); + $elements += config('views.settings')->get('field_rewrite_elements'); + } + + return $elements; + } + + /** + * Return the class of the field. + */ + function element_classes($row_index = NULL) { + $classes = explode(' ', $this->options['element_class']); + foreach ($classes as &$class) { + $class = $this->tokenize_value($class, $row_index); + $class = drupal_clean_css_identifier($class); + } + return implode(' ', $classes); + } + + /** + * Replace a value with tokens from the last field. + * + * This function actually figures out which field was last and uses its + * tokens so they will all be available. + */ + function tokenize_value($value, $row_index = NULL) { + if (strpos($value, '[') !== FALSE || strpos($value, '!') !== FALSE || strpos($value, '%') !== FALSE) { + $fake_item = array( + 'alter_text' => TRUE, + 'text' => $value, + ); + + // Use isset() because empty() will trigger on 0 and 0 is + // the first row. + if (isset($row_index) && isset($this->view->style_plugin->render_tokens[$row_index])) { + $tokens = $this->view->style_plugin->render_tokens[$row_index]; + } + else { + // Get tokens from the last field. + $last_field = end($this->view->field); + if (isset($last_field->last_tokens)) { + $tokens = $last_field->last_tokens; + } + else { + $tokens = $last_field->get_render_tokens($fake_item); + } + } + + $value = strip_tags($this->render_altered($fake_item, $tokens)); + if (!empty($this->options['alter']['trim_whitespace'])) { + $value = trim($value); + } + } + + return $value; + } + + /** + * Return the class of the field's label. + */ + function element_label_classes($row_index = NULL) { + $classes = explode(' ', $this->options['element_label_class']); + foreach ($classes as &$class) { + $class = $this->tokenize_value($class, $row_index); + $class = drupal_clean_css_identifier($class); + } + return implode(' ', $classes); + } + + /** + * Return the class of the field's wrapper. + */ + function element_wrapper_classes($row_index = NULL) { + $classes = explode(' ', $this->options['element_wrapper_class']); + foreach ($classes as &$class) { + $class = $this->tokenize_value($class, $row_index); + $class = drupal_clean_css_identifier($class); + } + return implode(' ', $classes); + } + + /** + * Get the entity matching the current row and relationship. + * + * @param $values + * An object containing all retrieved values. + */ + function get_entity($values) { + $relationship_id = $this->options['relationship']; + if ($relationship_id == 'none') { + return $values->_entity; + } + else { + return $values->_relationship_entities[$relationship_id]; + } + } + + /** + * Get the value that's supposed to be rendered. + * + * This api exists so that other modules can easy set the values of the field + * without having the need to change the render method as well. + * + * @param $values + * An object containing all retrieved values. + * @param $field + * Optional name of the field where the value is stored. + */ + function get_value($values, $field = NULL) { + $alias = isset($field) ? $this->aliases[$field] : $this->field_alias; + if (isset($values->{$alias})) { + return $values->{$alias}; + } + } + + /** + * Determines if this field will be available as an option to group the result + * by in the style settings. + * + * @return bool + * TRUE if this field handler is groupable, otherwise FALSE. + */ + function use_string_group_by() { + return TRUE; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['label'] = array('default' => $this->definition['title'], 'translatable' => TRUE); + $options['exclude'] = array('default' => FALSE, 'bool' => TRUE); + $options['alter'] = array( + 'contains' => array( + 'alter_text' => array('default' => FALSE, 'bool' => TRUE), + 'text' => array('default' => '', 'translatable' => TRUE), + 'make_link' => array('default' => FALSE, 'bool' => TRUE), + 'path' => array('default' => ''), + 'absolute' => array('default' => FALSE, 'bool' => TRUE), + 'external' => array('default' => FALSE, 'bool' => TRUE), + 'replace_spaces' => array('default' => FALSE, 'bool' => TRUE), + 'path_case' => array('default' => 'none', 'translatable' => FALSE), + 'trim_whitespace' => array('default' => FALSE, 'bool' => TRUE), + 'alt' => array('default' => '', 'translatable' => TRUE), + 'rel' => array('default' => ''), + 'link_class' => array('default' => ''), + 'prefix' => array('default' => '', 'translatable' => TRUE), + 'suffix' => array('default' => '', 'translatable' => TRUE), + 'target' => array('default' => '', 'translatable' => TRUE), + 'nl2br' => array('default' => FALSE, 'bool' => TRUE), + 'max_length' => array('default' => ''), + 'word_boundary' => array('default' => TRUE, 'bool' => TRUE), + 'ellipsis' => array('default' => TRUE, 'bool' => TRUE), + 'more_link' => array('default' => FALSE, 'bool' => TRUE), + 'more_link_text' => array('default' => '', 'translatable' => TRUE), + 'more_link_path' => array('default' => ''), + 'strip_tags' => array('default' => FALSE, 'bool' => TRUE), + 'trim' => array('default' => FALSE, 'bool' => TRUE), + 'preserve_tags' => array('default' => ''), + 'html' => array('default' => FALSE, 'bool' => TRUE), + ), + ); + $options['element_type'] = array('default' => ''); + $options['element_class'] = array('default' => ''); + + $options['element_label_type'] = array('default' => ''); + $options['element_label_class'] = array('default' => ''); + $options['element_label_colon'] = array('default' => TRUE, 'bool' => TRUE); + + $options['element_wrapper_type'] = array('default' => ''); + $options['element_wrapper_class'] = array('default' => ''); + + $options['element_default_classes'] = array('default' => TRUE, 'bool' => TRUE); + + $options['empty'] = array('default' => '', 'translatable' => TRUE); + $options['hide_empty'] = array('default' => FALSE, 'bool' => TRUE); + $options['empty_zero'] = array('default' => FALSE, 'bool' => TRUE); + $options['hide_alter_empty'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * Performs some cleanup tasks on the options array before saving it. + */ + public function submitOptionsForm(&$form, &$form_state) { + $options = &$form_state['values']['options']; + $types = array('element_type', 'element_label_type', 'element_wrapper_type'); + $classes = array_combine(array('element_class', 'element_label_class', 'element_wrapper_class'), $types); + + foreach ($types as $type) { + if (!$options[$type . '_enable']) { + $options[$type] = ''; + } + } + + foreach ($classes as $class => $type) { + if (!$options[$class . '_enable'] || !$options[$type . '_enable']) { + $options[$class] = ''; + } + } + + if (empty($options['custom_label'])) { + $options['label'] = ''; + $options['element_label_colon'] = FALSE; + } + } + + /** + * Default options form that provides the label widget that all fields + * should have. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $label = $this->label(); + $form['custom_label'] = array( + '#type' => 'checkbox', + '#title' => t('Create a label'), + '#description' => t('Enable to create a label for this field.'), + '#default_value' => $label !== '', + '#weight' => -103, + ); + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#default_value' => $label, + '#states' => array( + 'visible' => array( + ':input[name="options[custom_label]"]' => array('checked' => TRUE), + ), + ), + '#weight' => -102, + ); + $form['element_label_colon'] = array( + '#type' => 'checkbox', + '#title' => t('Place a colon after the label'), + '#default_value' => $this->options['element_label_colon'], + '#states' => array( + 'visible' => array( + ':input[name="options[custom_label]"]' => array('checked' => TRUE), + ), + ), + '#weight' => -101, + ); + + $form['exclude'] = array( + '#type' => 'checkbox', + '#title' => t('Exclude from display'), + '#default_value' => $this->options['exclude'], + '#description' => t('Enable to load this field as hidden. Often used to group fields, or to use as token in another field.'), + '#weight' => -100, + ); + + $form['style_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Style settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 99, + ); + + $form['element_type_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Customize field HTML'), + '#default_value' => !empty($this->options['element_type']) || (string) $this->options['element_type'] == '0' || !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_type'] = array( + '#title' => t('HTML element'), + '#options' => $this->get_elements(), + '#type' => 'select', + '#default_value' => $this->options['element_type'], + '#description' => t('Choose the HTML element to wrap around this field, e.g. H1, H2, etc.'), + '#states' => array( + 'visible' => array( + ':input[name="options[element_type_enable]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'style_settings', + ); + + $form['element_class_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Create a CSS class'), + '#states' => array( + 'visible' => array( + ':input[name="options[element_type_enable]"]' => array('checked' => TRUE), + ), + ), + '#default_value' => !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_class'] = array( + '#title' => t('CSS class'), + '#description' => t('You may use token substitutions from the rewriting section in this class.'), + '#type' => 'textfield', + '#default_value' => $this->options['element_class'], + '#states' => array( + 'visible' => array( + ':input[name="options[element_type_enable]"]' => array('checked' => TRUE), + ':input[name="options[element_class_enable]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'style_settings', + ); + + $form['element_label_type_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Customize label HTML'), + '#default_value' => !empty($this->options['element_label_type']) || (string) $this->options['element_label_type'] == '0' || !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_label_type'] = array( + '#title' => t('Label HTML element'), + '#options' => $this->get_elements(FALSE), + '#type' => 'select', + '#default_value' => $this->options['element_label_type'], + '#description' => t('Choose the HTML element to wrap around this label, e.g. H1, H2, etc.'), + '#states' => array( + 'visible' => array( + ':input[name="options[element_label_type_enable]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'style_settings', + ); + $form['element_label_class_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Create a CSS class'), + '#states' => array( + 'visible' => array( + ':input[name="options[element_label_type_enable]"]' => array('checked' => TRUE), + ), + ), + '#default_value' => !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_label_class'] = array( + '#title' => t('CSS class'), + '#description' => t('You may use token substitutions from the rewriting section in this class.'), + '#type' => 'textfield', + '#default_value' => $this->options['element_label_class'], + '#states' => array( + 'visible' => array( + ':input[name="options[element_label_type_enable]"]' => array('checked' => TRUE), + ':input[name="options[element_label_class_enable]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'style_settings', + ); + + $form['element_wrapper_type_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Customize field and label wrapper HTML'), + '#default_value' => !empty($this->options['element_wrapper_type']) || (string) $this->options['element_wrapper_type'] == '0' || !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_wrapper_type'] = array( + '#title' => t('Wrapper HTML element'), + '#options' => $this->get_elements(FALSE), + '#type' => 'select', + '#default_value' => $this->options['element_wrapper_type'], + '#description' => t('Choose the HTML element to wrap around this field and label, e.g. H1, H2, etc. This may not be used if the field and label are not rendered together, such as with a table.'), + '#states' => array( + 'visible' => array( + ':input[name="options[element_wrapper_type_enable]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'style_settings', + ); + + $form['element_wrapper_class_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Create a CSS class'), + '#states' => array( + 'visible' => array( + ':input[name="options[element_wrapper_type_enable]"]' => array('checked' => TRUE), + ), + ), + '#default_value' => !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_wrapper_class'] = array( + '#title' => t('CSS class'), + '#description' => t('You may use token substitutions from the rewriting section in this class.'), + '#type' => 'textfield', + '#default_value' => $this->options['element_wrapper_class'], + '#states' => array( + 'visible' => array( + ':input[name="options[element_wrapper_class_enable]"]' => array('checked' => TRUE), + ':input[name="options[element_wrapper_type_enable]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'style_settings', + ); + + $form['element_default_classes'] = array( + '#type' => 'checkbox', + '#title' => t('Add default classes'), + '#default_value' => $this->options['element_default_classes'], + '#description' => t('Use default Views classes to identify the field, field label and field content.'), + '#fieldset' => 'style_settings', + ); + + $form['alter'] = array( + '#title' => t('Rewrite results'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 100, + ); + + if ($this->allow_advanced_render()) { + $form['alter']['#tree'] = TRUE; + $form['alter']['alter_text'] = array( + '#type' => 'checkbox', + '#title' => t('Rewrite the output of this field'), + '#description' => t('Enable to override the output of this field with custom text or replacement tokens.'), + '#default_value' => $this->options['alter']['alter_text'], + ); + + $form['alter']['text'] = array( + '#title' => t('Text'), + '#type' => 'textarea', + '#default_value' => $this->options['alter']['text'], + '#description' => t('The text to display for this field. You may include HTML. You may enter data from this view as per the "Replacement patterns" below.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][alter_text]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['alter']['make_link'] = array( + '#type' => 'checkbox', + '#title' => t('Output this field as a link'), + '#description' => t('If checked, this field will be made into a link. The destination must be given below.'), + '#default_value' => $this->options['alter']['make_link'], + ); + $form['alter']['path'] = array( + '#title' => t('Link path'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['path'], + '#description' => t('The Drupal path or absolute URL for this link. You may enter data from this view as per the "Replacement patterns" below.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + '#maxlength' => 255, + ); + $form['alter']['absolute'] = array( + '#type' => 'checkbox', + '#title' => t('Use absolute path'), + '#default_value' => $this->options['alter']['absolute'], + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['replace_spaces'] = array( + '#type' => 'checkbox', + '#title' => t('Replace spaces with dashes'), + '#default_value' => $this->options['alter']['replace_spaces'], + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['external'] = array( + '#type' => 'checkbox', + '#title' => t('External server URL'), + '#default_value' => $this->options['alter']['external'], + '#description' => t("Links to an external server using a full URL: e.g. 'http://www.example.com' or 'www.example.com'."), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['path_case'] = array( + '#type' => 'select', + '#title' => t('Transform the case'), + '#description' => t('When printing url paths, how to transform the case of the filter value.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + '#options' => array( + 'none' => t('No transform'), + 'upper' => t('Upper case'), + 'lower' => t('Lower case'), + 'ucfirst' => t('Capitalize first letter'), + 'ucwords' => t('Capitalize each word'), + ), + '#default_value' => $this->options['alter']['path_case'], + ); + $form['alter']['link_class'] = array( + '#title' => t('Link class'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['link_class'], + '#description' => t('The CSS class to apply to the link.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['alt'] = array( + '#title' => t('Title text'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['alt'], + '#description' => t('Text to place as "title" text which most browsers display as a tooltip when hovering over the link.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['rel'] = array( + '#title' => t('Rel Text'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['rel'], + '#description' => t('Include Rel attribute for use in lightbox2 or other javascript utility.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['prefix'] = array( + '#title' => t('Prefix text'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['prefix'], + '#description' => t('Any text to display before this link. You may include HTML.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['suffix'] = array( + '#title' => t('Suffix text'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['suffix'], + '#description' => t('Any text to display after this link. You may include HTML.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['target'] = array( + '#title' => t('Target'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['target'], + '#description' => t("Target of the link, such as _blank, _parent or an iframe's name. This field is rarely used."), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + ), + ); + + + // Get a list of the available fields and arguments for token replacement. + $options = array(); + foreach ($this->view->display_handler->getHandlers('field') as $field => $handler) { + $options[t('Fields')]["[$field]"] = $handler->adminLabel(); + // We only use fields up to (and including) this one. + if ($field == $this->options['id']) { + break; + } + } + $count = 0; // This lets us prepare the key as we want it printed. + foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) { + $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); + $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); + } + + $this->document_self_tokens($options[t('Fields')]); + + // Default text. + $output = t('<p>You must add some additional fields to this display before using this field. These fields may be marked as <em>Exclude from display</em> if you prefer. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.</p>'); + // We have some options, so make a list. + if (!empty($options)) { + $output = t('<p>The following tokens are available for this field. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields. +If you would like to have the characters \'[\' and \']\' please use the html entity codes \'%5B\' or \'%5D\' or they will get replaced with empty space.</p>'); + foreach (array_keys($options) as $type) { + if (!empty($options[$type])) { + $items = array(); + foreach ($options[$type] as $key => $value) { + $items[] = $key . ' == ' . $value; + } + $output .= theme('item_list', + array( + 'items' => $items, + 'type' => $type + )); + } + } + } + // This construct uses 'hidden' and not markup because process doesn't + // run. It also has an extra div because the dependency wants to hide + // the parent in situations like this, so we need a second div to + // make this work. + $form['alter']['help'] = array( + '#type' => 'fieldset', + '#title' => t('Replacement patterns'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#value' => $output, + '#states' => array( + 'visible' => array( + array( + ':input[name="options[alter][make_link]"]' => array('checked' => TRUE), + ), + array( + ':input[name="options[alter][alter_text]"]' => array('checked' => TRUE), + ), + array( + ':input[name="options[alter][more_link]"]' => array('checked' => TRUE), + ), + ), + ), + ); + + $form['alter']['trim'] = array( + '#type' => 'checkbox', + '#title' => t('Trim this field to a maximum length'), + '#description' => t('Enable to trim the field to a maximum length of characters'), + '#default_value' => $this->options['alter']['trim'], + ); + + $form['alter']['max_length'] = array( + '#title' => t('Maximum length'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['max_length'], + '#description' => t('The maximum number of characters this field can be.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][trim]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['alter']['word_boundary'] = array( + '#type' => 'checkbox', + '#title' => t('Trim only on a word boundary'), + '#description' => t('If checked, this field be trimmed only on a word boundary. This is guaranteed to be the maximum characters stated or less. If there are no word boundaries this could trim a field to nothing.'), + '#default_value' => $this->options['alter']['word_boundary'], + '#states' => array( + 'visible' => array( + ':input[name="options[alter][trim]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['alter']['ellipsis'] = array( + '#type' => 'checkbox', + '#title' => t('Add an ellipsis'), + '#description' => t('If checked, a "..." will be added if a field was trimmed.'), + '#default_value' => $this->options['alter']['ellipsis'], + '#states' => array( + 'visible' => array( + ':input[name="options[alter][trim]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['alter']['more_link'] = array( + '#type' => 'checkbox', + '#title' => t('Add a read-more link if output is trimmed.'), + '#description' => t('If checked, a read-more link will be added at the end of the trimmed output'), + '#default_value' => $this->options['alter']['more_link'], + '#states' => array( + 'visible' => array( + ':input[name="options[alter][trim]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['alter']['more_link_text'] = array( + '#type' => 'textfield', + '#title' => t('More link text'), + '#default_value' => $this->options['alter']['more_link_text'], + '#description' => t('The text which will be displayed on the more link. You may enter data from this view as per the "Replacement patterns" above.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][trim]"]' => array('checked' => TRUE), + ':input[name="options[alter][more_link]"]' => array('checked' => TRUE), + ), + ), + ); + $form['alter']['more_link_path'] = array( + '#type' => 'textfield', + '#title' => t('More link path'), + '#default_value' => $this->options['alter']['more_link_path'], + '#description' => t('The path which is used for the more link. You may enter data from this view as per the "Replacement patterns" above.'), + '#states' => array( + 'visible' => array( + ':input[name="options[alter][trim]"]' => array('checked' => TRUE), + ':input[name="options[alter][more_link]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['alter']['html'] = array( + '#type' => 'checkbox', + '#title' => t('Field can contain HTML'), + '#description' => t('If checked, HTML corrector will be run to ensure tags are properly closed after trimming.'), + '#default_value' => $this->options['alter']['html'], + '#states' => array( + 'visible' => array( + ':input[name="options[alter][trim]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['alter']['strip_tags'] = array( + '#type' => 'checkbox', + '#title' => t('Strip HTML tags'), + '#description' => t('If checked, all HTML tags will be stripped.'), + '#default_value' => $this->options['alter']['strip_tags'], + ); + + $form['alter']['preserve_tags'] = array( + '#type' => 'textfield', + '#title' => t('Preserve certain tags'), + '#description' => t('List the tags that need to be preserved during the stripping process. example "<p> <br>" which will preserve all p and br elements'), + '#default_value' => $this->options['alter']['preserve_tags'], + '#states' => array( + 'visible' => array( + ':input[name="options[alter][strip_tags]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['alter']['trim_whitespace'] = array( + '#type' => 'checkbox', + '#title' => t('Remove whitespace'), + '#description' => t('If checked, all whitespaces at the beginning and the end of the output will be removed.'), + '#default_value' => $this->options['alter']['trim_whitespace'], + ); + + $form['alter']['nl2br'] = array( + '#type' => 'checkbox', + '#title' => t('Convert newlines to HTML <br> tags'), + '#description' => t('If checked, all newlines chars (e.g. \n) are converted into HTML <br> tags.'), + '#default_value' => $this->options['alter']['nl2br'], + ); + } + + $form['empty_field_behavior'] = array( + '#type' => 'fieldset', + '#title' => t('No results behavior'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 100, + ); + + $form['empty'] = array( + '#type' => 'textarea', + '#title' => t('No results text'), + '#default_value' => $this->options['empty'], + '#description' => t('Provide text to display if this field contains an empty result. You may include HTML. You may enter data from this view as per the "Replacement patterns" in the "Rewrite Results" section below.'), + '#fieldset' => 'empty_field_behavior', + ); + + $form['empty_zero'] = array( + '#type' => 'checkbox', + '#title' => t('Count the number 0 as empty'), + '#default_value' => $this->options['empty_zero'], + '#description' => t('Enable to display the "no results text" if the field contains the number 0.'), + '#fieldset' => 'empty_field_behavior', + ); + + $form['hide_empty'] = array( + '#type' => 'checkbox', + '#title' => t('Hide if empty'), + '#default_value' => $this->options['hide_empty'], + '#description' => t('Enable to hide this field if it is empty. Note that the field label or rewritten output may still be displayed. To hide labels, check the style or row style settings for empty fields. To hide rewritten content, check the "Hide rewriting if empty" checkbox.'), + '#fieldset' => 'empty_field_behavior', + ); + + $form['hide_alter_empty'] = array( + '#type' => 'checkbox', + '#title' => t('Hide rewriting if empty'), + '#default_value' => $this->options['hide_alter_empty'], + '#description' => t('Do not display rewritten content if this field is empty.'), + '#fieldset' => 'empty_field_behavior', + ); + } + + /** + * Provide extra data to the administration form + */ + public function adminSummary() { + return $this->label(); + } + + /** + * Run before any fields are rendered. + * + * This gives the handlers some time to set up before any handler has + * been rendered. + * + * @param $values + * An array of all objects returned from the query. + */ + function pre_render(&$values) { } + + /** + * Render the field. + * + * @param $values + * The values retrieved from the database. + */ + function render($values) { + $value = $this->get_value($values); + return $this->sanitizeValue($value); + } + + /** + * Render a field using advanced settings. + * + * This renders a field normally, then decides if render-as-link and + * text-replacement rendering is necessary. + */ + function advanced_render($values) { + if ($this->allow_advanced_render() && method_exists($this, 'render_item')) { + $raw_items = $this->get_items($values); + // If there are no items, set the original value to NULL. + if (empty($raw_items)) { + $this->original_value = NULL; + } + } + else { + $value = $this->render($values); + if (is_array($value)) { + $value = drupal_render($value); + } + $this->last_render = $value; + $this->original_value = $value; + } + + if ($this->allow_advanced_render()) { + $tokens = NULL; + if (method_exists($this, 'render_item')) { + $items = array(); + foreach ($raw_items as $count => $item) { + $value = $this->render_item($count, $item); + if (is_array($value)) { + $value = drupal_render($value); + } + $this->last_render = $value; + $this->original_value = $this->last_render; + + $alter = $item + $this->options['alter']; + $alter['phase'] = VIEWS_HANDLER_RENDER_TEXT_PHASE_SINGLE_ITEM; + $items[] = $this->render_text($alter); + } + + $value = $this->render_items($items); + } + else { + $alter = array('phase' => VIEWS_HANDLER_RENDER_TEXT_PHASE_COMPLETELY) + $this->options['alter']; + $value = $this->render_text($alter); + } + + if (is_array($value)) { + $value = drupal_render($value); + } + // This happens here so that render_as_link can get the unaltered value of + // this field as a token rather than the altered value. + $this->last_render = $value; + } + + if (empty($this->last_render)) { + if ($this->is_value_empty($this->last_render, $this->options['empty_zero'], FALSE)) { + $alter = $this->options['alter']; + $alter['alter_text'] = 1; + $alter['text'] = $this->options['empty']; + $alter['phase'] = VIEWS_HANDLER_RENDER_TEXT_PHASE_EMPTY; + $this->last_render = $this->render_text($alter); + } + } + + return $this->last_render; + } + + /** + * Checks if a field value is empty. + * + * @param $value + * The field value. + * @param bool $empty_zero + * Whether or not this field is configured to consider 0 as empty. + * @param bool $no_skip_empty + * Whether or not to use empty() to check the value. + * + * @return bool + * TRUE if the value is considered empty, FALSE otherwise. + */ + function is_value_empty($value, $empty_zero, $no_skip_empty = TRUE) { + if (!isset($value)) { + $empty = TRUE; + } + else { + $empty = ($empty_zero || ($value !== 0 && $value !== '0')); + } + + if ($no_skip_empty) { + $empty = empty($value) && $empty; + } + return $empty; + } + + /** + * Perform an advanced text render for the item. + * + * This is separated out as some fields may render lists, and this allows + * each item to be handled individually. + */ + function render_text($alter) { + $value = $this->last_render; + + if (!empty($alter['alter_text']) && $alter['text'] !== '') { + $tokens = $this->get_render_tokens($alter); + $value = $this->render_altered($alter, $tokens); + } + + if (!empty($this->options['alter']['trim_whitespace'])) { + $value = trim($value); + } + + // Check if there should be no further rewrite for empty values. + $no_rewrite_for_empty = $this->options['hide_alter_empty'] && $this->is_value_empty($this->original_value, $this->options['empty_zero']); + + // Check whether the value is empty and return nothing, so the field isn't rendered. + // First check whether the field should be hidden if the value(hide_alter_empty = TRUE) /the rewrite is empty (hide_alter_empty = FALSE). + // For numeric values you can specify whether "0"/0 should be empty. + if ((($this->options['hide_empty'] && empty($value)) + || ($alter['phase'] != VIEWS_HANDLER_RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty)) + && $this->is_value_empty($value, $this->options['empty_zero'], FALSE)) { + return ''; + } + // Only in empty phase. + if ($alter['phase'] == VIEWS_HANDLER_RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) { + // If we got here then $alter contains the value of "No results text" + // and so there is nothing left to do. + return $value; + } + + if (!empty($alter['strip_tags'])) { + $value = strip_tags($value, $alter['preserve_tags']); + } + + $suffix = ''; + if (!empty($alter['trim']) && !empty($alter['max_length'])) { + $length = strlen($value); + $value = $this->render_trim_text($alter, $value); + if ($this->options['alter']['more_link'] && strlen($value) < $length) { + $tokens = $this->get_render_tokens($alter); + $more_link_text = $this->options['alter']['more_link_text'] ? $this->options['alter']['more_link_text'] : t('more'); + $more_link_text = strtr(filter_xss_admin($more_link_text), $tokens); + $more_link_path = $this->options['alter']['more_link_path']; + $more_link_path = strip_tags(decode_entities(strtr($more_link_path, $tokens))); + + // Take sure that paths which was runned through url() does work as well. + $base_path = base_path(); + // Checks whether the path starts with the base_path. + if (strpos($more_link_path, $base_path) === 0) { + $more_link_path = drupal_substr($more_link_path, drupal_strlen($base_path)); + } + + $more_link = l($more_link_text, $more_link_path, array('attributes' => array('class' => array('views-more-link')))); + + $suffix .= " " . $more_link; + } + } + + if (!empty($alter['nl2br'])) { + $value = nl2br($value); + } + $this->last_render_text = $value; + + if (!empty($alter['make_link']) && !empty($alter['path'])) { + if (!isset($tokens)) { + $tokens = $this->get_render_tokens($alter); + } + $value = $this->render_as_link($alter, $value, $tokens); + } + + return $value . $suffix; + } + + /** + * Render this field as altered text, from a fieldset set by the user. + */ + function render_altered($alter, $tokens) { + // Filter this right away as our substitutions are already sanitized. + $value = filter_xss_admin($alter['text']); + $value = strtr($value, $tokens); + + return $value; + } + + /** + * Trim the field down to the specified length. + */ + function render_trim_text($alter, $value) { + if (!empty($alter['strip_tags'])) { + // NOTE: It's possible that some external fields might override the + // element type so if someone from, say, CCK runs into a bug here, + // this may be why =) + $this->definition['element type'] = 'span'; + } + return views_trim_text($alter, $value); + } + + /** + * Render this field as a link, with the info from a fieldset set by + * the user. + */ + function render_as_link($alter, $text, $tokens) { + $value = ''; + + if (!empty($alter['prefix'])) { + $value .= filter_xss_admin(strtr($alter['prefix'], $tokens)); + } + + $options = array( + 'html' => TRUE, + 'absolute' => !empty($alter['absolute']) ? TRUE : FALSE, + ); + + // $path will be run through check_url() by l() so we do not need to + // sanitize it ourselves. + $path = $alter['path']; + + // strip_tags() removes <front>, so check whether its different to front. + if ($path != '<front>') { + // Use strip tags as there should never be HTML in the path. + // However, we need to preserve special characters like " that + // were removed by check_plain(). + $path = strip_tags(decode_entities(strtr($path, $tokens))); + + if (!empty($alter['path_case']) && $alter['path_case'] != 'none') { + $path = $this->caseTransform($path, $this->options['alter']['path_case']); + } + + if (!empty($alter['replace_spaces'])) { + $path = str_replace(' ', '-', $path); + } + } + + // Parse the URL and move any query and fragment parameters out of the path. + $url = parse_url($path); + + // Seriously malformed URLs may return FALSE or empty arrays. + if (empty($url)) { + return $text; + } + + // If the path is empty do not build a link around the given text and return + // it as is. + // http://www.example.com URLs will not have a $url['path'], so check host as well. + if (empty($url['path']) && empty($url['host']) && empty($url['fragment'])) { + return $text; + } + + // If no scheme is provided in the $path, assign the default 'http://'. + // This allows a url of 'www.example.com' to be converted to 'http://www.example.com'. + // Only do this on for external URLs. + if ($alter['external']) { + if (!isset($url['scheme'])) { + // There is no scheme, add the default 'http://' to the $path. + $path = "http://$path"; + // Reset the $url array to include the new scheme. + $url = parse_url($path); + } + } + + if (isset($url['query'])) { + $path = strtr($path, array('?' . $url['query'] => '')); + $query = drupal_get_query_array($url['query']); + // Remove query parameters that were assigned a query string replacement + // token for which there is no value available. + foreach ($query as $param => $val) { + if ($val == '%' . $param) { + unset($query[$param]); + } + } + $options['query'] = $query; + } + if (isset($url['fragment'])) { + $path = strtr($path, array('#' . $url['fragment'] => '')); + // If the path is empty we want to have a fragment for the current site. + if ($path == '') { + $options['external'] = TRUE; + } + $options['fragment'] = $url['fragment']; + } + + $alt = strtr($alter['alt'], $tokens); + // Set the title attribute of the link only if it improves accessibility + if ($alt && $alt != $text) { + $options['attributes']['title'] = decode_entities($alt); + } + + $class = strtr($alter['link_class'], $tokens); + if ($class) { + $options['attributes']['class'] = array($class); + } + + if (!empty($alter['rel']) && $rel = strtr($alter['rel'], $tokens)) { + $options['attributes']['rel'] = $rel; + } + + $target = check_plain(trim(strtr($alter['target'], $tokens))); + if (!empty($target)) { + $options['attributes']['target'] = $target; + } + + // Allow the addition of arbitrary attributes to links. Additional attributes + // currently can only be altered in preprocessors and not within the UI. + if (isset($alter['link_attributes']) && is_array($alter['link_attributes'])) { + foreach ($alter['link_attributes'] as $key => $attribute) { + if (!isset($options['attributes'][$key])) { + $options['attributes'][$key] = strtr($attribute, $tokens); + } + } + } + + // If the query and fragment were programatically assigned overwrite any + // parsed values. + if (isset($alter['query'])) { + // Convert the query to a string, perform token replacement, and then + // convert back to an array form for l(). + $options['query'] = drupal_http_build_query($alter['query']); + $options['query'] = strtr($options['query'], $tokens); + $options['query'] = drupal_get_query_array($options['query']); + } + if (isset($alter['alias'])) { + // Alias is a boolean field, so no token. + $options['alias'] = $alter['alias']; + } + if (isset($alter['fragment'])) { + $options['fragment'] = strtr($alter['fragment'], $tokens); + } + if (isset($alter['language'])) { + $options['language'] = $alter['language']; + } + + // If the url came from entity_uri(), pass along the required options. + if (isset($alter['entity'])) { + $options['entity'] = $alter['entity']; + } + if (isset($alter['entity_type'])) { + $options['entity_type'] = $alter['entity_type']; + } + + $value .= l($text, $path, $options); + + if (!empty($alter['suffix'])) { + $value .= filter_xss_admin(strtr($alter['suffix'], $tokens)); + } + + return $value; + } + + /** + * Get the 'render' tokens to use for advanced rendering. + * + * This runs through all of the fields and arguments that + * are available and gets their values. This will then be + * used in one giant str_replace(). + */ + function get_render_tokens($item) { + $tokens = array(); + if (!empty($this->view->build_info['substitutions'])) { + $tokens = $this->view->build_info['substitutions']; + } + $count = 0; + foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) { + $token = '%' . ++$count; + if (!isset($tokens[$token])) { + $tokens[$token] = ''; + } + + // Use strip tags as there should never be HTML in the path. + // However, we need to preserve special characters like " that + // were removed by check_plain(). + $tokens['!' . $count] = isset($this->view->args[$count - 1]) ? strip_tags(decode_entities($this->view->args[$count - 1])) : ''; + } + + // Get flattened set of tokens for any array depth in $_GET parameters. + $tokens += $this->get_token_values_recursive(drupal_container()->get('request')->query->all()); + + // Now add replacements for our fields. + foreach ($this->view->display_handler->getHandlers('field') as $field => $handler) { + if (isset($handler->last_render)) { + $tokens["[$field]"] = $handler->last_render; + } + else { + $tokens["[$field]"] = ''; + } + if (!empty($item)) { + $this->add_self_tokens($tokens, $item); + } + + // We only use fields up to (and including) this one. + if ($field == $this->options['id']) { + break; + } + } + + // Store the tokens for the row so we can reference them later if necessary. + $this->view->style_plugin->render_tokens[$this->view->row_index] = $tokens; + $this->last_tokens = $tokens; + + return $tokens; + } + + /** + * Recursive function to add replacements for nested query string parameters. + * + * E.g. if you pass in the following array: + * array( + * 'foo' => array( + * 'a' => 'value', + * 'b' => 'value', + * ), + * 'bar' => array( + * 'a' => 'value', + * 'b' => array( + * 'c' => value, + * ), + * ), + * ); + * + * Would yield the following array of tokens: + * array( + * '%foo_a' => 'value' + * '%foo_b' => 'value' + * '%bar_a' => 'value' + * '%bar_b_c' => 'value' + * ); + * + * @param $array + * An array of values. + * + * @param $parent_keys + * An array of parent keys. This will represent the array depth. + * + * @return + * An array of available tokens, with nested keys representative of the array structure. + */ + function get_token_values_recursive(array $array, array $parent_keys = array()) { + $tokens = array(); + + foreach ($array as $param => $val) { + if (is_array($val)) { + // Copy parent_keys array, so we don't affect other elements of this + // iteration. + $child_parent_keys = $parent_keys; + $child_parent_keys[] = $param; + // Get the child tokens. + $child_tokens = $this->get_token_values_recursive($val, $child_parent_keys); + // Add them to the current tokens array. + $tokens += $child_tokens; + } + else { + // Create a token key based on array element structure. + $token_string = !empty($parent_keys) ? implode('_', $parent_keys) . '_' . $param : $param; + $tokens['%' . $token_string] = strip_tags(decode_entities($val)); + } + } + + return $tokens; + } + + /** + * Add any special tokens this field might use for itself. + * + * This method is intended to be overridden by items that generate + * fields as a list. For example, the field that displays all terms + * on a node might have tokens for the tid and the term. + * + * By convention, tokens should follow the format of [token-subtoken] + * where token is the field ID and subtoken is the field. If the + * field ID is terms, then the tokens might be [terms-tid] and [terms-name]. + */ + function add_self_tokens(&$tokens, $item) { } + + /** + * Document any special tokens this field might use for itself. + * + * @see add_self_tokens() + */ + function document_self_tokens(&$tokens) { } + + /** + * Call out to the theme() function, which probably just calls render() but + * allows sites to override output fairly easily. + */ + function theme($values) { + return theme($this->themeFunctions(), + array( + 'view' => $this->view, + 'field' => $this, + 'row' => $values + )); + } + + public function themeFunctions() { + $themes = array(); + $hook = 'views_view_field'; + + $display = $this->view->display_handler->display; + + if (!empty($display)) { + $themes[] = $hook . '__' . $this->view->storage->name . '__' . $display['id'] . '__' . $this->options['id']; + $themes[] = $hook . '__' . $this->view->storage->name . '__' . $display['id']; + $themes[] = $hook . '__' . $display['id'] . '__' . $this->options['id']; + $themes[] = $hook . '__' . $display['id']; + if ($display['id'] != $display['display_plugin']) { + $themes[] = $hook . '__' . $this->view->storage->name . '__' . $display['display_plugin'] . '__' . $this->options['id']; + $themes[] = $hook . '__' . $this->view->storage->name . '__' . $display['display_plugin']; + $themes[] = $hook . '__' . $display['display_plugin'] . '__' . $this->options['id']; + $themes[] = $hook . '__' . $display['display_plugin']; + } + } + $themes[] = $hook . '__' . $this->view->storage->name . '__' . $this->options['id']; + $themes[] = $hook . '__' . $this->view->storage->name; + $themes[] = $hook . '__' . $this->options['id']; + $themes[] = $hook; + + return $themes; + } + + public function adminLabel($short = FALSE) { + return $this->getField(parent::adminLabel($short)); + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/FileSize.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/FileSize.php new file mode 100644 index 0000000..e52f134 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/FileSize.php @@ -0,0 +1,59 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\FileSize. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Render a numeric value as a size. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "file_size" + * ) + */ +class FileSize extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['file_size_display'] = array('default' => 'formatted'); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['file_size_display'] = array( + '#title' => t('File size display'), + '#type' => 'select', + '#options' => array( + 'formatted' => t('Formatted (in KB or MB)'), + 'bytes' => t('Raw bytes'), + ), + ); + } + + function render($values) { + $value = $this->get_value($values); + if ($value) { + switch ($this->options['file_size_display']) { + case 'bytes': + return $value; + case 'formatted': + default: + return format_size($value); + } + } + else { + return ''; + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/MachineName.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/MachineName.php new file mode 100644 index 0000000..14a81c9 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/MachineName.php @@ -0,0 +1,83 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\MachineName. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Field handler whichs allows to show machine name content as human name. + * @ingroup views_field_handlers + * + * Definition items: + * - options callback: The function to call in order to generate the value options. If omitted, the options 'Yes' and 'No' will be used. + * - options arguments: An array of arguments to pass to the options callback. + * + * @Plugin( + * id = "machine_name" + * ) + */ +class MachineName extends FieldPluginBase { + + /** + * @var array Stores the available options. + */ + var $value_options; + + function get_value_options() { + if (isset($this->value_options)) { + return; + } + + if (isset($this->definition['options callback']) && is_callable($this->definition['options callback'])) { + if (isset($this->definition['options arguments']) && is_array($this->definition['options arguments'])) { + $this->value_options = call_user_func_array($this->definition['options callback'], $this->definition['options arguments']); + } + else { + $this->value_options = call_user_func($this->definition['options callback']); + } + } + else { + $this->value_options = array(); + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['machine_name'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['machine_name'] = array( + '#title' => t('Output machine name'), + '#description' => t('Display field as machine name.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['machine_name']), + ); + } + + function pre_render(&$values) { + $this->get_value_options(); + } + + function render($values) { + $value = $values->{$this->field_alias}; + if (!empty($this->options['machine_name']) || !isset($this->value_options[$value])) { + $result = check_plain($value); + } + else { + $result = $this->value_options[$value]; + } + + return $result; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Markup.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Markup.php new file mode 100644 index 0000000..d82a529 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Markup.php @@ -0,0 +1,67 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Markup. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; +use Drupal\views\ViewExecutable; + +/** + * A handler to run a field through check_markup, using a companion + * format field. + * + * - format: (REQUIRED) Either a string format id to use for this field or an + * array('field' => {$field}) where $field is the field in this table + * used to control the format such as the 'format' field in the node, + * which goes with the 'body' field. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "markup" + * ) + */ +class Markup extends FieldPluginBase { + + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + $this->format = $this->definition['format']; + + $this->additional_fields = array(); + if (is_array($this->format)) { + $this->additional_fields['format'] = $this->format; + } + } + + function render($values) { + $value = $this->get_value($values); + if (is_array($this->format)) { + $format = $this->get_value($values, 'format'); + } + else { + $format = $this->format; + } + if ($value) { + $value = str_replace('<!--break-->', '', $value); + return check_markup($value, $format, ''); + } + } + + function element_type($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) { + if ($inline) { + return 'span'; + } + + if (isset($this->definition['element type'])) { + return $this->definition['element type']; + } + + return 'div'; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Numeric.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Numeric.php new file mode 100644 index 0000000..fc06d72 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Numeric.php @@ -0,0 +1,159 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Numeric. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Render a field as a numeric value + * + * Definition terms: + * - float: If true this field contains a decimal value. If unset this field + * will be assumed to be integer. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "numeric" + * ) + */ +class Numeric extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['set_precision'] = array('default' => FALSE, 'bool' => TRUE); + $options['precision'] = array('default' => 0); + $options['decimal'] = array('default' => '.', 'translatable' => TRUE); + $options['separator'] = array('default' => ',', 'translatable' => TRUE); + $options['format_plural'] = array('default' => FALSE, 'bool' => TRUE); + $options['format_plural_singular'] = array('default' => '1'); + $options['format_plural_plural'] = array('default' => '@count'); + $options['prefix'] = array('default' => '', 'translatable' => TRUE); + $options['suffix'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + if (!empty($this->definition['float'])) { + $form['set_precision'] = array( + '#type' => 'checkbox', + '#title' => t('Round'), + '#description' => t('If checked, the number will be rounded.'), + '#default_value' => $this->options['set_precision'], + ); + $form['precision'] = array( + '#type' => 'textfield', + '#title' => t('Precision'), + '#default_value' => $this->options['precision'], + '#description' => t('Specify how many digits to print after the decimal point.'), + '#states' => array( + 'visible' => array( + ':input[name="options[set_precision]"]' => array('checked' => TRUE), + ), + ), + '#size' => 2, + ); + $form['decimal'] = array( + '#type' => 'textfield', + '#title' => t('Decimal point'), + '#default_value' => $this->options['decimal'], + '#description' => t('What single character to use as a decimal point.'), + '#size' => 2, + ); + } + $form['separator'] = array( + '#type' => 'select', + '#title' => t('Thousands marker'), + '#options' => array( + '' => t('- None -'), + ',' => t('Comma'), + ' ' => t('Space'), + '.' => t('Decimal'), + '\'' => t('Apostrophe'), + ), + '#default_value' => $this->options['separator'], + '#description' => t('What single character to use as the thousands separator.'), + '#size' => 2, + ); + $form['format_plural'] = array( + '#type' => 'checkbox', + '#title' => t('Format plural'), + '#description' => t('If checked, special handling will be used for plurality.'), + '#default_value' => $this->options['format_plural'], + ); + $form['format_plural_singular'] = array( + '#type' => 'textfield', + '#title' => t('Singular form'), + '#default_value' => $this->options['format_plural_singular'], + '#description' => t('Text to use for the singular form.'), + '#states' => array( + 'visible' => array( + ':input[name="options[format_plural]"]' => array('checked' => TRUE), + ), + ), + ); + $form['format_plural_plural'] = array( + '#type' => 'textfield', + '#title' => t('Plural form'), + '#default_value' => $this->options['format_plural_plural'], + '#description' => t('Text to use for the plural form, @count will be replaced with the value.'), + '#states' => array( + 'visible' => array( + ':input[name="options[format_plural]"]' => array('checked' => TRUE), + ), + ), + ); + $form['prefix'] = array( + '#type' => 'textfield', + '#title' => t('Prefix'), + '#default_value' => $this->options['prefix'], + '#description' => t('Text to put before the number, such as currency symbol.'), + ); + $form['suffix'] = array( + '#type' => 'textfield', + '#title' => t('Suffix'), + '#default_value' => $this->options['suffix'], + '#description' => t('Text to put after the number, such as currency symbol.'), + ); + + parent::buildOptionsForm($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + if (!empty($this->options['set_precision'])) { + $value = number_format($value, $this->options['precision'], $this->options['decimal'], $this->options['separator']); + } + else { + $remainder = abs($value) - intval(abs($value)); + $value = $value > 0 ? floor($value) : ceil($value); + $value = number_format($value, 0, '', $this->options['separator']); + if ($remainder) { + // The substr may not be locale safe. + $value .= $this->options['decimal'] . substr($remainder, 2); + } + } + + // Check to see if hiding should happen before adding prefix and suffix. + if ($this->options['hide_empty'] && empty($value) && ($value !== 0 || $this->options['empty_zero'])) { + return ''; + } + + // Should we format as a plural. + if (!empty($this->options['format_plural'])) { + $value = format_plural($value, $this->options['format_plural_singular'], $this->options['format_plural_plural']); + } + + return $this->sanitizeValue($this->options['prefix'], 'xss') + . $this->sanitizeValue($value) + . $this->sanitizeValue($this->options['suffix'], 'xss'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/PrerenderList.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/PrerenderList.php new file mode 100644 index 0000000..90b91e8 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/PrerenderList.php @@ -0,0 +1,125 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\PrerenderList. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Field handler to provide a list of items. + * + * The items are expected to be loaded by a child object during pre_render, + * and 'my field' is expected to be the pointer to the items in the list. + * + * Items to render should be in a list in $this->items + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "prerender_list" + * ) + */ +class PrerenderList extends FieldPluginBase { + + /** + * Stores all items which are used to render the items. + * It should be keyed first by the id of the base table, for example nid. + * The second key is the id of the thing which is displayed multiple times + * per row, for example the tid. + * + * @var array + */ + var $items = array(); + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['type'] = array('default' => 'separator'); + $options['separator'] = array('default' => ', '); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['type'] = array( + '#type' => 'radios', + '#title' => t('Display type'), + '#options' => array( + 'ul' => t('Unordered list'), + 'ol' => t('Ordered list'), + 'separator' => t('Simple separator'), + ), + '#default_value' => $this->options['type'], + ); + + $form['separator'] = array( + '#type' => 'textfield', + '#title' => t('Separator'), + '#default_value' => $this->options['separator'], + '#states' => array( + 'visible' => array( + ':input[name="options[type]"]' => array('value' => 'separator'), + ), + ), + ); + parent::buildOptionsForm($form, $form_state); + } + + /** + * Render all items in this field together. + * + * When using advanced render, each possible item in the list is rendered + * individually. Then the items are all pasted together. + */ + function render_items($items) { + if (!empty($items)) { + if ($this->options['type'] == 'separator') { + return implode($this->sanitizeValue($this->options['separator'], 'xss_admin'), $items); + } + else { + return theme('item_list', + array( + 'items' => $items, + 'title' => NULL, + 'type' => $this->options['type'] + )); + } + } + } + + /** + * Return an array of items for the field. + * + * Items should be stored in the result array, if possible, as an array + * with 'value' as the actual displayable value of the item, plus + * any items that might be found in the 'alter' options array for + * creating links, such as 'path', 'fragment', 'query' etc, such a thing + * is to be made. Additionally, items that might be turned into tokens + * should also be in this array. + */ + function get_items($values) { + $field = $this->get_value($values); + if (!empty($this->items[$field])) { + return $this->items[$field]; + } + + return array(); + } + + /** + * Determine if advanced rendering is allowed. + * + * By default, advanced rendering will NOT be allowed if the class + * inheriting from this does not implement a 'render_items' method. + */ + function allow_advanced_render() { + // Note that the advanced render bits also use the presence of + // this method to determine if it needs to render items as a list. + return method_exists($this, 'render_item'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Serialized.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Serialized.php new file mode 100644 index 0000000..1598a8a --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Serialized.php @@ -0,0 +1,78 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Serialized. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Field handler to show data of serialized fields. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "serialized" + * ) + */ +class Serialized extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['format'] = array('default' => 'unserialized'); + $options['key'] = array('default' => ''); + return $options; + } + + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['format'] = array( + '#type' => 'select', + '#title' => t('Display format'), + '#description' => t('How should the serialized data be displayed. You can choose a custom array/object key or a print_r on the full output.'), + '#options' => array( + 'unserialized' => t('Full data (unserialized)'), + 'serialized' => t('Full data (serialized)'), + 'key' => t('A certain key'), + ), + '#default_value' => $this->options['format'], + ); + $form['key'] = array( + '#type' => 'textfield', + '#title' => t('Which key should be displayed'), + '#default_value' => $this->options['key'], + '#states' => array( + 'visible' => array( + ':input[name="options[format]"]' => array('value' => 'key'), + ), + ), + ); + } + + public function validateOptionsForm(&$form, &$form_state) { + // Require a key if the format is key. + if ($form_state['values']['options']['format'] == 'key' && $form_state['values']['options']['key'] == '') { + form_error($form['key'], t('You have to enter a key if you want to display a key of the data.')); + } + } + + function render($values) { + $value = $values->{$this->field_alias}; + + if ($this->options['format'] == 'unserialized') { + return check_plain(print_r(unserialize($value), TRUE)); + } + elseif ($this->options['format'] == 'key' && !empty($this->options['key'])) { + $value = (array) unserialize($value); + return check_plain($value[$this->options['key']]); + } + + return $value; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Standard.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Standard.php new file mode 100644 index 0000000..52e7b5c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Standard.php @@ -0,0 +1,23 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Standard. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Default implementation of the base field plugin. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "standard" + * ) + */ +class Standard extends FieldPluginBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/TimeInterval.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/TimeInterval.php new file mode 100644 index 0000000..ff53056 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/TimeInterval.php @@ -0,0 +1,47 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\TimeInterval. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * A handler to provide proper displays for time intervals. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "time_interval" + * ) + */ +class TimeInterval extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['granularity'] = array('default' => 2); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['granularity'] = array( + '#type' => 'textfield', + '#title' => t('Granularity'), + '#description' => t('How many different units to display in the string.'), + '#default_value' => $this->options['granularity'], + ); + } + + function render($values) { + $value = $values->{$this->field_alias}; + return format_interval($value, isset($this->options['granularity']) ? $this->options['granularity'] : 2); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Url.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Url.php new file mode 100644 index 0000000..8a0183d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Url.php @@ -0,0 +1,53 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Url. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; + +/** + * Field handler to provide simple renderer that turns a URL into a clickable link. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "url" + * ) + */ +class Url extends FieldPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['display_as_link'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide link to the page being visited. + */ + public function buildOptionsForm(&$form, &$form_state) { + $form['display_as_link'] = array( + '#title' => t('Display as link'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['display_as_link']), + ); + parent::buildOptionsForm($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + if (!empty($this->options['display_as_link'])) { + return l($this->sanitizeValue($value), $value, array('html' => TRUE)); + } + else { + return $this->sanitizeValue($value, 'url'); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/field/Xss.php b/core/modules/views/lib/Drupal/views/Plugin/views/field/Xss.php new file mode 100644 index 0000000..3d06bb9 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/field/Xss.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\field\Xss + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\views\Plugin\views\field\FieldPluginBase; +use Drupal\Core\Annotation\Plugin; + +/** + * A handler to run a field through simple XSS filtering. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "xss" + * ) + */ +class Xss extends FieldPluginBase { + + function render($values) { + $value = $this->get_value($values); + return $this->sanitizeValue($value, 'xss'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/BooleanOperator.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/BooleanOperator.php new file mode 100644 index 0000000..dcf6df3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/BooleanOperator.php @@ -0,0 +1,194 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\BooleanOperator. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; +use Drupal\views\ViewExecutable; + +/** + * Simple filter to handle matching of boolean values + * + * Definition items: + * - label: (REQUIRED) The label for the checkbox. + * - type: For basic 'true false' types, an item can specify the following: + * - true-false: True/false (this is the default) + * - yes-no: Yes/No + * - on-off: On/Off + * - enabled-disabled: Enabled/Disabled + * - accept null: Treat a NULL value as false. + * - use_equal: If you use this flag the query will use = 1 instead of <> 0. + * This might be helpful for performance reasons. + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "boolean" + * ) + */ +class BooleanOperator extends FilterPluginBase { + + // exposed filter options + var $always_multiple = TRUE; + // Don't display empty space where the operator would be. + var $no_operator = TRUE; + // Whether to accept NULL as a false value or not + var $accept_null = FALSE; + + /** + * Overrides Drupal\views\Plugin\views\filter\FilterPluginBase::init(). + */ + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + $this->value_value = t('True'); + if (isset($this->definition['label'])) { + $this->value_value = $this->definition['label']; + } + if (isset($this->definition['accept null'])) { + $this->accept_null = (bool) $this->definition['accept null']; + } + elseif (isset($this->definition['accept_null'])) { + $this->accept_null = (bool) $this->definition['accept_null']; + } + $this->value_options = NULL; + } + + /** + * Return the possible options for this filter. + * + * Child classes should override this function to set the possible values + * for the filter. Since this is a boolean filter, the array should have + * two possible keys: 1 for "True" and 0 for "False", although the labels + * can be whatever makes sense for the filter. These values are used for + * configuring the filter, when the filter is exposed, and in the admin + * summary of the filter. Normally, this should be static data, but if it's + * dynamic for some reason, child classes should use a guard to reduce + * database hits as much as possible. + */ + function get_value_options() { + if (isset($this->definition['type'])) { + if ($this->definition['type'] == 'yes-no') { + $this->value_options = array(1 => t('Yes'), 0 => t('No')); + } + if ($this->definition['type'] == 'on-off') { + $this->value_options = array(1 => t('On'), 0 => t('Off')); + } + if ($this->definition['type'] == 'enabled-disabled') { + $this->value_options = array(1 => t('Enabled'), 0 => t('Disabled')); + } + } + + // Provide a fallback if the above didn't set anything. + if (!isset($this->value_options)) { + $this->value_options = array(1 => t('True'), 0 => t('False')); + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['value']['default'] = FALSE; + + return $options; + } + + function operator_form(&$form, &$form_state) { + $form['operator'] = array(); + } + + function value_form(&$form, &$form_state) { + if (empty($this->value_options)) { + // Initialize the array of possible values for this filter. + $this->get_value_options(); + } + if (!empty($form_state['exposed'])) { + // Exposed filter: use a select box to save space. + $filter_form_type = 'select'; + } + else { + // Configuring a filter: use radios for clarity. + $filter_form_type = 'radios'; + } + $form['value'] = array( + '#type' => $filter_form_type, + '#title' => $this->value_value, + '#options' => $this->value_options, + '#default_value' => $this->value, + ); + if (!empty($this->options['exposed'])) { + $identifier = $this->options['expose']['identifier']; + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $this->value; + } + // If we're configuring an exposed filter, add an <Any> option. + if (empty($form_state['exposed']) || empty($this->options['expose']['required'])) { + $any_label = config('views.settings')->get('ui.exposed_filter_any_label') == 'old_any' ? '<Any>' : t('- Any -'); + if ($form['value']['#type'] != 'select') { + $any_label = check_plain($any_label); + } + $form['value']['#options'] = array('All' => $any_label) + $form['value']['#options']; + } + } + } + + function value_validate($form, &$form_state) { + if ($form_state['values']['options']['value'] == 'All' && !empty($form_state['values']['options']['expose']['required'])) { + form_set_error('value', t('You must select a value unless this is an non-required exposed filter.')); + } + } + + public function adminSummary() { + if ($this->isAGroup()) { + return t('grouped'); + } + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + if (empty($this->value_options)) { + $this->get_value_options(); + } + // Now that we have the valid options for this filter, just return the + // human-readable label based on the current value. The value_options + // array is keyed with either 0 or 1, so if the current value is not + // empty, use the label for 1, and if it's empty, use the label for 0. + return $this->value_options[!empty($this->value)]; + } + + public function defaultExposeOptions() { + parent::defaultExposeOptions(); + $this->options['expose']['operator_id'] = ''; + $this->options['expose']['label'] = $this->value_value; + $this->options['expose']['required'] = TRUE; + } + + public function query() { + $this->ensureMyTable(); + $field = "$this->tableAlias.$this->realField"; + + if (empty($this->value)) { + if ($this->accept_null) { + $or = db_or() + ->condition($field, 0, '=') + ->condition($field, NULL, 'IS NULL'); + $this->query->add_where($this->options['group'], $or); + } + else { + $this->query->add_where($this->options['group'], $field, 0, '='); + } + } + else { + if (!empty($this->definition['use_equal'])) { + $this->query->add_where($this->options['group'], $field, 1, '='); + } + else { + $this->query->add_where($this->options['group'], $field, 0, '<>'); + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/BooleanOperatorString.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/BooleanOperatorString.php new file mode 100644 index 0000000..f26e245 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/BooleanOperatorString.php @@ -0,0 +1,45 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\BooleanOperatorString. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; + +/** + * Simple filter to handle matching of boolean values. + * + * This handler checks to see if a string field is empty (equal to '') or not. + * It is otherwise identical to the parent operator. + * + * Definition items: + * - label: (REQUIRED) The label for the checkbox. + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "boolean_string" + * ) + */ +class BooleanOperatorString extends BooleanOperator { + + public function query() { + $this->ensureMyTable(); + $where = "$this->tableAlias.$this->realField "; + + if (empty($this->value)) { + $where .= "= ''"; + if ($this->accept_null) { + $where = '(' . $where . " OR $this->tableAlias.$this->realField IS NULL)"; + } + } + else { + $where .= "<> ''"; + } + $this->query->add_where($this->options['group'], $where); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/Broken.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Broken.php new file mode 100644 index 0000000..02eb9a5 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Broken.php @@ -0,0 +1,43 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\Broken. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; +use Drupal\views\ViewExecutable; + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "broken" + * ) + */ +class Broken extends FilterPluginBase { + + public function adminLabel($short = FALSE) { + return t('Broken/missing handler'); + } + + public function init(ViewExecutable $view, &$options) { } + public function defineOptions() { return array(); } + public function ensureMyTable() { /* No table to ensure! */ } + public function query($group_by = FALSE) { /* No query to run */ } + public function buildOptionsForm(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '<div class="form-item description">' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '</div>', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + public function broken() { return TRUE; } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/Combine.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Combine.php new file mode 100644 index 0000000..c43d5b3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Combine.php @@ -0,0 +1,147 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\Combine. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; + +/** + * Filter handler which allows to search on multiple fields. + * + * @ingroup views_field_handlers + * + * @Plugin( + * id = "combine" + * ) + */ +class Combine extends String { + + /** + * @var views_plugin_query_default + */ + var $query; + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['fields'] = array('default' => array()); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $this->view->initStyle(); + + // Allow to choose all fields as possible + if ($this->view->style_plugin->usesFields()) { + $options = array(); + foreach ($this->view->display_handler->getHandlers('field') as $name => $field) { + $options[$name] = $field->adminLabel(TRUE); + } + if ($options) { + $form['fields'] = array( + '#type' => 'select', + '#title' => t('Choose fields to combine for filtering'), + '#description' => t("This filter doesn't work for very special field handlers."), + '#multiple' => TRUE, + '#options' => $options, + '#default_value' => $this->options['fields'], + ); + } + else { + form_set_error('', t('You have to add some fields to be able to use this filter.')); + } + } + } + + public function query() { + $this->view->_build('field'); + $fields = array(); + // Only add the fields if they have a proper field and table alias. + foreach ($this->options['fields'] as $id) { + $field = $this->view->field[$id]; + // Always add the table of the selected fields to be sure a table alias exists. + $field->ensureMyTable(); + if (!empty($field->field_alias) && !empty($field->field_alias)) { + $fields[] = "$field->tableAlias.$field->realField"; + } + } + if ($fields) { + $count = count($fields); + $seperated_fields = array(); + foreach ($fields as $key => $field) { + $seperated_fields[] = $field; + if ($key < $count-1) { + $seperated_fields[] = "' '"; + } + } + $expression = implode(', ', $seperated_fields); + $expression = "CONCAT_WS(' ', $expression)"; + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($expression); + } + } + } + + // By default things like op_equal uses add_where, that doesn't support + // complex expressions, so override all operators. + + function op_equal($field) { + $placeholder = $this->placeholder(); + $operator = $this->operator(); + $this->query->add_where_expression($this->options['group'], "$field $operator $placeholder", array($placeholder => $this->value)); + } + + function op_contains($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field LIKE $placeholder", array($placeholder => '%' . db_like($this->value) . '%')); + } + + function op_starts($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field LIKE $placeholder", array($placeholder => db_like($this->value) . '%')); + } + + function op_not_starts($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field NOT LIKE $placeholder", array($placeholder => db_like($this->value) . '%')); + } + + function op_ends($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field LIKE $placeholder", array($placeholder => '%' . db_like($this->value))); + } + + function op_not_ends($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field NOT LIKE $placeholder", array($placeholder => '%' . db_like($this->value))); + } + + function op_not($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field NOT LIKE $placeholder", array($placeholder => '%' . db_like($this->value) . '%')); + } + + function op_regex($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field RLIKE $placeholder", array($placeholder => $this->value)); + } + + function op_empty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_where_expression($this->options['group'], "$field $operator"); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/Date.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Date.php new file mode 100644 index 0000000..d8f4c29 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Date.php @@ -0,0 +1,193 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\Date. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; + +/** + * Filter to handle dates stored as a timestamp. + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "date" + * ) + */ +class Date extends Numeric { + + protected function defineOptions() { + $options = parent::defineOptions(); + + // value is already set up properly, we're just adding our new field to it. + $options['value']['contains']['type']['default'] = 'date'; + + return $options; + } + + /** + * Add a type selector to the value form + */ + function value_form(&$form, &$form_state) { + if (empty($form_state['exposed'])) { + $form['value']['type'] = array( + '#type' => 'radios', + '#title' => t('Value type'), + '#options' => array( + 'date' => t('A date in any machine readable format. CCYY-MM-DD HH:MM:SS is preferred.'), + 'offset' => t('An offset from the current time such as "!example1" or "!example2"', array('!example1' => '+1 day', '!example2' => '-2 hours -30 minutes')), + ), + '#default_value' => !empty($this->value['type']) ? $this->value['type'] : 'date', + ); + } + parent::value_form($form, $form_state); + } + + public function validateOptionsForm(&$form, &$form_state) { + parent::validateOptionsForm($form, $form_state); + + if (!empty($this->options['exposed']) && empty($form_state['values']['options']['expose']['required'])) { + // Who cares what the value is if it's exposed and non-required. + return; + } + + $this->validateValidTime($form['value'], $form_state['values']['options']['operator'], $form_state['values']['options']['value']); + } + + public function validateExposed(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + if (empty($this->options['expose']['required'])) { + // Who cares what the value is if it's exposed and non-required. + return; + } + + $value = &$form_state['values'][$this->options['expose']['identifier']]; + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id'])) { + $operator = $form_state['values'][$this->options['expose']['operator_id']]; + } + else { + $operator = $this->operator; + } + + $this->validateValidTime($this->options['expose']['identifier'], $operator, $value); + + } + + /** + * Validate that the time values convert to something usable. + */ + public function validateValidTime(&$form, $operator, $value) { + $operators = $this->operators(); + + if ($operators[$operator]['values'] == 1) { + $convert = strtotime($value['value']); + if (!empty($form['value']) && ($convert == -1 || $convert === FALSE)) { + form_error($form['value'], t('Invalid date format.')); + } + } + elseif ($operators[$operator]['values'] == 2) { + $min = strtotime($value['min']); + if ($min == -1 || $min === FALSE) { + form_error($form['min'], t('Invalid date format.')); + } + $max = strtotime($value['max']); + if ($max == -1 || $max === FALSE) { + form_error($form['max'], t('Invalid date format.')); + } + } + } + + /** + * Validate the build group options form. + */ + function build_group_validate($form, &$form_state) { + // Special case to validate grouped date filters, this is because the + // $group['value'] array contains the type of filter (date or offset) + // and therefore the number of items the comparission has to be done + // against 'one' instead of 'zero'. + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + // Check if the title is defined but value wasn't defined. + if (!empty($group['title'])) { + if ((!is_array($group['value']) && empty($group['value'])) || (is_array($group['value']) && count(array_filter($group['value'])) == 1)) { + form_error($form['group_info']['group_items'][$id]['value'], t('The value is required if title for this item is defined.')); + } + } + + // Check if the value is defined but title wasn't defined. + if ((!is_array($group['value']) && !empty($group['value'])) || (is_array($group['value']) && count(array_filter($group['value'])) > 1)) { + if (empty($group['title'])) { + form_error($form['group_info']['group_items'][$id]['title'], t('The title is required if value for this item is defined.')); + } + } + } + } + } + + + public function acceptExposedInput($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + // Store this because it will get overwritten. + $type = $this->value['type']; + $rc = parent::acceptExposedInput($input); + + // Don't filter if value(s) are empty. + $operators = $this->operators(); + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id'])) { + $operator = $input[$this->options['expose']['operator_id']]; + } + else { + $operator = $this->operator; + } + + if ($operators[$operator]['values'] == 1) { + if ($this->value['value'] == '') { + return FALSE; + } + } + else { + if ($this->value['min'] == '' || $this->value['max'] == '') { + return FALSE; + } + } + + // restore what got overwritten by the parent. + $this->value['type'] = $type; + return $rc; + } + + function op_between($field) { + $a = intval(strtotime($this->value['min'], 0)); + $b = intval(strtotime($this->value['max'], 0)); + + if ($this->value['type'] == 'offset') { + $a = '***CURRENT_TIME***' . sprintf('%+d', $a); // keep sign + $b = '***CURRENT_TIME***' . sprintf('%+d', $b); // keep sign + } + // This is safe because we are manually scrubbing the values. + // It is necessary to do it this way because $a and $b are formulas when using an offset. + $operator = strtoupper($this->operator); + $this->query->add_where_expression($this->options['group'], "$field $operator $a AND $b"); + } + + function op_simple($field) { + $value = intval(strtotime($this->value['value'], 0)); + if (!empty($this->value['type']) && $this->value['type'] == 'offset') { + $value = '***CURRENT_TIME***' . sprintf('%+d', $value); // keep sign + } + // This is safe because we are manually scrubbing the value. + // It is necessary to do it this way because $value is a formula when using an offset. + $this->query->add_where_expression($this->options['group'], "$field $this->operator $value"); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/Equality.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Equality.php new file mode 100644 index 0000000..c357801 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Equality.php @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\Equality. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; + +/** + * Simple filter to handle equal to / not equal to filters + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "equality" + * ) + */ +class Equality extends FilterPluginBase { + + // exposed filter options + var $always_multiple = TRUE; + + /** + * Provide simple equality operator + */ + function operator_options() { + return array( + '=' => t('Is equal to'), + '!=' => t('Is not equal to'), + ); + } + + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + $form['value'] = array( + '#type' => 'textfield', + '#title' => t('Value'), + '#size' => 30, + '#default_value' => $this->value, + ); + + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + if (!isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $this->value; + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/FilterPluginBase.php new file mode 100644 index 0000000..aa3b8d8 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/FilterPluginBase.php @@ -0,0 +1,1394 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\FilterPluginBase. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\views\Plugin\views\HandlerBase; +use Drupal\Core\Annotation\Plugin; +use Drupal\views\ViewExecutable; + +/** + * @defgroup views_filter_handlers Views filter handlers + * @{ + * Handlers to tell Views how to filter queries. + * + * Definition items: + * - allow empty: If true, the 'IS NULL' and 'IS NOT NULL' operators become + * available as standard operators. + * + * Object flags: + * You can set some specific behavior by setting up the following flags on + * your custom class. + * + * - always_multiple: + * Disable the possibility to force a single value. + * - no_operator: + * Disable the possibility to use operators. + * - always_required: + * Disable the possibility to allow a exposed input to be optional. + */ + +/** + * Base class for filters. + * + * @ingroup views_filter_handlers + */ +abstract class FilterPluginBase extends HandlerBase { + + /** + * Contains the actual value of the field,either configured in the views ui + * or entered in the exposed filters. + */ + var $value = NULL; + + /** + * Contains the operator which is used on the query. + */ + var $operator = '='; + + /** + * Contains the information of the selected item in a gruped filter. + */ + var $group_info = NULL; + + /** + * @var bool + * Disable the possibility to force a single value. + */ + var $always_multiple = FALSE; + + /** + * @var bool + * Disable the possibility to use operators. + */ + var $no_operator = FALSE; + + /** + * @var bool + * Disable the possibility to allow a exposed input to be optional. + */ + var $always_required = FALSE; + + /** + * Provide some extra help to get the operator/value easier to use. + * + * This likely has to be overridden by filters which are more complex + * than simple operator/value. + */ + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + $this->operator = $this->options['operator']; + $this->value = $this->options['value']; + $this->group_info = $this->options['group_info']['default_group']; + + // Set the default value of the operator ID. + if (!empty($options['exposed']) && !empty($options['expose']['operator']) && !isset($options['expose']['operator_id'])) { + $this->options['expose']['operator_id'] = $options['expose']['operator']; + } + + if ($this->multipleExposedInput()) { + $this->group_info = array_filter($options['group_info']['default_group_multiple']); + $this->options['expose']['multiple'] = TRUE; + } + + + // If there are relationships in the view, allow empty should be true + // so that we can do IS NULL checks on items. Not all filters respect + // allow empty, but string and numeric do and that covers enough. + if ($this->view->display_handler->getOption('relationships')) { + $this->definition['allow empty'] = TRUE; + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['operator'] = array('default' => '='); + $options['value'] = array('default' => ''); + $options['group'] = array('default' => '1'); + $options['exposed'] = array('default' => FALSE, 'bool' => TRUE); + $options['expose'] = array( + 'contains' => array( + 'operator_id' => array('default' => FALSE), + 'label' => array('default' => '', 'translatable' => TRUE), + 'description' => array('default' => '', 'translatable' => TRUE), + 'use_operator' => array('default' => FALSE, 'bool' => TRUE), + 'operator' => array('default' => ''), + 'identifier' => array('default' => ''), + 'required' => array('default' => FALSE, 'bool' => TRUE), + 'remember' => array('default' => FALSE, 'bool' => TRUE), + 'multiple' => array('default' => FALSE, 'bool' => TRUE), + 'remember_roles' => array('default' => array( + DRUPAL_AUTHENTICATED_RID => DRUPAL_AUTHENTICATED_RID, + )), + ), + ); + + // A group is a combination of a filter, an operator and a value + // operating like a single filter. + // Users can choose from a select box which group they want to apply. + // Views will filter the view according to the defined values. + // Because it acts as a standard filter, we have to define + // an identifier and other settings like the widget and the label. + // This settings are saved in another array to allow users to switch + // between a normal filter and a group of filters with a single click. + $options['is_grouped'] = array('default' => FALSE, 'bool' => TRUE); + $options['group_info'] = array( + 'contains' => array( + 'label' => array('default' => '', 'translatable' => TRUE), + 'description' => array('default' => '', 'translatable' => TRUE), + 'identifier' => array('default' => ''), + 'optional' => array('default' => TRUE, 'bool' => TRUE), + 'widget' => array('default' => 'select'), + 'multiple' => array('default' => FALSE, 'bool' => TRUE), + 'remember' => array('default' => 0), + 'default_group' => array('default' => 'All'), + 'default_group_multiple' => array('default' => array()), + 'group_items' => array('default' => array()), + ), + ); + + return $options; + } + + /** + * Display the filter on the administrative summary + */ + public function adminSummary() { + return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value); + } + + /** + * Determine if a filter can be exposed. + */ + public function canExpose() { return TRUE; } + + /** + * Determine if a filter can be converted into a group. + * Only exposed filters with operators available can be converted into groups. + */ + function can_build_group() { + return $this->isExposed() && (count($this->operator_options()) > 0); + } + + /** + * Returns TRUE if the exposed filter works like a grouped filter. + */ + public function isAGroup() { + return $this->isExposed() && !empty($this->options['is_grouped']); + } + + /** + * Provide the basic form which calls through to subforms. + * If overridden, it is best to call through to the parent, + * or to at least make sure all of the functions in this form + * are called. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + if ($this->canExpose()) { + $this->showExposeButton($form, $form_state); + } + if ($this->can_build_group()) { + $this->show_build_group_button($form, $form_state); + } + $form['clear_markup_start'] = array( + '#markup' => '<div class="clearfix">', + ); + if ($this->isAGroup()) { + if ($this->can_build_group()) { + $form['clear_markup_start'] = array( + '#markup' => '<div class="clearfix">', + ); + // Render the build group form. + $this->show_build_group_form($form, $form_state); + $form['clear_markup_end'] = array( + '#markup' => '</div>', + ); + } + } + else { + // Add the subform from operator_form(). + $this->show_operator_form($form, $form_state); + // Add the subform from value_form(). + $this->show_value_form($form, $form_state); + $form['clear_markup_end'] = array( + '#markup' => '</div>', + ); + if ($this->canExpose()) { + // Add the subform from buildExposeForm(). + $this->showExposeForm($form, $form_state); + } + } + } + + /** + * Simple validate handler + */ + public function validateOptionsForm(&$form, &$form_state) { + $this->operator_validate($form, $form_state); + $this->value_validate($form, $form_state); + if (!empty($this->options['exposed']) && !$this->isAGroup()) { + $this->validateExposeForm($form, $form_state); + } + if ($this->isAGroup()) { + $this->build_group_validate($form, $form_state); + } + } + + /** + * Simple submit handler + */ + public function submitOptionsForm(&$form, &$form_state) { + unset($form_state['values']['expose_button']); // don't store this. + unset($form_state['values']['group_button']); // don't store this. + if (!$this->isAGroup()) { + $this->operator_submit($form, $form_state); + $this->value_submit($form, $form_state); + } + if (!empty($this->options['exposed'])) { + $this->submitExposeForm($form, $form_state); + } + if ($this->isAGroup()) { + $this->build_group_submit($form, $form_state); + } + } + + /** + * Shortcut to display the operator form. + */ + function show_operator_form(&$form, &$form_state) { + $this->operator_form($form, $form_state); + $form['operator']['#prefix'] = '<div class="views-group-box views-left-30">'; + $form['operator']['#suffix'] = '</div>'; + } + + /** + * Options form subform for setting the operator. + * + * This may be overridden by child classes, and it must + * define $form['operator']; + * + * @see buildOptionsForm() + */ + function operator_form(&$form, &$form_state) { + $options = $this->operator_options(); + if (!empty($options)) { + $form['operator'] = array( + '#type' => count($options) < 10 ? 'radios' : 'select', + '#title' => t('Operator'), + '#default_value' => $this->operator, + '#options' => $options, + ); + } + } + + /** + * Provide a list of options for the default operator form. + * Should be overridden by classes that don't override operator_form + */ + function operator_options() { return array(); } + + /** + * Validate the operator form. + */ + function operator_validate($form, &$form_state) { } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function operator_submit($form, &$form_state) { } + + /** + * Shortcut to display the value form. + */ + function show_value_form(&$form, &$form_state) { + $this->value_form($form, $form_state); + if (empty($this->no_operator)) { + $form['value']['#prefix'] = '<div class="views-group-box views-right-70">' . (isset($form['value']['#prefix']) ? $form['value']['#prefix'] : ''); + $form['value']['#suffix'] = (isset($form['value']['#suffix']) ? $form['value']['#suffix'] : '') . '</div>'; + } + } + + /** + * Options form subform for setting options. + * + * This should be overridden by all child classes and it must + * define $form['value'] + * + * @see buildOptionsForm() + */ + function value_form(&$form, &$form_state) { $form['value'] = array(); } + + /** + * Validate the options form. + */ + function value_validate($form, &$form_state) { } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function value_submit($form, &$form_state) { } + + /** + * Shortcut to display the exposed options form. + */ + function show_build_group_form(&$form, &$form_state) { + if (empty($this->options['is_grouped'])) { + return; + } + + $this->build_group_form($form, $form_state); + + // When we click the expose button, we add new gadgets to the form but they + // have no data in $_POST so their defaults get wiped out. This prevents + // these defaults from getting wiped out. This setting will only be TRUE + // during a 2nd pass rerender. + if (!empty($form_state['force_build_group_options'])) { + foreach (element_children($form['group_info']) as $id) { + if (isset($form['group_info'][$id]['#default_value']) && !isset($form['group_info'][$id]['#value'])) { + $form['group_info'][$id]['#value'] = $form['group_info'][$id]['#default_value']; + } + } + } + } + + /** + * Shortcut to display the build_group/hide button. + */ + function show_build_group_button(&$form, &$form_state) { + + $form['group_button'] = array( + '#prefix' => '<div class="views-grouped clearfix">', + '#suffix' => '</div>', + // Should always come after the description and the relationship. + '#weight' => -190, + ); + + $grouped_description = t('Grouped filters allow a choice between predefined operator|value pairs.'); + $form['group_button']['radios'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('js-only')), + ); + $form['group_button']['radios']['radios'] = array( + '#title' => t('Filter type to expose'), + '#description' => $grouped_description, + '#type' => 'radios', + '#options' => array( + t('Single filter'), + t('Grouped filters'), + ), + ); + + if (empty($this->options['is_grouped'])) { + $form['group_button']['markup'] = array( + '#markup' => '<div class="description grouped-description">' . $grouped_description . '</div>', + ); + $form['group_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Grouped filters'), + '#submit' => array('views_ui_config_item_form_build_group'), + ); + $form['group_button']['radios']['radios']['#default_value'] = 0; + } + else { + $form['group_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Single filter'), + '#submit' => array('views_ui_config_item_form_build_group'), + ); + $form['group_button']['radios']['radios']['#default_value'] = 1; + } + } + /** + * Shortcut to display the expose/hide button. + */ + public function showExposeButton(&$form, &$form_state) { + $form['expose_button'] = array( + '#prefix' => '<div class="views-expose clearfix">', + '#suffix' => '</div>', + // Should always come after the description and the relationship. + '#weight' => -200, + ); + + // Add a checkbox for JS users, which will have behavior attached to it + // so it can replace the button. + $form['expose_button']['checkbox'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('js-only')), + ); + $form['expose_button']['checkbox']['checkbox'] = array( + '#title' => t('Expose this filter to visitors, to allow them to change it'), + '#type' => 'checkbox', + ); + + // Then add the button itself. + if (empty($this->options['exposed'])) { + $form['expose_button']['markup'] = array( + '#markup' => '<div class="description exposed-description">' . t('This filter is not exposed. Expose it to allow the users to change it.') . '</div>', + ); + $form['expose_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Expose filter'), + '#submit' => array('views_ui_config_item_form_expose'), + ); + $form['expose_button']['checkbox']['checkbox']['#default_value'] = 0; + } + else { + $form['expose_button']['markup'] = array( + '#markup' => '<div class="description exposed-description">' . t('This filter is exposed. If you hide it, users will not be able to change it.') . '</div>', + ); + $form['expose_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Hide filter'), + '#submit' => array('views_ui_config_item_form_expose'), + ); + $form['expose_button']['checkbox']['checkbox']['#default_value'] = 1; + } + } + + /** + * Options form subform for exposed filter options. + * + * @see buildOptionsForm() + */ + public function buildExposeForm(&$form, &$form_state) { + $form['#theme'] = 'views_ui_expose_filter_form'; + // #flatten will move everything from $form['expose'][$key] to $form[$key] + // prior to rendering. That's why the pre_render for it needs to run first, + // so that when the next pre_render (the one for fieldsets) runs, it gets + // the flattened data. + array_unshift($form['#pre_render'], 'views_ui_pre_render_flatten_data'); + $form['expose']['#flatten'] = TRUE; + + if (empty($this->always_required)) { + $form['expose']['required'] = array( + '#type' => 'checkbox', + '#title' => t('Required'), + '#default_value' => $this->options['expose']['required'], + ); + } + else { + $form['expose']['required'] = array( + '#type' => 'value', + '#value' => TRUE, + ); + } + $form['expose']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['label'], + '#title' => t('Label'), + '#size' => 40, + ); + + $form['expose']['description'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['description'], + '#title' => t('Description'), + '#size' => 60, + ); + + if (!empty($form['operator']['#type'])) { + // Increase the width of the left (operator) column. + $form['operator']['#prefix'] = '<div class="views-group-box views-left-40">'; + $form['operator']['#suffix'] = '</div>'; + $form['value']['#prefix'] = '<div class="views-group-box views-right-60">'; + $form['value']['#suffix'] = '</div>'; + + $form['expose']['use_operator'] = array( + '#type' => 'checkbox', + '#title' => t('Expose operator'), + '#description' => t('Allow the user to choose the operator.'), + '#default_value' => !empty($this->options['expose']['use_operator']), + ); + $form['expose']['operator_id'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['operator_id'], + '#title' => t('Operator identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this operator.'), + '#states' => array( + 'visible' => array( + ':input[name="options[expose][use_operator]"]' => array('checked' => TRUE), + ), + ), + '#fieldset' => 'more', + ); + } + else { + $form['expose']['operator_id'] = array( + '#type' => 'value', + '#value' => '', + ); + } + + if (empty($this->always_multiple)) { + $form['expose']['multiple'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple selections'), + '#description' => t('Enable to allow users to select multiple items.'), + '#default_value' => $this->options['expose']['multiple'], + ); + } + $form['expose']['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember the last selection'), + '#description' => t('Enable to remember the last selection made by the user.'), + '#default_value' => $this->options['expose']['remember'], + ); + + $role_options = array_map('check_plain', user_roles()); + $form['expose']['remember_roles'] = array( + '#type' => 'checkboxes', + '#title' => t('User roles'), + '#description' => t('Remember exposed selection only for the selected user role(s). If you select no roles, the exposed data will never be stored.'), + '#default_value' => $this->options['expose']['remember_roles'], + '#options' => $role_options, + '#states' => array( + 'invisible' => array( + ':input[name="options[expose][remember]"]' => array('checked' => FALSE), + ), + ), + ); + + $form['expose']['identifier'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['identifier'], + '#title' => t('Filter identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this filter. Cannot be blank.'), + '#fieldset' => 'more', + ); + } + + /** + * Validate the options form. + */ + public function validateExposeForm($form, &$form_state) { + if (empty($form_state['values']['options']['expose']['identifier'])) { + form_error($form['expose']['identifier'], t('The identifier is required if the filter is exposed.')); + } + + if (!empty($form_state['values']['options']['expose']['identifier']) && $form_state['values']['options']['expose']['identifier'] == 'value') { + form_error($form['expose']['identifier'], t('This identifier is not allowed.')); + } + + if (!$this->view->display_handler->isIdentifierUnique($form_state['id'], $form_state['values']['options']['expose']['identifier'])) { + form_error($form['expose']['identifier'], t('This identifier is used by another handler.')); + } + } + + /** + * Validate the build group options form. + */ + function build_group_validate($form, &$form_state) { + if (!empty($form_state['values']['options']['group_info'])) { + if (empty($form_state['values']['options']['group_info']['identifier'])) { + form_error($form['group_info']['identifier'], t('The identifier is required if the filter is exposed.')); + } + + if (!empty($form_state['values']['options']['group_info']['identifier']) && $form_state['values']['options']['group_info']['identifier'] == 'value') { + form_error($form['group_info']['identifier'], t('This identifier is not allowed.')); + } + + if (!$this->view->display_handler->isIdentifierUnique($form_state['id'], $form_state['values']['options']['group_info']['identifier'])) { + form_error($form['group_info']['identifier'], t('This identifier is used by another handler.')); + } + } + + if (!empty($form_state['values']['options']['group_info']['group_items'])) { + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + + // Check if the title is defined but value wasn't defined. + if (!empty($group['title'])) { + if ((!is_array($group['value']) && trim($group['value']) == "") || + (is_array($group['value']) && count(array_filter($group['value'], '_views_array_filter_zero')) == 0)) { + form_error($form['group_info']['group_items'][$id]['value'], + t('The value is required if title for this item is defined.')); + } + } + + // Check if the value is defined but title wasn't defined. + if ((!is_array($group['value']) && trim($group['value']) != "") || + (is_array($group['value']) && count(array_filter($group['value'], '_views_array_filter_zero')) > 0)) { + if (empty($group['title'])) { + form_error($form['group_info']['group_items'][$id]['title'], + t('The title is required if value for this item is defined.')); + } + } + } + } + } + } + + /** + * Save new group items, re-enumerates and remove groups marked to delete. + */ + function build_group_submit($form, &$form_state) { + $groups = array(); + uasort($form_state['values']['options']['group_info']['group_items'], 'drupal_sort_weight'); + // Filter out removed items. + + // Start from 1 to avoid problems with #default_value in the widget. + $new_id = 1; + $new_default = 'All'; + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + // Don't store this. + unset($group['remove']); + unset($group['weight']); + $groups[$new_id] = $group; + + if ($form_state['values']['options']['group_info']['default_group'] === $id) { + $new_default = $new_id; + } + } + $new_id++; + } + if ($new_default != 'All') { + $form_state['values']['options']['group_info']['default_group'] = $new_default; + } + $filter_default_multiple = array_filter($form_state['values']['options']['group_info']['default_group_multiple']); + $form_state['values']['options']['group_info']['default_group_multiple'] = $filter_default_multiple; + + $form_state['values']['options']['group_info']['group_items'] = $groups; + } + + /** + * Provide default options for exposed filters. + */ + public function defaultExposeOptions() { + $this->options['expose'] = array( + 'use_operator' => FALSE, + 'operator' => $this->options['id'] . '_op', + 'identifier' => $this->options['id'], + 'label' => $this->definition['title'], + 'description' => NULL, + 'remember' => FALSE, + 'multiple' => FALSE, + 'required' => FALSE, + ); + } + + /** + * Provide default options for exposed filters. + */ + function build_group_options() { + $this->options['group_info'] = array( + 'label' => $this->definition['title'], + 'description' => NULL, + 'identifier' => $this->options['id'], + 'optional' => TRUE, + 'widget' => 'select', + 'multiple' => FALSE, + 'remember' => FALSE, + 'default_group' => 'All', + 'default_group_multiple' => array(), + 'group_items' => array(), + ); + } + + /** + * Build a form containing a group of operator | values to apply as a + * single filter. + */ + function group_form(&$form, &$form_state) { + if (!empty($this->options['group_info']['optional']) && !$this->multipleExposedInput()) { + + $old_any = $this->options['group_info']['widget'] == 'select' ? '<Any>' : '<Any>'; + $any_label = config('views.settings')->get('ui.exposed_filter_any_label') == 'old_any' ? $old_any : t('- Any -'); + $groups = array('All' => $any_label); + } + foreach ($this->options['group_info']['group_items'] as $id => $group) { + if (!empty($group['title'])) { + $groups[$id] = $id != 'All' ? t($group['title']) : $group['title']; + } + } + + if (count($groups)) { + $value = $this->options['group_info']['identifier']; + + $form[$value] = array( + '#type' => $this->options['group_info']['widget'], + '#default_value' => $this->group_info, + '#options' => $groups, + ); + if (!empty($this->options['group_info']['multiple'])) { + if (count($groups) < 5) { + $form[$value]['#type'] = 'checkboxes'; + } + else { + $form[$value]['#type'] = 'select'; + $form[$value]['#size'] = 5; + $form[$value]['#multiple'] = TRUE; + } + unset($form[$value]['#default_value']); + if (empty($form_state['input'])) { + $form_state['input'][$value] = $this->group_info; + } + } + + $this->options['expose']['label'] = ''; + } + } + + + /** + * Render our chunk of the exposed filter form when selecting + * + * You can override this if it doesn't do what you expect. + */ + public function buildExposedForm(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + // Build the exposed form, when its based on an operator. + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id'])) { + $operator = $this->options['expose']['operator_id']; + $this->operator_form($form, $form_state); + $form[$operator] = $form['operator']; + + if (isset($form[$operator]['#title'])) { + unset($form[$operator]['#title']); + } + + $this->exposed_translate($form[$operator], 'operator'); + + unset($form['operator']); + } + + // Build the form and set the value based on the identifier. + if (!empty($this->options['expose']['identifier'])) { + $value = $this->options['expose']['identifier']; + $this->value_form($form, $form_state); + $form[$value] = $form['value']; + + if (isset($form[$value]['#title']) && !empty($form[$value]['#type']) && $form[$value]['#type'] != 'checkbox') { + unset($form[$value]['#title']); + } + + $this->exposed_translate($form[$value], 'value'); + + if (!empty($form['#type']) && ($form['#type'] == 'checkboxes' || ($form['#type'] == 'select' && !empty($form['#multiple'])))) { + unset($form[$value]['#default_value']); + } + + if (!empty($form['#type']) && $form['#type'] == 'select' && empty($form['#multiple'])) { + $form[$value]['#default_value'] = 'All'; + } + + if ($value != 'value') { + unset($form['value']); + } + } + } + + /** + * Build the form to let users create the group of exposed filters. + * This form is displayed when users click on button 'Build group' + */ + function build_group_form(&$form, &$form_state) { + if (empty($this->options['exposed']) || empty($this->options['is_grouped'])) { + return; + } + $form['#theme'] = 'views_ui_build_group_filter_form'; + + // #flatten will move everything from $form['group_info'][$key] to $form[$key] + // prior to rendering. That's why the pre_render for it needs to run first, + // so that when the next pre_render (the one for fieldsets) runs, it gets + // the flattened data. + array_unshift($form['#pre_render'], 'views_ui_pre_render_flatten_data'); + $form['group_info']['#flatten'] = TRUE; + + if (!empty($this->options['group_info']['identifier'])) { + $identifier = $this->options['group_info']['identifier']; + } + else { + $identifier = 'group_' . $this->options['expose']['identifier']; + } + $form['group_info']['identifier'] = array( + '#type' => 'textfield', + '#default_value' => $identifier, + '#title' => t('Filter identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this filter. Cannot be blank.'), + '#fieldset' => 'more', + ); + $form['group_info']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['group_info']['label'], + '#title' => t('Label'), + '#size' => 40, + ); + $form['group_info']['description'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['group_info']['description'], + '#title' => t('Description'), + '#size' => 60, + ); + $form['group_info']['optional'] = array( + '#type' => 'checkbox', + '#title' => t('Optional'), + '#description' => t('This exposed filter is optional and will have added options to allow it not to be set.'), + '#default_value' => $this->options['group_info']['optional'], + ); + $form['group_info']['multiple'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple selections'), + '#description' => t('Enable to allow users to select multiple items.'), + '#default_value' => $this->options['group_info']['multiple'], + ); + $form['group_info']['widget'] = array( + '#type' => 'radios', + '#default_value' => $this->options['group_info']['widget'], + '#title' => t('Widget type'), + '#options' => array( + 'radios' => t('Radios'), + 'select' => t('Select'), + ), + '#description' => t('Select which kind of widget will be used to render the group of filters'), + ); + $form['group_info']['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember'), + '#description' => t('Remember the last setting the user gave this filter.'), + '#default_value' => $this->options['group_info']['remember'], + ); + + if (!empty($this->options['group_info']['identifier'])) { + $identifier = $this->options['group_info']['identifier']; + } + else { + $identifier = 'group_' . $this->options['expose']['identifier']; + } + $form['group_info']['identifier'] = array( + '#type' => 'textfield', + '#default_value' => $identifier, + '#title' => t('Filter identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this filter. Cannot be blank.'), + '#fieldset' => 'more', + ); + $form['group_info']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['group_info']['label'], + '#title' => t('Label'), + '#size' => 40, + ); + $form['group_info']['optional'] = array( + '#type' => 'checkbox', + '#title' => t('Optional'), + '#description' => t('This exposed filter is optional and will have added options to allow it not to be set.'), + '#default_value' => $this->options['group_info']['optional'], + ); + $form['group_info']['widget'] = array( + '#type' => 'radios', + '#default_value' => $this->options['group_info']['widget'], + '#title' => t('Widget type'), + '#options' => array( + 'radios' => t('Radios'), + 'select' => t('Select'), + ), + '#description' => t('Select which kind of widget will be used to render the group of filters'), + ); + $form['group_info']['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember'), + '#description' => t('Remember the last setting the user gave this filter.'), + '#default_value' => $this->options['group_info']['remember'], + ); + + $groups = array('All' => '- Any -'); // The string '- Any -' will not be rendered see @theme_views_ui_build_group_filter_form + + // Provide 3 options to start when we are in a new group. + if (count($this->options['group_info']['group_items']) == 0) { + $this->options['group_info']['group_items'] = array_fill(1, 3, array()); + } + + // After the general settings, comes a table with all the existent groups. + $default_weight = 0; + foreach ($this->options['group_info']['group_items'] as $item_id => $item) { + if (!empty($form_state['values']['options']['group_info']['group_items'][$item_id]['remove'])) { + continue; + } + // Each rows contains three widgets: + // a) The title, where users define how they identify a pair of operator | value + // b) The operator + // c) The value (or values) to use in the filter with the selected operator + + // In each row, we have to display the operator form and the value from + // $row acts as a fake form to render each widget in a row. + $row = array(); + $groups[$item_id] = ''; + $this->operator_form($row, $form_state); + // Force the operator form to be a select box. Some handlers uses + // radios and they occupy a lot of space in a table row. + $row['operator']['#type'] = 'select'; + $row['operator']['#title'] = ''; + $this->value_form($row, $form_state); + + // Fix the dependencies to update value forms when operators + // changes. This is needed because forms are inside a new form and + // their ids changes. Dependencies are used when operator changes + // from to 'Between', 'Not Between', etc, and two or more widgets + // are displayed. + $without_children = TRUE; + foreach (element_children($row['value']) as $children) { + $has_state = FALSE; + $states = array(); + foreach ($row['value'][$children]['#states']['visible'] as $key => $state) { + if (isset($state[':input[name="options[operator]"]'])) { + $has_state = TRUE; + $states[$key] = $state[':input[name="options[operator]"]']['value']; + } + } + if ($has_state) { + foreach ($states as $key => $state) { + $row['value'][$children]['#states']['visible'][] = array( + ':input[name="options[group_info][group_items][' . $item_id . '][operator]"]' => array('value' => $state), + ); + unset($row['value'][$children]['#states']['visible'][$key]); + } + + $row['value'][$children]['#title'] = ''; + + if (!empty($this->options['group_info']['group_items'][$item_id]['value'][$children])) { + $row['value'][$children]['#default_value'] = $this->options['group_info']['group_items'][$item_id]['value'][$children]; + } + } + $without_children = FALSE; + } + + if ($without_children) { + if (!empty($this->options['group_info']['group_items'][$item_id]['value'])) { + $row['value']['#default_value'] = $this->options['group_info']['group_items'][$item_id]['value']; + } + } + + if (!empty($this->options['group_info']['group_items'][$item_id]['operator'])) { + $row['operator']['#default_value'] = $this->options['group_info']['group_items'][$item_id]['operator']; + } + + $default_title = ''; + if (!empty($this->options['group_info']['group_items'][$item_id]['title'])) { + $default_title = $this->options['group_info']['group_items'][$item_id]['title']; + } + + // Per item group, we have a title that identifies it. + $form['group_info']['group_items'][$item_id] = array( + 'title' => array( + '#type' => 'textfield', + '#size' => 20, + '#default_value' => $default_title, + ), + 'operator' => $row['operator'], + 'value' => $row['value'], + 'remove' => array( + '#type' => 'checkbox', + '#id' => 'views-removed-' . $item_id, + '#attributes' => array('class' => array('views-remove-checkbox')), + '#default_value' => 0, + ), + 'weight' => array( + '#type' => 'weight', + '#delta' => 10, + '#default_value' => $default_weight++, + '#attributes' => array('class' => array('weight')), + ), + ); + } + // From all groups, let chose which is the default. + $form['group_info']['default_group'] = array( + '#type' => 'radios', + '#options' => $groups, + '#default_value' => $this->options['group_info']['default_group'], + '#required' => TRUE, + '#attributes' => array( + 'class' => array('default-radios'), + ) + ); + // From all groups, let chose which is the default. + $form['group_info']['default_group_multiple'] = array( + '#type' => 'checkboxes', + '#options' => $groups, + '#default_value' => $this->options['group_info']['default_group_multiple'], + '#attributes' => array( + 'class' => array('default-checkboxes'), + ) + ); + + $form['group_info']['add_group'] = array( + '#prefix' => '<div class="views-build-group clear-block">', + '#suffix' => '</div>', + '#type' => 'submit', + '#value' => t('Add another item'), + '#submit' => array('views_ui_config_item_form_add_group'), + ); + + $js = array(); + $js['tableDrag']['views-filter-groups']['weight'][0] = array( + 'target' => 'weight', + 'source' => NULL, + 'relationship' => 'sibling', + 'action' => 'order', + 'hidden' => TRUE, + 'limit' => 0, + ); + if (!empty($form_state['js settings']) && is_array($js)) { + $form_state['js settings'] = array_merge($form_state['js settings'], $js); + } + else { + $form_state['js settings'] = $js; + } + } + + + /** + * Make some translations to a form item to make it more suitable to + * exposing. + */ + function exposed_translate(&$form, $type) { + if (!isset($form['#type'])) { + return; + } + + if ($form['#type'] == 'radios') { + $form['#type'] = 'select'; + } + // Checkboxes don't work so well in exposed forms due to GET conversions. + if ($form['#type'] == 'checkboxes') { + if (empty($form['#no_convert']) || empty($this->options['expose']['multiple'])) { + $form['#type'] = 'select'; + } + if (!empty($this->options['expose']['multiple'])) { + $form['#multiple'] = TRUE; + } + } + if (empty($this->options['expose']['multiple']) && isset($form['#multiple'])) { + unset($form['#multiple']); + $form['#size'] = NULL; + } + + // Cleanup in case the translated element's (radios or checkboxes) display value contains html. + if ($form['#type'] == 'select') { + $this->prepare_filter_select_options($form['#options']); + } + + if ($type == 'value' && empty($this->always_required) && empty($this->options['expose']['required']) && $form['#type'] == 'select' && empty($form['#multiple'])) { + $any_label = config('views.settings')->get('ui.exposed_filter_any_label') == 'old_any' ? t('<Any>') : t('- Any -'); + $form['#options'] = array('All' => $any_label) + $form['#options']; + $form['#default_value'] = 'All'; + } + + if (!empty($this->options['expose']['required'])) { + $form['#required'] = TRUE; + } + } + + + + /** + * Sanitizes the HTML select element's options. + * + * The function is recursive to support optgroups. + */ + function prepare_filter_select_options(&$options) { + foreach ($options as $value => $label) { + // Recurse for optgroups. + if (is_array($label)) { + $this->prepare_filter_select_options($options[$value]); + } + // FAPI has some special value to allow hierarchy. + // @see _form_options_flatten + elseif (is_object($label)) { + $this->prepare_filter_select_options($options[$value]->option); + } + else { + $options[$value] = strip_tags(decode_entities($label)); + } + } + } + + /** + * Tell the renderer about our exposed form. This only needs to be + * overridden for particularly complex forms. And maybe not even then. + * + * @return array|null + * For standard exposed filters. An array with the following keys: + * - operator: The $form key of the operator. Set to NULL if no operator. + * - value: The $form key of the value. Set to NULL if no value. + * - label: The label to use for this piece. + * For grouped exposed filters. An array with the following keys: + * - value: The $form key of the value. Set to NULL if no value. + * - label: The label to use for this piece. + */ + public function exposedInfo() { + if (empty($this->options['exposed'])) { + return; + } + + if ($this->isAGroup()) { + return array( + 'value' => $this->options['group_info']['identifier'], + 'label' => $this->options['group_info']['label'], + 'description' => $this->options['group_info']['description'], + ); + } + + return array( + 'operator' => $this->options['expose']['operator_id'], + 'value' => $this->options['expose']['identifier'], + 'label' => $this->options['expose']['label'], + 'description' => $this->options['expose']['description'], + ); + } + + /* + * Transform the input from a grouped filter into a standard filter. + * + * When a filter is a group, find the set of operator and values + * that the choosed item represents, and inform views that a normal + * filter was submitted by telling the operator and the value selected. + * + * The param $selected_group_id is only passed when the filter uses the + * checkboxes widget, and this function will be called for each item + * choosed in the checkboxes. + */ + function convert_exposed_input(&$input, $selected_group_id = NULL) { + if ($this->isAGroup()) { + // If it is already defined the selected group, use it. Only valid + // when the filter uses checkboxes for widget. + if (!empty($selected_group_id)) { + $selected_group = $selected_group_id; + } + else { + $selected_group = $input[$this->options['group_info']['identifier']]; + } + if ($selected_group == 'All' && !empty($this->options['group_info']['optional'])) { + return NULL; + } + if ($selected_group != 'All' && empty($this->options['group_info']['group_items'][$selected_group])) { + return FALSE; + } + if (isset($selected_group) && isset($this->options['group_info']['group_items'][$selected_group])) { + $input[$this->options['expose']['operator']] = $this->options['group_info']['group_items'][$selected_group]['operator']; + + // Value can be optional, For example for 'empty' and 'not empty' filters. + if (!empty($this->options['group_info']['group_items'][$selected_group]['value'])) { + $input[$this->options['expose']['identifier']] = $this->options['group_info']['group_items'][$selected_group]['value']; + } + $this->options['expose']['use_operator'] = TRUE; + + $this->group_info = $input[$this->options['group_info']['identifier']]; + return TRUE; + } + else { + return FALSE; + } + } + } + + /** + * Returns the options available for a grouped filter that users checkboxes + * as widget, and therefore has to be applied several times, one per + * item selected. + */ + function group_multiple_exposed_input(&$input) { + if (!empty($input[$this->options['group_info']['identifier']])) { + return array_filter($input[$this->options['group_info']['identifier']]); + } + return array(); + } + + /** + * Returns TRUE if users can select multiple groups items of a + * grouped exposed filter. + */ + public function multipleExposedInput() { + return $this->isAGroup() && !empty($this->options['group_info']['multiple']); + } + + /** + * If set to remember exposed input in the session, store it there. + * This function is similar to storeExposedInput but modified to + * work properly when the filter is a group. + */ + function store_group_input($input, $status) { + if (!$this->isAGroup() || empty($this->options['group_info']['identifier'])) { + return TRUE; + } + + if (empty($this->options['group_info']['remember'])) { + return; + } + + // Figure out which display id is responsible for the filters, so we + // know where to look for session stored values. + $display_id = ($this->view->display_handler->isDefaulted('filters')) ? 'default' : $this->view->current_display; + + // false means that we got a setting that means to recuse ourselves, + // so we should erase whatever happened to be there. + if ($status === FALSE && isset($_SESSION['views'][$this->view->storage->name][$display_id])) { + $session = &$_SESSION['views'][$this->view->storage->name][$display_id]; + + if (isset($session[$this->options['group_info']['identifier']])) { + unset($session[$this->options['group_info']['identifier']]); + } + } + + if ($status !== FALSE) { + if (!isset($_SESSION['views'][$this->view->storage->name][$display_id])) { + $_SESSION['views'][$this->view->storage->name][$display_id] = array(); + } + + $session = &$_SESSION['views'][$this->view->storage->name][$display_id]; + + $session[$this->options['group_info']['identifier']] = $input[$this->options['group_info']['identifier']]; + } + } + + /** + * Check to see if input from the exposed filters should change + * the behavior of this filter. + */ + public function acceptExposedInput($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) { + $this->operator = $input[$this->options['expose']['operator_id']]; + } + + if (!empty($this->options['expose']['identifier'])) { + $value = $input[$this->options['expose']['identifier']]; + + // Various ways to check for the absence of non-required input. + if (empty($this->options['expose']['required'])) { + if (($this->operator == 'empty' || $this->operator == 'not empty') && $value === '') { + $value = ' '; + } + + if ($this->operator != 'empty' && $this->operator != 'not empty') { + if ($value == 'All' || $value === array()) { + return FALSE; + } + } + + if (!empty($this->always_multiple) && $value === '') { + return FALSE; + } + } + + + if (isset($value)) { + $this->value = $value; + if (empty($this->always_multiple) && empty($this->options['expose']['multiple'])) { + $this->value = array($value); + } + } + else { + return FALSE; + } + } + + return TRUE; + } + + public function storeExposedInput($input, $status) { + if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) { + return TRUE; + } + + if (empty($this->options['expose']['remember'])) { + return; + } + + // Check if we store exposed value for current user. + global $user; + $allowed_rids = empty($this->options['expose']['remember_roles']) ? array() : array_filter($this->options['expose']['remember_roles']); + $intersect_rids = array_intersect_key($allowed_rids, $user->roles); + if (empty($intersect_rids)) { + return; + } + + // Figure out which display id is responsible for the filters, so we + // know where to look for session stored values. + $display_id = ($this->view->display_handler->isDefaulted('filters')) ? 'default' : $this->view->current_display; + + // shortcut test. + $operator = !empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']); + + // false means that we got a setting that means to recuse ourselves, + // so we should erase whatever happened to be there. + if (!$status && isset($_SESSION['views'][$this->view->storage->name][$display_id])) { + $session = &$_SESSION['views'][$this->view->storage->name][$display_id]; + if ($operator && isset($session[$this->options['expose']['operator_id']])) { + unset($session[$this->options['expose']['operator_id']]); + } + + if (isset($session[$this->options['expose']['identifier']])) { + unset($session[$this->options['expose']['identifier']]); + } + } + + if ($status) { + if (!isset($_SESSION['views'][$this->view->storage->name][$display_id])) { + $_SESSION['views'][$this->view->storage->name][$display_id] = array(); + } + + $session = &$_SESSION['views'][$this->view->storage->name][$display_id]; + + if ($operator && isset($input[$this->options['expose']['operator_id']])) { + $session[$this->options['expose']['operator_id']] = $input[$this->options['expose']['operator_id']]; + } + + $session[$this->options['expose']['identifier']] = $input[$this->options['expose']['identifier']]; + } + } + + /** + * Add this filter to the query. + * + * Due to the nature of fapi, the value and the operator have an unintended + * level of indirection. You will find them in $this->operator + * and $this->value respectively. + */ + public function query() { + $this->ensureMyTable(); + $this->query->add_where($this->options['group'], "$this->tableAlias.$this->realField", $this->value, $this->operator); + } + + /** + * Can this filter be used in OR groups? + * + * Some filters have complicated where clauses that cannot be easily used + * with OR groups. Some filters must also use HAVING which also makes + * them not groupable. These filters will end up in a special group + * if OR grouping is in use. + * + * @return bool + */ + function can_group() { + return TRUE; + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/GroupByNumeric.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/GroupByNumeric.php new file mode 100644 index 0000000..c0118b6 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/GroupByNumeric.php @@ -0,0 +1,66 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\GroupByNumeric. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; + +/** + * Simple filter to handle greater than/less than filters + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "groupby_numeric" + * ) + */ +class GroupByNumeric extends Numeric { + + public function query() { + $this->ensureMyTable(); + $field = $this->getField(); + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } + function op_between($field) { + $placeholder_min = $this->placeholder(); + $placeholder_max = $this->placeholder(); + if ($this->operator == 'between') { + $this->query->add_having_expression($this->options['group'], "$field >= $placeholder_min", array($placeholder_min => $this->value['min'])); + $this->query->add_having_expression($this->options['group'], "$field <= $placeholder_max", array($placeholder_max => $this->value['max'])); + } + else { + $this->query->add_having_expression($this->options['group'], "$field <= $placeholder_min OR $field >= $placeholder_max", array($placeholder_min => $this->value['min'], $placeholder_max => $this->value['max'])); + } + } + + function op_simple($field) { + $placeholder = $this->placeholder(); + $this->query->add_having_expression($this->options['group'], "$field $this->operator $placeholder", array($placeholder => $this->value['value'])); + } + + function op_empty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_having_expression($this->options['group'], "$field $operator"); + } + + public function adminLabel($short = FALSE) { + return $this->getField(parent::adminLabel($short)); + } + + function can_group() { return FALSE; } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/InOperator.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/InOperator.php new file mode 100644 index 0000000..f3ae1d7 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/InOperator.php @@ -0,0 +1,446 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\InOperator. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; +use Drupal\views\ViewExecutable; + +/** + * Simple filter to handle matching of multiple options selectable via checkboxes + * + * Definition items: + * - options callback: The function to call in order to generate the value options. If omitted, the options 'Yes' and 'No' will be used. + * - options arguments: An array of arguments to pass to the options callback. + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "in_operator" + * ) + */ +class InOperator extends FilterPluginBase { + + var $value_form_type = 'checkboxes'; + + /** + * @var array + * Stores all operations which are available on the form. + */ + var $value_options = NULL; + + /** + * Overrides Drupal\views\Plugin\views\filter\FilterPluginBase::init(). + */ + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + $this->value_title = t('Options'); + $this->value_options = NULL; + } + + /** + * Child classes should be used to override this function and set the + * 'value options', unless 'options callback' is defined as a valid function + * or static public method to generate these values. + * + * This can use a guard to be used to reduce database hits as much as + * possible. + * + * @return + * Return the stored values in $this->value_options if someone expects it. + */ + function get_value_options() { + if (isset($this->value_options)) { + return; + } + + if (isset($this->definition['options callback']) && is_callable($this->definition['options callback'])) { + if (isset($this->definition['options arguments']) && is_array($this->definition['options arguments'])) { + $this->value_options = call_user_func_array($this->definition['options callback'], $this->definition['options arguments']); + } + else { + $this->value_options = call_user_func($this->definition['options callback']); + } + } + else { + $this->value_options = array(t('Yes'), t('No')); + } + + return $this->value_options; + } + + public function defaultExposeOptions() { + parent::defaultExposeOptions(); + $this->options['expose']['reduce'] = FALSE; + } + + public function buildExposeForm(&$form, &$form_state) { + parent::buildExposeForm($form, $form_state); + $form['expose']['reduce'] = array( + '#type' => 'checkbox', + '#title' => t('Limit list to selected items'), + '#description' => t('If checked, the only items presented to the user will be the ones selected here.'), + '#default_value' => !empty($this->options['expose']['reduce']), // safety + ); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['operator']['default'] = 'in'; + $options['value']['default'] = array(); + $options['expose']['contains']['reduce'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * This kind of construct makes it relatively easy for a child class + * to add or remove functionality by overriding this function and + * adding/removing items from this array. + */ + function operators() { + $operators = array( + 'in' => array( + 'title' => t('Is one of'), + 'short' => t('in'), + 'short_single' => t('='), + 'method' => 'op_simple', + 'values' => 1, + ), + 'not in' => array( + 'title' => t('Is not one of'), + 'short' => t('not in'), + 'short_single' => t('<>'), + 'method' => 'op_simple', + 'values' => 1, + ), + ); + // if the definition allows for the empty operator, add it. + if (!empty($this->definition['allow empty'])) { + $operators += array( + 'empty' => array( + 'title' => t('Is empty (NULL)'), + 'method' => 'op_empty', + 'short' => t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => t('Is not empty (NOT NULL)'), + 'method' => 'op_empty', + 'short' => t('not empty'), + 'values' => 0, + ), + ); + } + + return $operators; + } + + /** + * Build strings from the operators() for 'select' options + */ + function operator_options($which = 'title') { + $options = array(); + foreach ($this->operators() as $id => $info) { + $options[$id] = $info[$which]; + } + + return $options; + } + + function operator_values($values = 1) { + $options = array(); + foreach ($this->operators() as $id => $info) { + if (isset($info['values']) && $info['values'] == $values) { + $options[] = $id; + } + } + + return $options; + } + + function value_form(&$form, &$form_state) { + $form['value'] = array(); + $options = array(); + + if (empty($form_state['exposed'])) { + // Add a select all option to the value form. + $options = array('all' => t('Select all')); + } + + $this->get_value_options(); + $options += $this->value_options; + $default_value = (array) $this->value; + + $which = 'all'; + if (!empty($form['operator'])) { + $source = ':input[name="options[operator]"]'; + } + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { + // exposed and locked. + $which = in_array($this->operator, $this->operator_values(1)) ? 'value' : 'none'; + } + else { + $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]'; + } + + if (!empty($this->options['expose']['reduce'])) { + $options = $this->reduce_value_options(); + + if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) { + $default_value = array(); + } + } + + if (empty($this->options['expose']['multiple'])) { + if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) { + $default_value = 'All'; + } + elseif (empty($default_value)) { + $keys = array_keys($options); + $default_value = array_shift($keys); + } + else { + $copy = $default_value; + $default_value = array_shift($copy); + } + } + } + + if ($which == 'all' || $which == 'value') { + $form['value'] = array( + '#type' => $this->value_form_type, + '#title' => $this->value_title, + '#options' => $options, + '#default_value' => $default_value, + // These are only valid for 'select' type, but do no harm to checkboxes. + '#multiple' => TRUE, + '#size' => count($options) > 8 ? 8 : count($options), + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $default_value; + } + + if ($which == 'all') { + if (empty($form_state['exposed']) && (in_array($this->value_form_type, array('checkbox', 'checkboxes', 'radios', 'select')))) { + $form['value']['#prefix'] = '<div id="edit-options-value-wrapper">'; + $form['value']['#suffix'] = '</div>'; + } + // Setup #states for all operators with one value. + foreach ($this->operator_values(1) as $operator) { + $form['value']['#states']['visible'][] = array( + $source => array('value' => $operator), + ); + } + } + } + } + + /** + * When using exposed filters, we may be required to reduce the set. + */ + function reduce_value_options($input = NULL) { + if (!isset($input)) { + $input = $this->value_options; + } + + // Because options may be an array of strings, or an array of mixed arrays + // and strings (optgroups) or an array of objects, we have to + // step through and handle each one individually. + $options = array(); + foreach ($input as $id => $option) { + if (is_array($option)) { + $options[$id] = $this->reduce_value_options($option); + continue; + } + elseif (is_object($option)) { + $keys = array_keys($option->option); + $key = array_shift($keys); + if (isset($this->options['value'][$key])) { + $options[$id] = $option; + } + } + elseif (isset($this->options['value'][$id])) { + $options[$id] = $option; + } + } + return $options; + } + + public function acceptExposedInput($input) { + // A very special override because the All state for this type of + // filter could have a default: + if (empty($this->options['exposed'])) { + return TRUE; + } + + // If this is non-multiple and non-required, then this filter will + // participate, but using the default settings, *if* 'limit is true. + if (empty($this->options['expose']['multiple']) && empty($this->options['expose']['required']) && !empty($this->options['expose']['limit'])) { + $identifier = $this->options['expose']['identifier']; + if ($input[$identifier] == 'All') { + return TRUE; + } + } + + return parent::acceptExposedInput($input); + } + + function value_submit($form, &$form_state) { + // Drupal's FAPI system automatically puts '0' in for any checkbox that + // was not set, and the key to the checkbox if it is set. + // Unfortunately, this means that if the key to that checkbox is 0, + // we are unable to tell if that checkbox was set or not. + + // Luckily, the '#value' on the checkboxes form actually contains + // *only* a list of checkboxes that were set, and we can use that + // instead. + + $form_state['values']['options']['value'] = $form['value']['#value']; + } + + public function adminSummary() { + if ($this->isAGroup()) { + return t('grouped'); + } + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + $info = $this->operators(); + + $this->get_value_options(); + + if (!is_array($this->value)) { + return; + } + + $operator = check_plain($info[$this->operator]['short']); + $values = ''; + if (in_array($this->operator, $this->operator_values(1))) { + // Remove every element which is not known. + foreach ($this->value as $value) { + if (!isset($this->value_options[$value])) { + unset($this->value[$value]); + } + } + // Choose different kind of ouput for 0, a single and multiple values. + if (count($this->value) == 0) { + $values = t('Unknown'); + } + else if (count($this->value) == 1) { + // If any, use the 'single' short name of the operator instead. + if (isset($info[$this->operator]['short_single'])) { + $operator = check_plain($info[$this->operator]['short_single']); + } + + $keys = $this->value; + $value = array_shift($keys); + if (isset($this->value_options[$value])) { + $values = check_plain($this->value_options[$value]); + } + else { + $values = ''; + } + } + else { + foreach ($this->value as $value) { + if ($values !== '') { + $values .= ', '; + } + if (drupal_strlen($values) > 8) { + $values .= '...'; + break; + } + if (isset($this->value_options[$value])) { + $values .= check_plain($this->value_options[$value]); + } + } + } + } + + return $operator . (($values !== '') ? ' ' . $values : ''); + } + + public function query() { + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}(); + } + } + + function op_simple() { + if (empty($this->value)) { + return; + } + $this->ensureMyTable(); + + // We use array_values() because the checkboxes keep keys and that can cause + // array addition problems. + $this->query->add_where($this->options['group'], "$this->tableAlias.$this->realField", array_values($this->value), $this->operator); + } + + function op_empty() { + $this->ensureMyTable(); + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_where($this->options['group'], "$this->tableAlias.$this->realField", NULL, $operator); + } + + public function validate() { + $this->get_value_options(); + $errors = array(); + + // If the operator is an operator which doesn't require a value, there is + // no need for additional validation. + if (in_array($this->operator, $this->operator_values(0))) { + return array(); + } + + if (!in_array($this->operator, $this->operator_values(1))) { + $errors[] = t('The operator is invalid on filter: @filter.', array('@filter' => $this->adminLabel(TRUE))); + } + if (is_array($this->value)) { + if (!isset($this->value_options)) { + // Don't validate if there are none value options provided, for example for special handlers. + return $errors; + } + if ($this->options['exposed'] && !$this->options['expose']['required'] && empty($this->value)) { + // Don't validate if the field is exposed and no default value is provided. + return $errors; + } + + // Some filter_in_operator usage uses optgroups forms, so flatten it. + $flat_options = form_options_flatten($this->value_options, TRUE); + + // Remove every element which is not known. + foreach ($this->value as $value) { + if (!isset($flat_options[$value])) { + unset($this->value[$value]); + } + } + // Choose different kind of ouput for 0, a single and multiple values. + if (count($this->value) == 0) { + $errors[] = t('No valid values found on filter: @filter.', array('@filter' => $this->adminLabel(TRUE))); + } + } + elseif (!empty($this->value) && ($this->operator == 'in' || $this->operator == 'not in')) { + $errors[] = t('The value @value is not an array for @operator on filter: @filter', array('@value' => var_export($this->value), '@operator' => $this->operator, '@filter' => $this->adminLabel(TRUE))); + } + return $errors; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/ManyToOne.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/ManyToOne.php new file mode 100644 index 0000000..d3ff72c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/ManyToOne.php @@ -0,0 +1,137 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\ManyToOne. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\views\ViewExecutable; +use Drupal\views\ManyToOneHelper; +use Drupal\Core\Annotation\Plugin; + +/** + * Complex filter to handle filtering for many to one relationships, + * such as terms (many terms per node) or roles (many roles per user). + * + * The construct method needs to be overridden to provide a list of options; + * alternately, the value_form and adminSummary methods need to be overriden + * to provide something that isn't just a select list. + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "many_to_one" + * ) + */ +class ManyToOne extends InOperator { + + /** + * @var Drupal\views\ManyToOneHelper + * + * Stores the Helper object which handles the many_to_one complexity. + */ + var $helper = NULL; + + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + $this->helper = new ManyToOneHelper($this); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['operator']['default'] = 'or'; + $options['value']['default'] = array(); + + if (isset($this->helper)) { + $this->helper->defineOptions($options); + } + else { + $helper = new ManyToOneHelper($this); + $helper->defineOptions($options); + } + + return $options; + } + + function operators() { + $operators = array( + 'or' => array( + 'title' => t('Is one of'), + 'short' => t('or'), + 'short_single' => t('='), + 'method' => 'op_helper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + 'and' => array( + 'title' => t('Is all of'), + 'short' => t('and'), + 'short_single' => t('='), + 'method' => 'op_helper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + 'not' => array( + 'title' => t('Is none of'), + 'short' => t('not'), + 'short_single' => t('<>'), + 'method' => 'op_helper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + ); + // if the definition allows for the empty operator, add it. + if (!empty($this->definition['allow empty'])) { + $operators += array( + 'empty' => array( + 'title' => t('Is empty (NULL)'), + 'method' => 'op_empty', + 'short' => t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => t('Is not empty (NOT NULL)'), + 'method' => 'op_empty', + 'short' => t('not empty'), + 'values' => 0, + ), + ); + } + + return $operators; + } + + var $value_form_type = 'select'; + function value_form(&$form, &$form_state) { + parent::value_form($form, $form_state); + + if (empty($form_state['exposed'])) { + $this->helper->buildOptionsForm($form, $form_state); + } + } + + /** + * Override ensureMyTable so we can control how this joins in. + * The operator actually has influence over joining. + */ + public function ensureMyTable() { + // Defer to helper if the operator specifies it. + $info = $this->operators(); + if (isset($info[$this->operator]['ensure_my_table']) && $info[$this->operator]['ensure_my_table'] == 'helper') { + return $this->helper->ensureMyTable(); + } + + return parent::ensureMyTable(); + } + + function op_helper() { + if (empty($this->value)) { + return; + } + $this->helper->add_filter(); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/Numeric.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Numeric.php new file mode 100644 index 0000000..3e5c9db --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Numeric.php @@ -0,0 +1,346 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\Numeric. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Database\Database; +use Drupal\Core\Annotation\Plugin; + +/** + * Simple filter to handle greater than/less than filters + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "numeric" + * ) + */ +class Numeric extends FilterPluginBase { + + var $always_multiple = TRUE; + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['value'] = array( + 'contains' => array( + 'min' => array('default' => ''), + 'max' => array('default' => ''), + 'value' => array('default' => ''), + ), + ); + + return $options; + } + + function operators() { + $operators = array( + '<' => array( + 'title' => t('Is less than'), + 'method' => 'op_simple', + 'short' => t('<'), + 'values' => 1, + ), + '<=' => array( + 'title' => t('Is less than or equal to'), + 'method' => 'op_simple', + 'short' => t('<='), + 'values' => 1, + ), + '=' => array( + 'title' => t('Is equal to'), + 'method' => 'op_simple', + 'short' => t('='), + 'values' => 1, + ), + '!=' => array( + 'title' => t('Is not equal to'), + 'method' => 'op_simple', + 'short' => t('!='), + 'values' => 1, + ), + '>=' => array( + 'title' => t('Is greater than or equal to'), + 'method' => 'op_simple', + 'short' => t('>='), + 'values' => 1, + ), + '>' => array( + 'title' => t('Is greater than'), + 'method' => 'op_simple', + 'short' => t('>'), + 'values' => 1, + ), + 'between' => array( + 'title' => t('Is between'), + 'method' => 'op_between', + 'short' => t('between'), + 'values' => 2, + ), + 'not between' => array( + 'title' => t('Is not between'), + 'method' => 'op_between', + 'short' => t('not between'), + 'values' => 2, + ), + ); + + // if the definition allows for the empty operator, add it. + if (!empty($this->definition['allow empty'])) { + $operators += array( + 'empty' => array( + 'title' => t('Is empty (NULL)'), + 'method' => 'op_empty', + 'short' => t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => t('Is not empty (NOT NULL)'), + 'method' => 'op_empty', + 'short' => t('not empty'), + 'values' => 0, + ), + ); + } + + // Add regexp support for MySQL. + if (Database::getConnection()->databaseType() == 'mysql') { + $operators += array( + 'regular_expression' => array( + 'title' => t('Regular expression'), + 'short' => t('regex'), + 'method' => 'op_regex', + 'values' => 1, + ), + ); + } + + return $operators; + } + + /** + * Provide a list of all the numeric operators + */ + function operator_options($which = 'title') { + $options = array(); + foreach ($this->operators() as $id => $info) { + $options[$id] = $info[$which]; + } + + return $options; + } + + function operator_values($values = 1) { + $options = array(); + foreach ($this->operators() as $id => $info) { + if ($info['values'] == $values) { + $options[] = $id; + } + } + + return $options; + } + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + $form['value']['#tree'] = TRUE; + + // We have to make some choices when creating this as an exposed + // filter form. For example, if the operator is locked and thus + // not rendered, we can't render dependencies; instead we only + // render the form items we need. + $which = 'all'; + if (!empty($form['operator'])) { + $source = ':input[name="options[operator]"]'; + } + + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { + // exposed and locked. + $which = in_array($this->operator, $this->operator_values(2)) ? 'minmax' : 'value'; + } + else { + $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]'; + } + } + + if ($which == 'all') { + $form['value']['value'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Value') : '', + '#size' => 30, + '#default_value' => $this->value['value'], + ); + // Setup #states for all operators with one value. + foreach ($this->operator_values(1) as $operator) { + $form['value']['value']['#states']['visible'][] = array( + $source => array('value' => $operator), + ); + } + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['value'])) { + $form_state['input'][$identifier]['value'] = $this->value['value']; + } + } + elseif ($which == 'value') { + // When exposed we drop the value-value and just do value if + // the operator is locked. + $form['value'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Value') : '', + '#size' => 30, + '#default_value' => $this->value['value'], + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $this->value['value']; + } + } + + if ($which == 'all' || $which == 'minmax') { + $form['value']['min'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Min') : '', + '#size' => 30, + '#default_value' => $this->value['min'], + ); + $form['value']['max'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('And max') : t('And'), + '#size' => 30, + '#default_value' => $this->value['max'], + ); + if ($which == 'all') { + $states = array(); + // Setup #states for all operators with two values. + foreach ($this->operator_values(2) as $operator) { + $states['#states']['visible'][] = array( + $source => array('value' => $operator), + ); + } + $form['value']['min'] += $states; + $form['value']['max'] += $states; + } + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['min'])) { + $form_state['input'][$identifier]['min'] = $this->value['min']; + } + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['max'])) { + $form_state['input'][$identifier]['max'] = $this->value['max']; + } + + if (!isset($form['value'])) { + // Ensure there is something in the 'value'. + $form['value'] = array( + '#type' => 'value', + '#value' => NULL + ); + } + } + } + + public function query() { + $this->ensureMyTable(); + $field = "$this->tableAlias.$this->realField"; + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } + + function op_between($field) { + if ($this->operator == 'between') { + $this->query->add_where($this->options['group'], $field, array($this->value['min'], $this->value['max']), 'BETWEEN'); + } + else { + $this->query->add_where($this->options['group'], db_or()->condition($field, $this->value['min'], '<=')->condition($field, $this->value['max'], '>=')); + } + } + + function op_simple($field) { + $this->query->add_where($this->options['group'], $field, $this->value['value'], $this->operator); + } + + function op_empty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_where($this->options['group'], $field, NULL, $operator); + } + + function op_regex($field) { + $this->query->add_where($this->options['group'], $field, $this->value, 'RLIKE'); + } + + public function adminSummary() { + if ($this->isAGroup()) { + return t('grouped'); + } + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + + $options = $this->operator_options('short'); + $output = check_plain($options[$this->operator]); + if (in_array($this->operator, $this->operator_values(2))) { + $output .= ' ' . t('@min and @max', array('@min' => $this->value['min'], '@max' => $this->value['max'])); + } + elseif (in_array($this->operator, $this->operator_values(1))) { + $output .= ' ' . check_plain($this->value['value']); + } + return $output; + } + + /** + * Do some minor translation of the exposed input + */ + public function acceptExposedInput($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + // rewrite the input value so that it's in the correct format so that + // the parent gets the right data. + if (!empty($this->options['expose']['identifier'])) { + $value = &$input[$this->options['expose']['identifier']]; + if (!is_array($value)) { + $value = array( + 'value' => $value, + ); + } + } + + $rc = parent::acceptExposedInput($input); + + if (empty($this->options['expose']['required'])) { + // We have to do some of our own checking for non-required filters. + $info = $this->operators(); + if (!empty($info[$this->operator]['values'])) { + switch ($info[$this->operator]['values']) { + case 1: + if ($value['value'] === '') { + return FALSE; + } + break; + case 2: + if ($value['min'] === '' && $value['max'] === '') { + return FALSE; + } + break; + } + } + } + + return $rc; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/Standard.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Standard.php new file mode 100644 index 0000000..e86dc12 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/Standard.php @@ -0,0 +1,23 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\Standard. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; + +/** + * Default implementation of the base filter plugin. + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "standard" + * ) + */ +class Standard extends FilterPluginBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/filter/String.php b/core/modules/views/lib/Drupal/views/Plugin/views/filter/String.php new file mode 100644 index 0000000..ed59742 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/filter/String.php @@ -0,0 +1,351 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\filter\String. + */ + +namespace Drupal\views\Plugin\views\filter; + +use Drupal\Core\Database\Database; +use Drupal\Core\Annotation\Plugin; + +/** + * Basic textfield filter to handle string filtering commands + * including equality, like, not like, etc. + * + * @ingroup views_filter_handlers + * + * @Plugin( + * id = "string" + * ) + */ +class String extends FilterPluginBase { + + // exposed filter options + var $always_multiple = TRUE; + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['expose']['contains']['required'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * This kind of construct makes it relatively easy for a child class + * to add or remove functionality by overriding this function and + * adding/removing items from this array. + */ + function operators() { + $operators = array( + '=' => array( + 'title' => t('Is equal to'), + 'short' => t('='), + 'method' => 'op_equal', + 'values' => 1, + ), + '!=' => array( + 'title' => t('Is not equal to'), + 'short' => t('!='), + 'method' => 'op_equal', + 'values' => 1, + ), + 'contains' => array( + 'title' => t('Contains'), + 'short' => t('contains'), + 'method' => 'op_contains', + 'values' => 1, + ), + 'word' => array( + 'title' => t('Contains any word'), + 'short' => t('has word'), + 'method' => 'op_word', + 'values' => 1, + ), + 'allwords' => array( + 'title' => t('Contains all words'), + 'short' => t('has all'), + 'method' => 'op_word', + 'values' => 1, + ), + 'starts' => array( + 'title' => t('Starts with'), + 'short' => t('begins'), + 'method' => 'op_starts', + 'values' => 1, + ), + 'not_starts' => array( + 'title' => t('Does not start with'), + 'short' => t('not_begins'), + 'method' => 'op_not_starts', + 'values' => 1, + ), + 'ends' => array( + 'title' => t('Ends with'), + 'short' => t('ends'), + 'method' => 'op_ends', + 'values' => 1, + ), + 'not_ends' => array( + 'title' => t('Does not end with'), + 'short' => t('not_ends'), + 'method' => 'op_not_ends', + 'values' => 1, + ), + 'not' => array( + 'title' => t('Does not contain'), + 'short' => t('!has'), + 'method' => 'op_not', + 'values' => 1, + ), + 'shorterthan' => array( + 'title' => t('Length is shorter than'), + 'short' => t('shorter than'), + 'method' => 'op_shorter', + 'values' => 1, + ), + 'longerthan' => array( + 'title' => t('Length is longer than'), + 'short' => t('longer than'), + 'method' => 'op_longer', + 'values' => 1, + ), + ); + // if the definition allows for the empty operator, add it. + if (!empty($this->definition['allow empty'])) { + $operators += array( + 'empty' => array( + 'title' => t('Is empty (NULL)'), + 'method' => 'op_empty', + 'short' => t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => t('Is not empty (NOT NULL)'), + 'method' => 'op_empty', + 'short' => t('not empty'), + 'values' => 0, + ), + ); + } + // Add regexp support for MySQL. + if (Database::getConnection()->databaseType() == 'mysql') { + $operators += array( + 'regular_expression' => array( + 'title' => t('Regular expression'), + 'short' => t('regex'), + 'method' => 'op_regex', + 'values' => 1, + ), + ); + } + + return $operators; + } + + /** + * Build strings from the operators() for 'select' options + */ + function operator_options($which = 'title') { + $options = array(); + foreach ($this->operators() as $id => $info) { + $options[$id] = $info[$which]; + } + + return $options; + } + + public function adminSummary() { + if ($this->isAGroup()) { + return t('grouped'); + } + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + + $options = $this->operator_options('short'); + $output = ''; + if (!empty($options[$this->operator])) { + $output = check_plain($options[$this->operator]); + } + if (in_array($this->operator, $this->operator_values(1))) { + $output .= ' ' . check_plain($this->value); + } + return $output; + } + + function operator_values($values = 1) { + $options = array(); + foreach ($this->operators() as $id => $info) { + if (isset($info['values']) && $info['values'] == $values) { + $options[] = $id; + } + } + + return $options; + } + + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + // We have to make some choices when creating this as an exposed + // filter form. For example, if the operator is locked and thus + // not rendered, we can't render dependencies; instead we only + // render the form items we need. + $which = 'all'; + if (!empty($form['operator'])) { + $source = ':input[name="options[operator]"]'; + } + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { + // exposed and locked. + $which = in_array($this->operator, $this->operator_values(1)) ? 'value' : 'none'; + } + else { + $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]'; + } + } + + if ($which == 'all' || $which == 'value') { + $form['value'] = array( + '#type' => 'textfield', + '#title' => t('Value'), + '#size' => 30, + '#default_value' => $this->value, + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $this->value; + } + + if ($which == 'all') { + // Setup #states for all operators with one value. + foreach ($this->operator_values(1) as $operator) { + $form['value']['#states']['visible'][] = array( + $source => array('value' => $operator), + ); + } + } + } + + if (!isset($form['value'])) { + // Ensure there is something in the 'value'. + $form['value'] = array( + '#type' => 'value', + '#value' => NULL + ); + } + } + + function operator() { + return $this->operator == '=' ? 'LIKE' : 'NOT LIKE'; + } + + /** + * Add this filter to the query. + * + * Due to the nature of fapi, the value and the operator have an unintended + * level of indirection. You will find them in $this->operator + * and $this->value respectively. + */ + public function query() { + $this->ensureMyTable(); + $field = "$this->tableAlias.$this->realField"; + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } + + function op_equal($field) { + $this->query->add_where($this->options['group'], $field, $this->value, $this->operator()); + } + + function op_contains($field) { + $this->query->add_where($this->options['group'], $field, '%' . db_like($this->value) . '%', 'LIKE'); + } + + function op_word($field) { + $where = $this->operator == 'word' ? db_or() : db_and(); + + // Don't filter on empty strings. + if (empty($this->value)) { + return; + } + + preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' ' . $this->value, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $phrase = FALSE; + // Strip off phrase quotes + if ($match[2]{0} == '"') { + $match[2] = substr($match[2], 1, -1); + $phrase = TRUE; + } + $words = trim($match[2], ',?!();:-'); + $words = $phrase ? array($words) : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY); + foreach ($words as $word) { + $placeholder = $this->placeholder(); + $where->condition($field, '%' . db_like(trim($word, " ,!?")) . '%', 'LIKE'); + } + } + + if (!$where) { + return; + } + + // previously this was a call_user_func_array but that's unnecessary + // as views will unpack an array that is a single arg. + $this->query->add_where($this->options['group'], $where); + } + + function op_starts($field) { + $this->query->add_where($this->options['group'], $field, db_like($this->value) . '%', 'LIKE'); + } + + function op_not_starts($field) { + $this->query->add_where($this->options['group'], $field, db_like($this->value) . '%', 'NOT LIKE'); + } + + function op_ends($field) { + $this->query->add_where($this->options['group'], $field, '%' . db_like($this->value), 'LIKE'); + } + + function op_not_ends($field) { + $this->query->add_where($this->options['group'], $field, '%' . db_like($this->value), 'NOT LIKE'); + } + + function op_not($field) { + $this->query->add_where($this->options['group'], $field, '%' . db_like($this->value) . '%', 'NOT LIKE'); + } + + function op_shorter($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "LENGTH($field) < $placeholder", array($placeholder => $this->value)); + } + + function op_longer($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "LENGTH($field) > $placeholder", array($placeholder => $this->value)); + } + + function op_regex($field) { + $this->query->add_where($this->options['group'], $field, $this->value, 'RLIKE'); + } + + function op_empty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_where($this->options['group'], $field, NULL, $operator); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/join/JoinPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/join/JoinPluginBase.php new file mode 100644 index 0000000..f4efc69 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/join/JoinPluginBase.php @@ -0,0 +1,284 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\join\JoinPluginBase. + */ + +namespace Drupal\views\Plugin\views\join; +use Drupal\Component\Plugin\PluginBase; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * @defgroup views_join_handlers Views join handlers + * @{ + * Handlers to tell Views how to join tables together. + * + * Here is an example how to join from table one to example two so it produces + * the following sql: + * @code + * INNER JOIN {two} ON one.field_a = two.field_b + * @code. + * The required php code for this kind of functionality is the following: + * @code + * $configuration = array( + * 'table' => 'two', + * 'field' => 'field_b', + * 'left_table' => 'one', + * 'left_field' => 'field_a', + * 'operator' => '=' + * ); + * $join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $configuration); + * + * Here is how you do complex joins: + * + * @code + * class JoinComplex extends JoinPluginBase { + * public function buildJoin($select_query, $table, $view_query) { + * // Add an additional hardcoded condition to the query. + * $this->extra = 'foo.bar = baz.boing'; + * parent::buildJoin($select_query, $table, $view_query); + * } + * } + * @endcode + */ + +/** + * Represents a join and creates the SQL necessary to implement the join. + * + * @todo It might make sense to create an interface for joins. + * + * Extensions of this class can be used to create more interesting joins. + */ +class JoinPluginBase extends PluginBase { + + /** + * The table to join (right table). + * + * @var string + */ + public $table; + + /** + * The field to join on (right field). + * + * @var string + */ + public $field; + + /** + * The table we join to. + * + * @var string + */ + public $leftTable; + + /** + * The field we join to. + * + * @var string + */ + public $leftField; + + /** + * An array of extra conditions on the join. + * + * Each condition is either a string that's directly added, or an array of + * items: + * - table(optional): If not set, current table; if NULL, no table. If you + * specify a table in cached configuration, Views will try to load from an + * existing alias. If you use realtime joins, it works better. + * - field(optional): Field or formula. In formulas we can reference the + * right table by using %alias. + * - operator(optional): The operator used, Defaults to "=". + * - value: Must be set. If an array, operator will be defaulted to IN. + * - numeric: If true, the value will not be surrounded in quotes. + * + * @see SelectQueryInterface::addJoin() + * + * @var array + */ + public $extra; + + /** + * The join type, so for example LEFT (default) or INNER. + * + * @var string + */ + public $type; + + /** + * The configuration array passed by initJoin. + * + * @var array + * + * @see Drupal\views\Plugin\views\join\JoinPluginBase::initJoin() + */ + public $configuration = array(); + + /** + * How all the extras will be combined. Either AND or OR. + * + * @var string + */ + public $extraOperator; + + /** + * Defines whether a join has been adjusted. + * + * Views updates the join object to set the table alias instead of the table + * name. Once views has changed the alias it sets the adjusted value so it + * does not have to be updated anymore. If you create your own join object + * you should set the adjusted in the definition array to TRUE if you already + * know the table alias. + * + * @var bool + * + * @see Drupal\views\Plugin\HandlerBase::getTableJoin() + * @see Drupal\views\Plugin\views\query\Sql::adjust_join() + * @see Drupal\views\Plugin\views\relationship\RelationshipPluginBase::query() + */ + public $adjusted; + + /** + * Constructs a Drupal\views\Plugin\views\join\JoinPluginBase object. + */ + public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) { + parent::__construct($configuration, $plugin_id, $discovery); + // Merge in some default values. + $configuration += array( + 'type' => 'LEFT', + 'extra_operator' => 'AND' + ); + $this->configuration = $configuration; + + if (!empty($configuration['table'])) { + $this->table = $configuration['table']; + } + + $this->leftTable = $configuration['left_table']; + $this->leftField = $configuration['left_field']; + $this->field = $configuration['field']; + + if (!empty($configuration['extra'])) { + $this->extra = $configuration['extra']; + } + + if (isset($configuration['adjusted'])) { + $this->extra = $configuration['adjusted']; + } + + $this->extraOperator = strtoupper($configuration['extra_operator']); + $this->type = $configuration['type']; + } + + /** + * Build the SQL for the join this object represents. + * + * When possible, try to use table alias instead of table names. + * + * @param $select_query + * An implementation of SelectQueryInterface. + * @param $table + * The base table to join. + * @param $view_query + * The source query, implementation of views_plugin_query. + */ + public function buildJoin($select_query, $table, $view_query) { + if (empty($this->configuration['table formula'])) { + $right_table = $this->table; + } + else { + $right_table = $this->configuration['table formula']; + } + + if ($this->leftTable) { + $left = $view_query->get_table_info($this->leftTable); + $left_field = "$left[alias].$this->leftField"; + } + else { + // This can be used if left_field is a formula or something. It should be used only *very* rarely. + $left_field = $this->leftField; + } + + $condition = "$left_field = $table[alias].$this->field"; + $arguments = array(); + + // Tack on the extra. + if (isset($this->extra)) { + if (is_array($this->extra)) { + $extras = array(); + foreach ($this->extra as $info) { + $extra = ''; + // Figure out the table name. Remember, only use aliases provided + // if at all possible. + $join_table = ''; + if (!array_key_exists('table', $info)) { + $join_table = $table['alias'] . '.'; + } + elseif (isset($info['table'])) { + // If we're aware of a table alias for this table, use the table + // alias instead of the table name. + if (isset($left) && $left['table'] == $info['table']) { + $join_table = $left['alias'] . '.'; + } + else { + $join_table = $info['table'] . '.'; + } + } + + // Convert a single-valued array of values to the single-value case, + // and transform from IN() notation to = notation + if (is_array($info['value']) && count($info['value']) == 1) { + if (empty($info['operator'])) { + $operator = '='; + } + else { + $operator = $info['operator'] == 'NOT IN' ? '!=' : '='; + } + $info['value'] = array_shift($info['value']); + } + + if (is_array($info['value'])) { + // With an array of values, we need multiple placeholders and the + // 'IN' operator is implicit. + foreach ($info['value'] as $value) { + $placeholder_i = ':views_join_condition_' . $select_query->nextPlaceholder(); + $arguments[$placeholder_i] = $value; + } + + $operator = !empty($info['operator']) ? $info['operator'] : 'IN'; + $placeholder = '( ' . implode(', ', array_keys($arguments)) . ' )'; + } + else { + // With a single value, the '=' operator is implicit. + $operator = !empty($info['operator']) ? $info['operator'] : '='; + $placeholder = ':views_join_condition_' . $select_query->nextPlaceholder(); + $arguments[$placeholder] = $info['value']; + } + + $extras[] = "$join_table$info[field] $operator $placeholder"; + } + + if ($extras) { + if (count($extras) == 1) { + $condition .= ' AND ' . array_shift($extras); + } + else { + $condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')'; + } + } + } + elseif ($this->extra && is_string($this->extra)) { + $condition .= " AND ($this->extra)"; + } + } + + $select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments); + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/join/Standard.php b/core/modules/views/lib/Drupal/views/Plugin/views/join/Standard.php new file mode 100644 index 0000000..ba54cc4 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/join/Standard.php @@ -0,0 +1,21 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\join\Standard. + */ + +namespace Drupal\views\Plugin\views\join; + +use Drupal\Core\Annotation\Plugin; + +/** + * Default implementation of the join plugin. + * + * @Plugin( + * id = "standard" + * ) + */ +class Standard extends JoinPluginBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/join/Subquery.php b/core/modules/views/lib/Drupal/views/Plugin/views/join/Subquery.php new file mode 100644 index 0000000..913c866 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/join/Subquery.php @@ -0,0 +1,112 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\join\Subquery. + */ + +namespace Drupal\views\Plugin\views\join; +use Drupal\Core\Annotation\Plugin; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * Join handler for relationships that join with a subquery as the left field. + * eg: + * LEFT JOIN node node_term_data ON ([YOUR SUBQUERY HERE]) = node_term_data.nid + * + * join definition + * same as Join class above, except: + * - left_query: The subquery to use in the left side of the join clause. + * + * @Plugin( + * id = "subquery" + * ) + */ +class Subquery extends JoinPluginBase { + + /** + * Constructs a Subquery object. + */ + public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) { + parent::__construct($configuration, $plugin_id, $discovery); + + $this->left_query = $this->configuration['left_query']; + } + + /** + * Build the SQL for the join this object represents. + * + * @param $select_query + * An implementation of SelectQueryInterface. + * @param $table + * The base table to join. + * @param $view_query + * The source query, implementation of views_plugin_query. + * @return + * + */ + public function buildJoin($select_query, $table, $view_query) { + if (empty($this->configuration['table formula'])) { + $right_table = "{" . $this->table . "}"; + } + else { + $right_table = $this->configuration['table formula']; + } + + // Add our join condition, using a subquery on the left instead of a field. + $condition = "($this->left_query) = $table[alias].$this->field"; + $arguments = array(); + + // Tack on the extra. + // This is just copied verbatim from the parent class, which itself has a bug: http://drupal.org/node/1118100 + if (isset($this->extra)) { + if (is_array($this->extra)) { + $extras = array(); + foreach ($this->extra as $info) { + $extra = ''; + // Figure out the table name. Remember, only use aliases provided + // if at all possible. + $join_table = ''; + if (!array_key_exists('table', $info)) { + $join_table = $table['alias'] . '.'; + } + elseif (isset($info['table'])) { + $join_table = $info['table'] . '.'; + } + + $placeholder = ':views_join_condition_' . $select_query->nextPlaceholder(); + + if (is_array($info['value'])) { + $operator = !empty($info['operator']) ? $info['operator'] : 'IN'; + // Transform from IN() notation to = notation if just one value. + if (count($info['value']) == 1) { + $info['value'] = array_shift($info['value']); + $operator = $operator == 'NOT IN' ? '!=' : '='; + } + } + else { + $operator = !empty($info['operator']) ? $info['operator'] : '='; + } + + $extras[] = "$join_table$info[field] $operator $placeholder"; + $arguments[$placeholder] = $info['value']; + } + + if ($extras) { + if (count($extras) == 1) { + $condition .= ' AND ' . array_shift($extras); + } + else { + $condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')'; + } + } + } + elseif ($this->extra && is_string($this->extra)) { + $condition .= " AND ($this->extra)"; + } + } + + $select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/pager/Full.php b/core/modules/views/lib/Drupal/views/Plugin/views/pager/Full.php new file mode 100644 index 0000000..f99eabe --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/pager/Full.php @@ -0,0 +1,443 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\pager\Full. + */ + +namespace Drupal\views\Plugin\views\pager; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The plugin to handle full pager. + * + * @ingroup views_pager_plugins + * + * @Plugin( + * id = "full", + * title = @Translation("Paged output, full pager"), + * short_title = @Translation("Full"), + * help = @Translation("Paged output, full Drupal style") + * ) + */ +class Full extends PagerPluginBase { + + public function summaryTitle() { + if (!empty($this->options['offset'])) { + return format_plural($this->options['items_per_page'], '@count item, skip @skip', 'Paged, @count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], '@count item', 'Paged, @count items', array('@count' => $this->options['items_per_page'])); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['items_per_page'] = array('default' => 10); + $options['offset'] = array('default' => 0); + $options['id'] = array('default' => 0); + $options['total_pages'] = array('default' => ''); + // Use the same default quantity that core uses by default. + $options['quantity'] = array('default' => 9); + $options['expose'] = array( + 'contains' => array( + 'items_per_page' => array('default' => FALSE, 'bool' => TRUE), + 'items_per_page_label' => array('default' => 'Items per page', 'translatable' => TRUE), + 'items_per_page_options' => array('default' => '5, 10, 20, 40, 60'), + 'items_per_page_options_all' => array('default' => FALSE, 'bool' => TRUE), + 'items_per_page_options_all_label' => array('default' => '- All -', 'translatable' => TRUE), + + 'offset' => array('default' => FALSE, 'bool' => TRUE), + 'offset_label' => array('default' => 'Offset', 'translatable' => TRUE), + ), + ); + $options['tags'] = array( + 'contains' => array( + 'first' => array('default' => '« first', 'translatable' => TRUE), + 'previous' => array('default' => '‹ previous', 'translatable' => TRUE), + 'next' => array('default' => 'next ›', 'translatable' => TRUE), + 'last' => array('default' => 'last »', 'translatable' => TRUE), + ), + ); + return $options; + } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $pager_text = $this->displayHandler->getPagerText(); + $form['items_per_page'] = array( + '#title' => $pager_text['items per page title'], + '#type' => 'number', + '#description' => $pager_text['items per page description'], + '#default_value' => $this->options['items_per_page'], + ); + + $form['offset'] = array( + '#type' => 'number', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + + $form['id'] = array( + '#type' => 'number', + '#title' => t('Pager ID'), + '#description' => t("Unless you're experiencing problems with pagers related to this view, you should leave this at 0. If using multiple pagers on one page you may need to set this number to a higher value so as not to conflict within the ?page= array. Large values will add a lot of commas to your URLs, so avoid if possible."), + '#default_value' => $this->options['id'], + ); + + $form['total_pages'] = array( + '#type' => 'number', + '#title' => t('Number of pages'), + '#description' => t('The total number of pages. Leave empty to show all pages.'), + '#default_value' => $this->options['total_pages'], + ); + + $form['quantity'] = array( + '#type' => 'number', + '#title' => t('Number of pager links visible'), + '#description' => t('Specify the number of links to pages to display in the pager.'), + '#default_value' => $this->options['quantity'], + ); + + $form['tags'] = array( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#collapsed' => FALSE, + '#tree' => TRUE, + '#title' => t('Tags'), + '#input' => TRUE, + '#description' => t('A lists of labels for the controls in the pager'), + ); + + $form['tags']['first'] = array( + '#type' => 'textfield', + '#title' => t('Text for "first"-link'), + '#description' => t('Text for "first"-link'), + '#default_value' => $this->options['tags']['first'], + ); + + $form['tags']['previous'] = array( + '#type' => 'textfield', + '#title' => t('Text for "previous"-link'), + '#description' => t('Text for "previous"-link'), + '#default_value' => $this->options['tags']['previous'], + ); + + $form['tags']['next'] = array( + '#type' => 'textfield', + '#title' => t('Text for "next"-link'), + '#description' => t('Text for "next"-link'), + '#default_value' => $this->options['tags']['next'], + ); + + $form['tags']['last'] = array( + '#type' => 'textfield', + '#title' => t('Text for "last"-link'), + '#description' => t('Text for "last"-link'), + '#default_value' => $this->options['tags']['last'], + ); + + $form['expose'] = array( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#collapsed' => FALSE, + '#tree' => TRUE, + '#title' => t('Exposed options'), + '#input' => TRUE, + '#description' => t('Exposing this options allows users to define their values in a exposed form when view is displayed'), + ); + + $form['expose']['items_per_page'] = array( + '#type' => 'checkbox', + '#title' => t('Expose items per page'), + '#description' => t('When checked, users can determine how many items per page show in a view'), + '#default_value' => $this->options['expose']['items_per_page'], + ); + + $form['expose']['items_per_page_label'] = array( + '#type' => 'textfield', + '#title' => t('Items per page label'), + '#required' => TRUE, + '#description' => t('Label to use in the exposed items per page form element.'), + '#default_value' => $this->options['expose']['items_per_page_label'], + '#states' => array( + 'invisible' => array( + 'input[name="pager_options[expose][items_per_page]"]' => array('checked' => FALSE), + ), + ), + ); + + $form['expose']['items_per_page_options'] = array( + '#type' => 'textfield', + '#title' => t('Exposed items per page options'), + '#required' => TRUE, + '#description' => t('Set between which values the user can choose when determining the items per page. Separated by comma.'), + '#default_value' => $this->options['expose']['items_per_page_options'], + '#states' => array( + 'invisible' => array( + 'input[name="pager_options[expose][items_per_page]"]' => array('checked' => FALSE), + ), + ), + ); + + + $form['expose']['items_per_page_options_all'] = array( + '#type' => 'checkbox', + '#title' => t('Include all items option'), + '#description' => t('If checked, an extra item will be included to items per page to display all items'), + '#default_value' => $this->options['expose']['items_per_page_options_all'], + ); + + $form['expose']['items_per_page_options_all_label'] = array( + '#type' => 'textfield', + '#title' => t('All items label'), + '#description' => t('Which label will be used to display all items'), + '#default_value' => $this->options['expose']['items_per_page_options_all_label'], + '#states' => array( + 'invisible' => array( + 'input[name="pager_options[expose][items_per_page_options_all]"]' => array('checked' => FALSE), + ), + ), + ); + + $form['expose']['offset'] = array( + '#type' => 'checkbox', + '#title' => t('Expose Offset'), + '#description' => t('When checked, users can determine how many items should be skipped at the beginning.'), + '#default_value' => $this->options['expose']['offset'], + ); + + $form['expose']['offset_label'] = array( + '#type' => 'textfield', + '#title' => t('Offset label'), + '#required' => TRUE, + '#description' => t('Label to use in the exposed offset form element.'), + '#default_value' => $this->options['expose']['offset_label'], + '#states' => array( + 'invisible' => array( + 'input[name="pager_options[expose][offset]"]' => array('checked' => FALSE), + ), + ), + ); + } + + public function validateOptionsForm(&$form, &$form_state) { + // Only accept integer values. + $error = FALSE; + $exposed_options = $form_state['values']['pager_options']['expose']['items_per_page_options']; + if (strpos($exposed_options, '.') !== FALSE) { + $error = TRUE; + } + $options = explode(',', $exposed_options); + if (!$error && is_array($options)) { + foreach ($options as $option) { + if (!is_numeric($option) || intval($option) == 0) { + $error = TRUE; + } + } + } + else { + $error = TRUE; + } + if ($error) { + form_set_error('pager_options][expose][items_per_page_options', t('Please insert a list of integer numeric values separated by commas: e.g: 10, 20, 50, 100')); + } + + // Take sure that the items_per_page is part of the expose settings. + if (!empty($form_state['values']['pager_options']['expose']['items_per_page']) && !empty($form_state['values']['pager_options']['items_per_page'])) { + $items_per_page = $form_state['values']['pager_options']['items_per_page']; + if (array_search($items_per_page, $options) === FALSE) { + form_set_error('pager_options][expose][items_per_page_options', t('Please insert the items per page (@items_per_page) from above.', + array('@items_per_page' => $items_per_page)) + ); + } + } + } + + public function query() { + if ($this->items_per_page_exposed()) { + $query = drupal_container()->get('request')->query; + $items_per_page = $query->get('items_per_page'); + if ($items_per_page > 0) { + $this->options['items_per_page'] = $items_per_page; + } + elseif ($items_per_page == 'All' && $this->options['expose']['items_per_page_options_all']) { + $this->options['items_per_page'] = 0; + } + } + if ($this->offset_exposed()) { + $query = drupal_container()->get('request')->query; + $offset = $query->get('offset'); + if (isset($offset) && $offset >= 0) { + $this->options['offset'] = $offset; + } + } + + $limit = $this->options['items_per_page']; + $offset = $this->current_page * $this->options['items_per_page'] + $this->options['offset']; + if (!empty($this->options['total_pages'])) { + if ($this->current_page >= $this->options['total_pages']) { + $limit = $this->options['items_per_page']; + $offset = $this->options['total_pages'] * $this->options['items_per_page']; + } + } + + $this->view->query->set_limit($limit); + $this->view->query->set_offset($offset); + } + + function render($input) { + $pager_theme = views_theme_functions('pager', $this->view, $this->view->display_handler->display); + // The 0, 1, 3, 4 index are correct. See theme_pager documentation. + $tags = array( + 0 => $this->options['tags']['first'], + 1 => $this->options['tags']['previous'], + 3 => $this->options['tags']['next'], + 4 => $this->options['tags']['last'], + ); + $output = theme($pager_theme, array( + 'tags' => $tags, + 'element' => $this->options['id'], + 'parameters' => $input, + 'quantity' => $this->options['quantity'], + )); + return $output; + } + + /** + * Set the current page. + * + * @param $number + * If provided, the page number will be set to this. If NOT provided, + * the page number will be set from the global page array. + */ + function set_current_page($number = NULL) { + if (isset($number)) { + $this->current_page = max(0, $number); + return; + } + + // If the current page number was not specified, extract it from the global + // page array. + global $pager_page_array; + + if (empty($pager_page_array)) { + $pager_page_array = array(); + } + + // Fill in missing values in the global page array, in case the global page + // array hasn't been initialized before. + $page = drupal_container()->get('request')->query->get('page'); + $page = isset($page) ? explode(',', $page) : array(); + + for ($i = 0; $i <= $this->options['id'] || $i < count($pager_page_array); $i++) { + $pager_page_array[$i] = empty($page[$i]) ? 0 : $page[$i]; + } + + // Don't allow the number to be less than zero. + $this->current_page = max(0, intval($pager_page_array[$this->options['id']])); + } + + function get_pager_total() { + if ($items_per_page = intval($this->get_items_per_page())) { + return ceil($this->total_items / $items_per_page); + } + else { + return 1; + } + } + + /** + * Update global paging info. + * + * This is called after the count query has been run to set the total + * items available and to update the current page if the requested + * page is out of range. + */ + function update_page_info() { + if (!empty($this->options['total_pages'])) { + if (($this->options['total_pages'] * $this->options['items_per_page']) < $this->total_items) { + $this->total_items = $this->options['total_pages'] * $this->options['items_per_page']; + } + } + + // Don't set pager settings for items per page = 0. + $items_per_page = $this->get_items_per_page(); + if (!empty($items_per_page)) { + // Dump information about what we already know into the globals. + global $pager_page_array, $pager_total, $pager_total_items, $pager_limits; + // Set the limit. + $pager_limits[$this->options['id']] = $this->options['items_per_page']; + // Set the item count for the pager. + $pager_total_items[$this->options['id']] = $this->total_items; + // Calculate and set the count of available pages. + $pager_total[$this->options['id']] = $this->get_pager_total(); + + // See if the requested page was within range: + if ($this->current_page >= $pager_total[$this->options['id']]) { + // Pages are numbered from 0 so if there are 10 pages, the last page is 9. + $this->set_current_page($pager_total[$this->options['id']] - 1); + } + + // Put this number in to guarantee that we do not generate notices when the pager + // goes to look for it later. + $pager_page_array[$this->options['id']] = $this->current_page; + } + } + + function uses_exposed() { + return $this->items_per_page_exposed() || $this->offset_exposed(); + } + + function items_per_page_exposed() { + return !empty($this->options['expose']['items_per_page']); + } + + function offset_exposed() { + return !empty($this->options['expose']['offset']); + } + + function exposed_form_alter(&$form, &$form_state) { + if ($this->items_per_page_exposed()) { + $options = explode(',', $this->options['expose']['items_per_page_options']); + $sanitized_options = array(); + if (is_array($options)) { + foreach ($options as $option) { + $sanitized_options[intval($option)] = intval($option); + } + if (!empty($this->options['expose']['items_per_page_options_all']) && !empty($this->options['expose']['items_per_page_options_all_label'])) { + $sanitized_options['All'] = $this->options['expose']['items_per_page_options_all_label']; + } + $form['items_per_page'] = array( + '#type' => 'select', + '#title' => $this->options['expose']['items_per_page_label'], + '#options' => $sanitized_options, + '#default_value' => $this->get_items_per_page(), + ); + } + } + + if ($this->offset_exposed()) { + $form['offset'] = array( + '#type' => 'textfield', + '#size' => 10, + '#maxlength' => 10, + '#title' => $this->options['expose']['offset_label'], + '#default_value' => $this->get_offset(), + ); + } + } + + function exposed_form_validate(&$form, &$form_state) { + if (!empty($form_state['values']['offset']) && trim($form_state['values']['offset'])) { + if (!is_numeric($form_state['values']['offset']) || $form_state['values']['offset'] < 0) { + form_set_error('offset', t('Offset must be an number greather or equal than 0.')); + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/pager/Mini.php b/core/modules/views/lib/Drupal/views/Plugin/views/pager/Mini.php new file mode 100644 index 0000000..1fb58cf --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/pager/Mini.php @@ -0,0 +1,40 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\pager\Mini. + */ + +namespace Drupal\views\Plugin\views\pager; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The plugin to handle full pager. + * + * @ingroup views_pager_plugins + * + * @Plugin( + * id = "mini", + * title = @Translation("Paged output, mini pager"), + * short_title = @Translation("Mini"), + * help = @Translation("Use the mini pager output.") + * ) + */ +class Mini extends PagerPluginBase { + + public function summaryTitle() { + if (!empty($this->options['offset'])) { + return format_plural($this->options['items_per_page'], 'Mini pager, @count item, skip @skip', 'Mini pager, @count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], 'Mini pager, @count item', 'Mini pager, @count items', array('@count' => $this->options['items_per_page'])); + } + + function render($input) { + $pager_theme = views_theme_functions('views_mini_pager', $this->view, $this->view->display_handler->display); + return theme($pager_theme, array( + 'parameters' => $input, 'element' => $this->options['id'])); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/pager/None.php b/core/modules/views/lib/Drupal/views/Plugin/views/pager/None.php new file mode 100644 index 0000000..7d6ec89 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/pager/None.php @@ -0,0 +1,89 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\pager\None. + */ + +namespace Drupal\views\Plugin\views\pager; + +use Drupal\views\ViewExecutable; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Plugin for views without pagers. + * + * @ingroup views_pager_plugins + * + * @Plugin( + * id = "none", + * title = @Translation("Display all items"), + * help = @Translation("Display all items that this view might find."), + * type = "basic" + * ) + */ +class None extends PagerPluginBase { + + public function init(ViewExecutable $view, &$display, $options = array()) { + parent::init($view, $display, $options); + + // If the pager is set to none, then it should show all items. + $this->set_items_per_page(0); + } + + public function summaryTitle() { + if (!empty($this->options['offset'])) { + return t('All items, skip @skip', array('@skip' => $this->options['offset'])); + } + return t('All items'); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['offset'] = array('default' => 0); + + return $options; + } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['offset'] = array( + '#type' => 'textfield', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + } + + function use_pager() { + return FALSE; + } + + function use_count_query() { + return FALSE; + } + + function get_items_per_page() { + return 0; + } + + function execute_count_query(&$count_query) { + // If we are displaying all items, never count. But we can update the count in post_execute. + } + + public function postExecute(&$result) { + $this->total_items = count($result); + } + + public function query() { + // The only query modifications we might do are offsets. + if (!empty($this->options['offset'])) { + $this->view->query->set_offset($this->options['offset']); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/pager/PagerPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/pager/PagerPluginBase.php new file mode 100644 index 0000000..11b8724 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/pager/PagerPluginBase.php @@ -0,0 +1,253 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\pager\PagerPluginBase. + */ + +namespace Drupal\views\Plugin\views\pager; + +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\ViewExecutable; + +/** + * @defgroup views_pager_plugins Views pager plugins + * @{ + * The base plugin to handler pagers of a view. + * + * The pager takes care about altering the query for its needs, altering some + * global information of pagers and finally rendering itself. + * + * @see hook_views_plugins() + */ + +/** + * The base plugin to handle pager. + */ +abstract class PagerPluginBase extends PluginBase { + + var $current_page = NULL; + + var $total_items = 0; + + /** + * Overrides Drupal\views\Plugin\Plugin::$usesOptions. + */ + protected $usesOptions = TRUE; + + /** + * Initialize the plugin. + * + * @param $view + * The view object. + * @param $display + * The display handler. + */ + public function init(ViewExecutable $view, &$display, $options = array()) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->displayHandler = &$display; + + $this->unpackOptions($this->options, $options); + } + + /** + * Get how many items per page this pager will display. + * + * All but the leanest pagers should probably return a value here, so + * most pagers will not need to override this method. + */ + function get_items_per_page() { + return isset($this->options['items_per_page']) ? $this->options['items_per_page'] : 0; + } + + /** + * Set how many items per page this pager will display. + * + * This is mostly used for things that will override the value. + */ + function set_items_per_page($items) { + $this->options['items_per_page'] = $items; + } + + /** + * Get the page offset, or how many items to skip. + * + * Even pagers that don't actually page can skip items at the beginning, + * so few pagers will need to override this method. + */ + function get_offset() { + return isset($this->options['offset']) ? $this->options['offset'] : 0; + } + + /** + * Set the page offset, or how many items to skip. + */ + function set_offset($offset) { + $this->options['offset'] = $offset; + } + + /** + * Get the current page. + * + * If NULL, we do not know what the current page is. + */ + function get_current_page() { + return $this->current_page; + } + + /** + * Set the current page. + * + * @param $number + * If provided, the page number will be set to this. If NOT provided, + * the page number will be set from the global page array. + */ + function set_current_page($number = NULL) { + if (!is_numeric($number) || $number < 0) { + $number = 0; + } + $this->current_page = $number; + } + + /** + * Get the total number of items. + * + * If NULL, we do not yet know what the total number of items are. + */ + function get_total_items() { + return $this->total_items; + } + + /** + * Get the pager id, if it exists + */ + function get_pager_id() { + return isset($this->options['id']) ? $this->options['id'] : 0; + } + + /** + * Provide the default form form for validating options + */ + public function validateOptionsForm(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + public function submitOptionsForm(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * pager plugin. + */ + public function summaryTitle() { + return t('Unknown'); + } + + /** + * Determine if this pager actually uses a pager. + * + * Only a couple of very specific pagers will set this to false. + */ + function use_pager() { + return TRUE; + } + + /** + * Determine if a pager needs a count query. + * + * If a pager needs a count query, a simple query + */ + function use_count_query() { + return TRUE; + } + + /** + * Execute the count query, which will be done just prior to the query + * itself being executed. + */ + function execute_count_query(&$count_query) { + $this->total_items = $count_query->execute()->fetchField(); + if (!empty($this->options['offset'])) { + $this->total_items -= $this->options['offset']; + } + + $this->update_page_info(); + return $this->total_items; + } + + /** + * If there are pagers that need global values set, this method can + * be used to set them. It will be called when the count query is run. + */ + function update_page_info() { + + } + + /** + * Modify the query for paging + * + * This is called during the build phase and can directly modify the query. + */ + public function query() { } + + /** + * Perform any needed actions just prior to the query executing. + */ + function pre_execute(&$query) { } + + /** + * Perform any needed actions just after the query executing. + */ + public function postExecute(&$result) { } + + /** + * Perform any needed actions just before rendering. + */ + function pre_render(&$result) { } + + /** + * Render the pager. + * + * Called during the view render process, this will render the + * pager. + * + * @param $input + * Any extra GET parameters that should be retained, such as exposed + * input. + */ + function render($input) { } + + /** + * Determine if there are more records available. + * + * This is primarily used to control the display of a more link. + */ + function has_more_records() { + return $this->get_items_per_page() + && $this->total_items > (intval($this->current_page) + 1) * $this->get_items_per_page(); + } + + function exposed_form_alter(&$form, &$form_state) { } + + function exposed_form_validate(&$form, &$form_state) { } + + function exposed_form_submit(&$form, &$form_state, &$exclude) { } + + function uses_exposed() { + return FALSE; + } + + function items_per_page_exposed() { + return FALSE; + } + + function offset_exposed() { + return FALSE; + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/pager/Some.php b/core/modules/views/lib/Drupal/views/Plugin/views/pager/Some.php new file mode 100644 index 0000000..a917ed6 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/pager/Some.php @@ -0,0 +1,76 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\pager\Some. + */ + +namespace Drupal\views\Plugin\views\pager; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Plugin for views without pagers. + * + * @ingroup views_pager_plugins + * + * @Plugin( + * id = "some", + * title = @Translation("Display a specified number of items"), + * help = @Translation("Display a limited number items that this view might find."), + * type = "basic" + * ) + */ +class Some extends PagerPluginBase { + + public function summaryTitle() { + if (!empty($this->options['offset'])) { + return format_plural($this->options['items_per_page'], '@count item, skip @skip', '@count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], '@count item', '@count items', array('@count' => $this->options['items_per_page'])); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['items_per_page'] = array('default' => 10); + $options['offset'] = array('default' => 0); + + return $options; + } + + /** + * Provide the default form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $pager_text = $this->displayHandler->getPagerText(); + $form['items_per_page'] = array( + '#title' => $pager_text['items per page title'], + '#type' => 'textfield', + '#description' => $pager_text['items per page description'], + '#default_value' => $this->options['items_per_page'], + ); + + $form['offset'] = array( + '#type' => 'textfield', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + } + + function use_pager() { + return FALSE; + } + + function use_count_query() { + return FALSE; + } + + public function query() { + $this->view->query->set_limit($this->options['items_per_page']); + $this->view->query->set_offset($this->options['offset']); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/query/QueryInterface.php b/core/modules/views/lib/Drupal/views/Plugin/views/query/QueryInterface.php new file mode 100644 index 0000000..0b00a17 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/query/QueryInterface.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\query\QueryInterface. + */ + +namespace Drupal\views\Plugin\views\query; + +use Drupal\views\Plugin\views\PluginInterface; + +/** + * @todo. + */ +interface QueryInterface extends PluginInterface { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/query/QueryPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/query/QueryPluginBase.php new file mode 100644 index 0000000..0cc44c3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/query/QueryPluginBase.php @@ -0,0 +1,167 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\query\QueryPluginBase. + */ + +namespace Drupal\views\Plugin\views\query; + +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\ViewExecutable; + +/** + * @todo. + */ +abstract class QueryPluginBase extends PluginBase implements QueryInterface { + + /** + * A pager plugin that should be provided by the display. + * + * @var views_plugin_pager + */ + var $pager = NULL; + + /** + * Constructor; Create the basic query object and fill with default values. + */ + public function init($base_table, $base_field, $options) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->base_table = $base_table; + $this->base_field = $base_field; + $this->unpackOptions($this->options, $options); + } + + /** + * Generate a query and a countquery from all of the information supplied + * to the object. + * + * @param $get_count + * Provide a countquery if this is true, otherwise provide a normal query. + */ + public function query($get_count = FALSE) { } + + /** + * Let modules modify the query just prior to finalizing it. + * + * @param view $view + * The view which is executed. + */ + function alter(ViewExecutable $view) { } + + /** + * Builds the necessary info to execute the query. + * + * @param view $view + * The view which is executed. + */ + function build(ViewExecutable $view) { } + + /** + * Executes the query and fills the associated view object with according + * values. + * + * Values to set: $view->result, $view->total_rows, $view->execute_time, + * $view->pager['current_page']. + * + * $view->result should contain an array of objects. The array must use a + * numeric index starting at 0. + * + * @param view $view + * The view which is executed. + */ + function execute(ViewExecutable $view) { } + + /** + * Add a signature to the query, if such a thing is feasible. + * + * This signature is something that can be used when perusing query logs to + * discern where particular queries might be coming from. + * + * @param view $view + * The view which is executed. + */ + function add_signature(ViewExecutable $view) { } + + /** + * Get aggregation info for group by queries. + * + * If NULL, aggregation is not allowed. + */ + function get_aggregation_info() { } + + public function validateOptionsForm(&$form, &$form_state) { } + + public function submitOptionsForm(&$form, &$form_state) { } + + public function summaryTitle() { + return t('Settings'); + } + + /** + * Set a LIMIT on the query, specifying a maximum number of results. + */ + function set_limit($limit) { + $this->limit = $limit; + } + + /** + * Set an OFFSET on the query, specifying a number of results to skip + */ + function set_offset($offset) { + $this->offset = $offset; + } + + /** + * Create a new grouping for the WHERE or HAVING clause. + * + * @param $type + * Either 'AND' or 'OR'. All items within this group will be added + * to the WHERE clause with this logical operator. + * @param $group + * An ID to use for this group. If unspecified, an ID will be generated. + * @param $where + * 'where' or 'having'. + * + * @return $group + * The group ID generated. + */ + function set_where_group($type = 'AND', $group = NULL, $where = 'where') { + // Set an alias. + $groups = &$this->$where; + + if (!isset($group)) { + $group = empty($groups) ? 1 : max(array_keys($groups)) + 1; + } + + // Create an empty group + if (empty($groups[$group])) { + $groups[$group] = array('conditions' => array(), 'args' => array()); + } + + $groups[$group]['type'] = strtoupper($type); + return $group; + } + + /** + * Control how all WHERE and HAVING groups are put together. + * + * @param $type + * Either 'AND' or 'OR' + */ + function set_group_operator($type = 'AND') { + $this->group_operator = strtoupper($type); + } + + /** + * Loads all entities contained in the passed-in $results. + *. + * If the entity belongs to the base table, then it gets stored in + * $result->_entity. Otherwise, it gets stored in + * $result->_relationship_entities[$relationship_id]; + * + * Query plugins that don't support entities can leave the method empty. + */ + function load_entities(&$results) {} + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/query/Sql.php b/core/modules/views/lib/Drupal/views/Plugin/views/query/Sql.php new file mode 100644 index 0000000..2a43726 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/query/Sql.php @@ -0,0 +1,1745 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\query\Sql. + */ + +namespace Drupal\views\Plugin\views\query; + +use Drupal\Core\Database\Database; +use Drupal\Core\Database\DatabaseExceptionWrapper; +use Drupal\views\Plugin\views\join\JoinPluginBase; +use Drupal\views\Plugin\views\HandlerBase; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\ViewExecutable; + +/** + * @todo. + * + * @Plugin( + * id = "views_query", + * title = @Translation("SQL Query"), + * help = @Translation("Query will be generated and run using the Drupal database API.") + * ) + */ +class Sql extends QueryPluginBase { + + /** + * A list of tables in the order they should be added, keyed by alias. + */ + var $table_queue = array(); + + /** + * Holds an array of tables and counts added so that we can create aliases + */ + var $tables = array(); + + /** + * Holds an array of relationships, which are aliases of the primary + * table that represent different ways to join the same table in. + */ + var $relationships = array(); + + /** + * An array of sections of the WHERE query. Each section is in itself + * an array of pieces and a flag as to whether or not it should be AND + * or OR. + */ + var $where = array(); + /** + * An array of sections of the HAVING query. Each section is in itself + * an array of pieces and a flag as to whether or not it should be AND + * or OR. + */ + var $having = array(); + /** + * The default operator to use when connecting the WHERE groups. May be + * AND or OR. + */ + var $group_operator = 'AND'; + + /** + * A simple array of order by clauses. + */ + var $orderby = array(); + + /** + * A simple array of group by clauses. + */ + var $groupby = array(); + + + /** + * An array of fields. + */ + var $fields = array(); + + + /** + * The table header to use for tablesort. This matters because tablesort + * needs to modify the query and needs the header. + */ + var $header = array(); + + /** + * A flag as to whether or not to make the primary field distinct. + */ + var $distinct = FALSE; + + var $has_aggregate = FALSE; + + /** + * Should this query be optimized for counts, for example no sorts. + */ + var $get_count_optimized = NULL; + + /** + * An array mapping table aliases and field names to field aliases. + */ + var $field_aliases = array(); + + /** + * Query tags which will be passed over to the dbtng query object. + */ + var $tags = array(); + + /** + * Is the view marked as not distinct. + * + * @var bool + */ + var $no_distinct; + + /** + * Constructor; Create the basic query object and fill with default values. + */ + public function init($base_table = 'node', $base_field = 'nid', $options) { + parent::init($base_table, $base_field, $options); + $this->base_table = $base_table; // Predefine these above, for clarity. + $this->base_field = $base_field; + $this->relationships[$base_table] = array( + 'link' => NULL, + 'table' => $base_table, + 'alias' => $base_table, + 'base' => $base_table + ); + + // init the table queue with our primary table. + $this->table_queue[$base_table] = array( + 'alias' => $base_table, + 'table' => $base_table, + 'relationship' => $base_table, + 'join' => NULL, + ); + + // init the tables with our primary table + $this->tables[$base_table][$base_table] = array( + 'count' => 1, + 'alias' => $base_table, + ); + + $this->count_field = array( + 'table' => $base_table, + 'field' => $base_field, + 'alias' => $base_field, + 'count' => TRUE, + ); + } + + // ---------------------------------------------------------------- + // Utility methods to set flags and data. + + /** + * Set the view to be distinct (per base field). + * + * @param bool $value + * Should the view by distincted. + */ + function set_distinct($value = TRUE) { + if (!(isset($this->no_distinct) && $value)) { + $this->distinct = $value; + } + } + + /** + * Set what field the query will count() on for paging. + */ + function set_count_field($table, $field, $alias = NULL) { + if (empty($alias)) { + $alias = $table . '_' . $field; + } + $this->count_field = array( + 'table' => $table, + 'field' => $field, + 'alias' => $alias, + 'count' => TRUE, + ); + } + + /** + * Set the table header; used for click-sorting because it's needed + * info to modify the ORDER BY clause. + */ + function set_header($header) { + $this->header = $header; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['disable_sql_rewrite'] = array( + 'default' => FALSE, + 'translatable' => FALSE, + 'bool' => TRUE, + ); + $options['distinct'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + $options['slave'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + $options['query_comment'] = array( + 'default' => '', + ); + $options['query_tags'] = array( + 'default' => array(), + ); + + return $options; + } + + /** + * Add settings for the ui. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['disable_sql_rewrite'] = array( + '#title' => t('Disable SQL rewriting'), + '#description' => t('Disabling SQL rewriting will disable node_access checks as well as other modules that implement hook_query_alter().'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['disable_sql_rewrite']), + '#suffix' => '<div class="messages warning sql-rewrite-warning js-hide">' . t('WARNING: Disabling SQL rewriting means that node access security is disabled. This may allow users to see data they should not be able to see if your view is misconfigured. Please use this option only if you understand and accept this security risk.') . '</div>', + ); + $form['distinct'] = array( + '#type' => 'checkbox', + '#title' => t('Distinct'), + '#description' => t('This will make the view display only distinct items. If there are multiple identical items, each will be displayed only once. You can use this to try and remove duplicates from a view, though it does not always work. Note that this can slow queries down, so use it with caution.'), + '#default_value' => !empty($this->options['distinct']), + ); + $form['slave'] = array( + '#type' => 'checkbox', + '#title' => t('Use Slave Server'), + '#description' => t('This will make the query attempt to connect to a slave server if available. If no slave server is defined or available, it will fall back to the default server.'), + '#default_value' => !empty($this->options['slave']), + ); + $form['query_comment'] = array( + '#type' => 'textfield', + '#title' => t('Query Comment'), + '#description' => t('If set, this comment will be embedded in the query and passed to the SQL server. This can be helpful for logging or debugging.'), + '#default_value' => $this->options['query_comment'], + ); + $form['query_tags'] = array( + '#type' => 'textfield', + '#title' => t('Query Tags'), + '#description' => t('If set, these tags will be appended to the query and can be used to identify the query in a module. This can be helpful for altering queries.'), + '#default_value' => implode(', ', $this->options['query_tags']), + '#element_validate' => array('views_element_validate_tags'), + ); + $form_state['build_info']['files']['foo'] = drupal_get_path('module', 'views') . '/lib/Drupal/views/Plugin/Query/SqlQuery.php'; + } + + /** + * Special submit handling. + */ + public function submitOptionsForm(&$form, &$form_state) { + $element = array('#parents' => array('query', 'options', 'query_tags')); + $value = explode(',', drupal_array_get_nested_value($form_state['values'], $element['#parents'])); + $value = array_filter(array_map('trim', $value)); + form_set_value($element, $value, $form_state); + } + + // ---------------------------------------------------------------- + // Table/join adding + + /** + * A relationship is an alternative endpoint to a series of table + * joins. Relationships must be aliases of the primary table and + * they must join either to the primary table or to a pre-existing + * relationship. + * + * An example of a relationship would be a nodereference table. + * If you have a nodereference named 'book_parent' which links to a + * parent node, you could set up a relationship 'node_book_parent' + * to 'node'. Then, anything that links to 'node' can link to + * 'node_book_parent' instead, thus allowing all properties of + * both nodes to be available in the query. + * + * @param $alias + * What this relationship will be called, and is also the alias + * for the table. + * @param Drupal\views\Plugin\views\join\JoinPluginBase $join + * A Join object (or derived object) to join the alias in. + * @param $base + * The name of the 'base' table this relationship represents; this + * tells the join search which path to attempt to use when finding + * the path to this relationship. + * @param $link_point + * If this relationship links to something other than the primary + * table, specify that table here. For example, a 'track' node + * might have a relationship to an 'album' node, which might + * have a relationship to an 'artist' node. + */ + function add_relationship($alias, JoinPluginBase $join, $base, $link_point = NULL) { + if (empty($link_point)) { + $link_point = $this->base_table; + } + elseif (!array_key_exists($link_point, $this->relationships)) { + return FALSE; + } + + // Make sure $alias isn't already used; if it, start adding stuff. + $alias_base = $alias; + $count = 1; + while (!empty($this->relationships[$alias])) { + $alias = $alias_base . '_' . $count++; + } + + // Make sure this join is adjusted for our relationship. + if ($link_point && isset($this->relationships[$link_point])) { + $join = $this->adjust_join($join, $link_point); + } + + // Add the table directly to the queue to avoid accidentally marking + // it. + $this->table_queue[$alias] = array( + 'table' => $join->table, + 'num' => 1, + 'alias' => $alias, + 'join' => $join, + 'relationship' => $link_point, + ); + + $this->relationships[$alias] = array( + 'link' => $link_point, + 'table' => $join->table, + 'base' => $base, + ); + + $this->tables[$this->base_table][$alias] = array( + 'count' => 1, + 'alias' => $alias, + ); + + return $alias; + } + + /** + * Add a table to the query, ensuring the path exists. + * + * This function will test to ensure that the path back to the primary + * table is valid and exists; if you do not wish for this testing to + * occur, use $query->queue_table() instead. + * + * @param $table + * The name of the table to add. It needs to exist in the global table + * array. + * @param $relationship + * An alias of a table; if this is set, the path back to this table will + * be tested prior to adding the table, making sure that all intermediary + * tables exist and are properly aliased. If set to NULL the path to + * the primary table will be ensured. If the path cannot be made, the + * table will NOT be added. + * @param Drupal\views\Plugin\views\join\JoinPluginBase $join + * In some join configurations this table may actually join back through + * a different method; this is most likely to be used when tracing + * a hierarchy path. (node->parent->parent2->parent3). This parameter + * will specify how this table joins if it is not the default. + * @param $alias + * A specific alias to use, rather than the default alias. + * + * @return $alias + * The alias of the table; this alias can be used to access information + * about the table and should always be used to refer to the table when + * adding parts to the query. Or FALSE if the table was not able to be + * added. + */ + function add_table($table, $relationship = NULL, JoinPluginBase $join = NULL, $alias = NULL) { + if (!$this->ensure_path($table, $relationship, $join)) { + return FALSE; + } + + if ($join && $relationship) { + $join = $this->adjust_join($join, $relationship); + } + + return $this->queue_table($table, $relationship, $join, $alias); + } + + /** + * Add a table to the query without ensuring the path. + * + * This is a pretty internal function to Views and add_table() or + * ensure_table() should be used instead of this one, unless you are + * absolutely sure this is what you want. + * + * @param $table + * The name of the table to add. It needs to exist in the global table + * array. + * @param $relationship + * The primary table alias this table is related to. If not set, the + * primary table will be used. + * @param Drupal\views\Plugin\views\join\JoinPluginBase $join + * In some join configurations this table may actually join back through + * a different method; this is most likely to be used when tracing + * a hierarchy path. (node->parent->parent2->parent3). This parameter + * will specify how this table joins if it is not the default. + * @param $alias + * A specific alias to use, rather than the default alias. + * + * @return $alias + * The alias of the table; this alias can be used to access information + * about the table and should always be used to refer to the table when + * adding parts to the query. Or FALSE if the table was not able to be + * added. + */ + function queue_table($table, $relationship = NULL, JoinPluginBase $join = NULL, $alias = NULL) { + // If the alias is set, make sure it doesn't already exist. + if (isset($this->table_queue[$alias])) { + return $alias; + } + + if (empty($relationship)) { + $relationship = $this->base_table; + } + + if (!array_key_exists($relationship, $this->relationships)) { + return FALSE; + } + + if (!$alias && $join && $relationship && !empty($join->adjusted) && $table != $join->table) { + if ($relationship == $this->base_table) { + $alias = $table; + } + else { + $alias = $relationship . '_' . $table; + } + } + + // Check this again to make sure we don't blow up existing aliases for already + // adjusted joins. + if (isset($this->table_queue[$alias])) { + return $alias; + } + + $alias = $this->mark_table($table, $relationship, $alias); + + // If no alias is specified, give it the default. + if (!isset($alias)) { + $alias = $this->tables[$relationship][$table]['alias'] . $this->tables[$relationship][$table]['count']; + } + + // If this is a relationship based table, add a marker with + // the relationship as a primary table for the alias. + if ($table != $alias) { + $this->mark_table($alias, $this->base_table, $alias); + } + + // If no join is specified, pull it from the table data. + if (!isset($join)) { + $join = $this->get_join_data($table, $this->relationships[$relationship]['base']); + if (empty($join)) { + return FALSE; + } + + $join = $this->adjust_join($join, $relationship); + } + + $this->table_queue[$alias] = array( + 'table' => $table, + 'num' => $this->tables[$relationship][$table]['count'], + 'alias' => $alias, + 'join' => $join, + 'relationship' => $relationship, + ); + + return $alias; + } + + function mark_table($table, $relationship, $alias) { + // Mark that this table has been added. + if (empty($this->tables[$relationship][$table])) { + if (!isset($alias)) { + $alias = ''; + if ($relationship != $this->base_table) { + // double underscore will help prevent accidental name + // space collisions. + $alias = $relationship . '__'; + } + $alias .= $table; + } + $this->tables[$relationship][$table] = array( + 'count' => 1, + 'alias' => $alias, + ); + } + else { + $this->tables[$relationship][$table]['count']++; + } + + return $alias; + } + + /** + * Ensure a table exists in the queue; if it already exists it won't + * do anything, but if it doesn't it will add the table queue. It will ensure + * a path leads back to the relationship table. + * + * @param $table + * The unaliased name of the table to ensure. + * @param $relationship + * The relationship to ensure the table links to. Each relationship will + * get a unique instance of the table being added. If not specified, + * will be the primary table. + * @param Drupal\views\Plugin\views\join\JoinPluginBase $join + * A Join object (or derived object) to join the alias in. + * + * @return + * The alias used to refer to this specific table, or NULL if the table + * cannot be ensured. + */ + function ensure_table($table, $relationship = NULL, JoinPluginBase $join = NULL) { + // ensure a relationship + if (empty($relationship)) { + $relationship = $this->base_table; + } + + // If the relationship is the primary table, this actually be a relationship + // link back from an alias. We store all aliases along with the primary table + // to detect this state, because eventually it'll hit a table we already + // have and that's when we want to stop. + if ($relationship == $this->base_table && !empty($this->tables[$relationship][$table])) { + return $this->tables[$relationship][$table]['alias']; + } + + if (!array_key_exists($relationship, $this->relationships)) { + return FALSE; + } + + if ($table == $this->relationships[$relationship]['base']) { + return $relationship; + } + + // If we do not have join info, fetch it. + if (!isset($join)) { + $join = $this->get_join_data($table, $this->relationships[$relationship]['base']); + } + + // If it can't be fetched, this won't work. + if (empty($join)) { + return; + } + + // Adjust this join for the relationship, which will ensure that the 'base' + // table it links to is correct. Tables adjoined to a relationship + // join to a link point, not the base table. + $join = $this->adjust_join($join, $relationship); + + if ($this->ensure_path($table, $relationship, $join)) { + // Attempt to eliminate redundant joins. If this table's + // relationship and join exactly matches an existing table's + // relationship and join, we do not have to join to it again; + // just return the existing table's alias. See + // http://groups.drupal.org/node/11288 for details. + // + // This can be done safely here but not lower down in + // queue_table(), because queue_table() is also used by + // add_table() which requires the ability to intentionally add + // the same table with the same join multiple times. For + // example, a view that filters on 3 taxonomy terms using AND + // needs to join taxonomy_term_data 3 times with the same join. + + // scan through the table queue to see if a matching join and + // relationship exists. If so, use it instead of this join. + + // TODO: Scanning through $this->table_queue results in an + // O(N^2) algorithm, and this code runs every time the view is + // instantiated (Views 2 does not currently cache queries). + // There are a couple possible "improvements" but we should do + // some performance testing before picking one. + foreach ($this->table_queue as $queued_table) { + // In PHP 4 and 5, the == operation returns TRUE for two objects + // if they are instances of the same class and have the same + // attributes and values. + if ($queued_table['relationship'] == $relationship && $queued_table['join'] == $join) { + return $queued_table['alias']; + } + } + + return $this->queue_table($table, $relationship, $join); + } + } + + /** + * Make sure that the specified table can be properly linked to the primary + * table in the JOINs. This function uses recursion. If the tables + * needed to complete the path back to the primary table are not in the + * query they will be added, but additional copies will NOT be added + * if the table is already there. + */ + function ensure_path($table, $relationship = NULL, $join = NULL, $traced = array(), $add = array()) { + if (!isset($relationship)) { + $relationship = $this->base_table; + } + + if (!array_key_exists($relationship, $this->relationships)) { + return FALSE; + } + + // If we do not have join info, fetch it. + if (!isset($join)) { + $join = $this->get_join_data($table, $this->relationships[$relationship]['base']); + } + + // If it can't be fetched, this won't work. + if (empty($join)) { + return FALSE; + } + + // Does a table along this path exist? + if (isset($this->tables[$relationship][$table]) || + ($join && $join->leftTable == $relationship) || + ($join && $join->leftTable == $this->relationships[$relationship]['table'])) { + + // Make sure that we're linking to the correct table for our relationship. + foreach (array_reverse($add) as $table => $path_join) { + $this->queue_table($table, $relationship, $this->adjust_join($path_join, $relationship)); + } + return TRUE; + } + + // Have we been this way? + if (isset($traced[$join->leftTable])) { + // we looped. Broked. + return FALSE; + } + + // Do we have to add this table? + $left_join = $this->get_join_data($join->leftTable, $this->relationships[$relationship]['base']); + if (!isset($this->tables[$relationship][$join->leftTable])) { + $add[$join->leftTable] = $left_join; + } + + // Keep looking. + $traced[$join->leftTable] = TRUE; + return $this->ensure_path($join->leftTable, $relationship, $left_join, $traced, $add); + } + + /** + * Fix a join to adhere to the proper relationship; the left table can vary + * based upon what relationship items are joined in on. + */ + function adjust_join($join, $relationship) { + if (!empty($join->adjusted)) { + return $join; + } + + if (empty($relationship) || empty($this->relationships[$relationship])) { + return $join; + } + + // Adjusts the left table for our relationship. + if ($relationship != $this->base_table) { + // If we're linking to the primary table, the relationship to use will + // be the prior relationship. Unless it's a direct link. + + // Safety! Don't modify an original here. + $join = clone $join; + + // Do we need to try to ensure a path? + if ($join->leftTable != $this->relationships[$relationship]['table'] && + $join->leftTable != $this->relationships[$relationship]['base'] && + !isset($this->tables[$relationship][$join->leftTable]['alias'])) { + $this->ensure_table($join->leftTable, $relationship); + } + + // First, if this is our link point/anchor table, just use the relationship + if ($join->leftTable == $this->relationships[$relationship]['table']) { + $join->leftTable = $relationship; + } + // then, try the base alias. + elseif (isset($this->tables[$relationship][$join->leftTable]['alias'])) { + $join->leftTable = $this->tables[$relationship][$join->leftTable]['alias']; + } + // But if we're already looking at an alias, use that instead. + elseif (isset($this->table_queue[$relationship]['alias'])) { + $join->leftTable = $this->table_queue[$relationship]['alias']; + } + } + + $join->adjusted = TRUE; + return $join; + } + + /** + * Retrieve join data from the larger join data cache. + * + * @param $table + * The table to get the join information for. + * @param $base_table + * The path we're following to get this join. + * + * @return Drupal\views\Plugin\views\join\JoinPluginBase + * A Join object or child object, if one exists. + */ + function get_join_data($table, $base_table) { + // Check to see if we're linking to a known alias. If so, get the real + // table's data instead. + if (!empty($this->table_queue[$table])) { + $table = $this->table_queue[$table]['table']; + } + return HandlerBase::getTableJoin($table, $base_table); + } + + /** + * Get the information associated with a table. + * + * If you need the alias of a table with a particular relationship, use + * ensure_table(). + */ + function get_table_info($table) { + if (!empty($this->table_queue[$table])) { + return $this->table_queue[$table]; + } + + // In rare cases we might *only* have aliased versions of the table. + if (!empty($this->tables[$this->base_table][$table])) { + $alias = $this->tables[$this->base_table][$table]['alias']; + if (!empty($this->table_queue[$alias])) { + return $this->table_queue[$alias]; + } + } + } + + /** + * Add a field to the query table, possibly with an alias. This will + * automatically call ensure_table to make sure the required table + * exists, *unless* $table is unset. + * + * @param $table + * The table this field is attached to. If NULL, it is assumed this will + * be a formula; otherwise, ensure_table is used to make sure the + * table exists. + * @param $field + * The name of the field to add. This may be a real field or a formula. + * @param $alias + * The alias to create. If not specified, the alias will be $table_$field + * unless $table is NULL. When adding formulae, it is recommended that an + * alias be used. + * @param $params + * An array of parameters additional to the field that will control items + * such as aggregation functions and DISTINCT. + * + * @return $name + * The name that this field can be referred to as. Usually this is the alias. + */ + function add_field($table, $field, $alias = '', $params = array()) { + // We check for this specifically because it gets a special alias. + if ($table == $this->base_table && $field == $this->base_field && empty($alias)) { + $alias = $this->base_field; + } + + if ($table && empty($this->table_queue[$table])) { + $this->ensure_table($table); + } + + if (!$alias && $table) { + $alias = $table . '_' . $field; + } + + // Make sure an alias is assigned + $alias = $alias ? $alias : $field; + + // PostgreSQL truncates aliases to 63 characters: http://drupal.org/node/571548 + + // We limit the length of the original alias up to 60 characters + // to get a unique alias later if its have duplicates + $alias = strtolower(substr($alias, 0, 60)); + + // Create a field info array. + $field_info = array( + 'field' => $field, + 'table' => $table, + 'alias' => $alias, + ) + $params; + + // Test to see if the field is actually the same or not. Due to + // differing parameters changing the aggregation function, we need + // to do some automatic alias collision detection: + $base = $alias; + $counter = 0; + while (!empty($this->fields[$alias]) && $this->fields[$alias] != $field_info) { + $field_info['alias'] = $alias = $base . '_' . ++$counter; + } + + if (empty($this->fields[$alias])) { + $this->fields[$alias] = $field_info; + } + + // Keep track of all aliases used. + $this->field_aliases[$table][$field] = $alias; + + return $alias; + } + + /** + * Remove all fields that may've been added; primarily used for summary + * mode where we're changing the query because we didn't get data we needed. + */ + function clear_fields() { + $this->fields = array(); + } + + /** + * Add a simple WHERE clause to the query. The caller is responsible for + * ensuring that all fields are fully qualified (TABLE.FIELD) and that + * the table already exists in the query. + * + * @param $group + * The WHERE group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param $field + * The name of the field to check. + * @param $value + * The value to test the field against. In most cases, this is a scalar. For more + * complex options, it is an array. The meaning of each element in the array is + * dependent on the $operator. + * @param $operator + * The comparison operator, such as =, <, or >=. It also accepts more complex + * options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is an array + * = otherwise. If $field is a string you have to use 'formula' here. + * + * The $field, $value and $operator arguments can also be passed in with a + * single DatabaseCondition object, like this: + * @code + * $this->query->add_where( + * $this->options['group'], + * db_or() + * ->condition($field, $value, 'NOT IN') + * ->condition($field, $value, 'IS NULL') + * ); + * @endcode + * + * @see Drupal\Core\Database\Query\ConditionInterface::condition() + * @see Drupal\Core\Database\Query\Condition + */ + function add_where($group, $field, $value = NULL, $operator = NULL) { + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + + // Check for a group. + if (!isset($this->where[$group])) { + $this->set_where_group('AND', $group); + } + + $this->where[$group]['conditions'][] = array( + 'field' => $field, + 'value' => $value, + 'operator' => $operator, + ); + } + + /** + * Add a complex WHERE clause to the query. + * + * The caller is reponsible for ensuring that all fields are fully qualified + * (TABLE.FIELD) and that the table already exists in the query. + * Internally the dbtng method "where" is used. + * + * @param $group + * The WHERE group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param $snippet + * The snippet to check. This can be either a column or + * a complex expression like "UPPER(table.field) = 'value'" + * @param $args + * An associative array of arguments. + * + * @see QueryConditionInterface::where() + */ + function add_where_expression($group, $snippet, $args = array()) { + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + + // Check for a group. + if (!isset($this->where[$group])) { + $this->set_where_group('AND', $group); + } + + $this->where[$group]['conditions'][] = array( + 'field' => $snippet, + 'value' => $args, + 'operator' => 'formula', + ); + } + + /** + * Add a simple HAVING clause to the query. + * + * The caller is responsible for ensuring that all fields are fully qualified + * (TABLE.FIELD) and that the table and an appropriate GROUP BY already exist in the query. + * Internally the dbtng method "havingCondition" is used. + * + * @param $group + * The HAVING group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param $field + * The name of the field to check. + * @param $value + * The value to test the field against. In most cases, this is a scalar. For more + * complex options, it is an array. The meaning of each element in the array is + * dependent on the $operator. + * @param $operator + * The comparison operator, such as =, <, or >=. It also accepts more complex + * options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is an array + * = otherwise. If $field is a string you have to use 'formula' here. + * + * @see SelectQueryInterface::havingCondition() + */ + function add_having($group, $field, $value = NULL, $operator = NULL) { + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + + // Check for a group. + if (!isset($this->having[$group])) { + $this->set_where_group('AND', $group, 'having'); + } + + // Add the clause and the args. + $this->having[$group]['conditions'][] = array( + 'field' => $field, + 'value' => $value, + 'operator' => $operator, + ); + } + + /** + * Add a complex HAVING clause to the query. + * The caller is responsible for ensuring that all fields are fully qualified + * (TABLE.FIELD) and that the table and an appropriate GROUP BY already exist in the query. + * Internally the dbtng method "having" is used. + * + * @param $group + * The HAVING group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param $snippet + * The snippet to check. This can be either a column or + * a complex expression like "COUNT(table.field) > 3" + * @param $args + * An associative array of arguments. + * + * @see QueryConditionInterface::having() + */ + function add_having_expression($group, $snippet, $args = array()) { + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + + // Check for a group. + if (!isset($this->having[$group])) { + $this->set_where_group('AND', $group, 'having'); + } + + // Add the clause and the args. + $this->having[$group]['conditions'][] = array( + 'field' => $snippet, + 'value' => $args, + 'operator' => 'formula', + ); + } + + /** + * Add an ORDER BY clause to the query. + * + * @param $table + * The table this field is part of. If a formula, enter NULL. + * If you want to orderby random use "rand" as table and nothing else. + * @param $field + * The field or formula to sort on. If already a field, enter NULL + * and put in the alias. + * @param $order + * Either ASC or DESC. + * @param $alias + * The alias to add the field as. In SQL, all fields in the order by + * must also be in the SELECT portion. If an $alias isn't specified + * one will be generated for from the $field; however, if the + * $field is a formula, this alias will likely fail. + * @param $params + * Any params that should be passed through to the add_field. + */ + function add_orderby($table, $field = NULL, $order = 'ASC', $alias = '', $params = array()) { + // Only ensure the table if it's not the special random key. + // @todo: Maybe it would make sense to just add a add_orderby_rand or something similar. + if ($table && $table != 'rand') { + $this->ensure_table($table); + } + + // Only fill out this aliasing if there is a table; + // otherwise we assume it is a formula. + if (!$alias && $table) { + $as = $table . '_' . $field; + } + else { + $as = $alias; + } + + if ($field) { + $as = $this->add_field($table, $field, $as, $params); + } + + $this->orderby[] = array( + 'field' => $as, + 'direction' => strtoupper($order) + ); + + /** + * -- removing, this should be taken care of by field adding now. + * -- leaving commented because I am unsure. + // If grouping, all items in the order by must also be in the + // group by clause. Check $table to ensure that this is not a + // formula. + if ($this->groupby && $table) { + $this->add_groupby($as); + } + */ + } + + /** + * Add a simple GROUP BY clause to the query. The caller is responsible + * for ensuring that the fields are fully qualified and the table is properly + * added. + */ + function add_groupby($clause) { + // Only add it if it's not already in there. + if (!in_array($clause, $this->groupby)) { + $this->groupby[] = $clause; + } + } + + /** + * Returns the alias for the given field added to $table. + * + * @see views_plugin_query_default::add_field() + */ + function get_field_alias($table_alias, $field) { + return isset($this->field_aliases[$table_alias][$field]) ? $this->field_aliases[$table_alias][$field] : FALSE; + } + + /** + * Adds a query tag to the sql object. + * + * @see SelectQuery::addTag() + */ + function add_tag($tag) { + $this->tags[] = $tag; + } + + /** + * Generates a unique placeholder used in the db query. + */ + function placeholder($base = 'views') { + static $placeholders = array(); + if (!isset($placeholders[$base])) { + $placeholders[$base] = 0; + return ':' . $base; + } + else { + return ':' . $base . ++$placeholders[$base]; + } + } + + /** + * Construct the "WHERE" or "HAVING" part of the query. + * + * As views has to wrap the conditions from arguments with AND, a special + * group is wrapped around all conditions. This special group has the ID 0. + * There is other code in filters which makes sure that the group IDs are + * higher than zero. + * + * @param $where + * 'where' or 'having'. + */ + function build_condition($where = 'where') { + $has_condition = FALSE; + $has_arguments = FALSE; + $has_filter = FALSE; + + $main_group = db_and(); + $filter_group = $this->group_operator == 'OR' ? db_or() : db_and(); + + foreach ($this->$where as $group => $info) { + + if (!empty($info['conditions'])) { + $sub_group = $info['type'] == 'OR' ? db_or() : db_and(); + foreach ($info['conditions'] as $key => $clause) { + // DBTNG doesn't support to add the same subquery twice to the main + // query and the count query, so clone the subquery to have two instances + // of the same object. - http://drupal.org/node/1112854 + if (is_object($clause['value']) && $clause['value'] instanceof SelectQuery) { + $clause['value'] = clone $clause['value']; + } + if ($clause['operator'] == 'formula') { + $has_condition = TRUE; + $sub_group->where($clause['field'], $clause['value']); + } + else { + $has_condition = TRUE; + $sub_group->condition($clause['field'], $clause['value'], $clause['operator']); + } + } + + // Add the item to the filter group. + if ($group != 0) { + $has_filter = TRUE; + $filter_group->condition($sub_group); + } + else { + $has_arguments = TRUE; + $main_group->condition($sub_group); + } + } + } + + if ($has_filter) { + $main_group->condition($filter_group); + } + + if (!$has_arguments && $has_condition) { + return $filter_group; + } + if ($has_arguments && $has_condition) { + return $main_group; + } + } + + /** + * Returns a list of non-aggregates to be added to the "group by" clause. + * + * Non-aggregates are fields that have no aggregation function (count, sum, + * etc) applied. Since the SQL standard requires all fields to either have + * an aggregation function applied, or to be in the GROUP BY clause, Views + * gathers those fields and adds them to the GROUP BY clause. + * + * @return array + * An array of the fieldnames which are non-aggregates. + */ + function get_non_aggregates() { + $non_aggregates = array(); + foreach ($this->fields as $field) { + $string = ''; + if (!empty($field['table'])) { + $string .= $field['table'] . '.'; + } + $string .= $field['field']; + $fieldname = (!empty($field['alias']) ? $field['alias'] : $string); + + if (!empty($field['count'])) { + // Retained for compatibility. + $field['function'] = 'count'; + } + + if (!empty($field['function'])) { + $this->has_aggregate = TRUE; + } + // This is a formula, using no tables. + elseif (empty($field['table'])) { + $non_aggregates[] = $fieldname; + } + elseif (empty($field['aggregate'])) { + $non_aggregates[] = $fieldname; + } + + if ($this->get_count_optimized) { + // We only want the first field in this case. + break; + } + } + + return $non_aggregates; + } + + /** + * Adds fields to the query. + * + * @param Drupal\Core\Database\Query\SelectInterface $query + * The drupal query object. + */ + function compile_fields($query) { + foreach ($this->fields as $field) { + $string = ''; + if (!empty($field['table'])) { + $string .= $field['table'] . '.'; + } + $string .= $field['field']; + $fieldname = (!empty($field['alias']) ? $field['alias'] : $string); + + if (!empty($field['count'])) { + // Retained for compatibility. + $field['function'] = 'count'; + } + + if (!empty($field['function'])) { + $info = $this->get_aggregation_info(); + if (!empty($info[$field['function']]['method']) && is_callable(array($this, $info[$field['function']]['method']))) { + $string = $this::$info[$field['function']]['method']($field['function'], $string); + $placeholders = !empty($field['placeholders']) ? $field['placeholders'] : array(); + $query->addExpression($string, $fieldname, $placeholders); + } + + $this->has_aggregate = TRUE; + } + // This is a formula, using no tables. + elseif (empty($field['table'])) { + $placeholders = !empty($field['placeholders']) ? $field['placeholders'] : array(); + $query->addExpression($string, $fieldname, $placeholders); + } + elseif ($this->distinct && !in_array($fieldname, $this->groupby)) { + $query->addField(!empty($field['table']) ? $field['table'] : $this->base_table, $field['field'], $fieldname); + } + elseif (empty($field['aggregate'])) { + $query->addField(!empty($field['table']) ? $field['table'] : $this->base_table, $field['field'], $fieldname); + } + + if ($this->get_count_optimized) { + // We only want the first field in this case. + break; + } + } + } + + /** + * Generate a query and a countquery from all of the information supplied + * to the object. + * + * @param $get_count + * Provide a countquery if this is true, otherwise provide a normal query. + */ + public function query($get_count = FALSE) { + // Check query distinct value. + if (empty($this->no_distinct) && $this->distinct && !empty($this->fields)) { + $base_field_alias = $this->add_field($this->base_table, $this->base_field); + $this->add_groupby($base_field_alias); + $distinct = TRUE; + } + + /** + * An optimized count query includes just the base field instead of all the fields. + * Determine of this query qualifies by checking for a groupby or distinct. + */ + if ($get_count && !$this->groupby) { + foreach ($this->fields as $field) { + if (!empty($field['distinct']) || !empty($field['function'])) { + $this->get_count_optimized = FALSE; + break; + } + } + } + else { + $this->get_count_optimized = FALSE; + } + if (!isset($this->get_count_optimized)) { + $this->get_count_optimized = TRUE; + } + + $options = array(); + $target = 'default'; + $key = 'default'; + // Detect an external database and set the + if (isset($this->view->base_database)) { + $key = $this->view->base_database; + } + + // Set the slave target if the slave option is set + if (!empty($this->options['slave'])) { + $target = 'slave'; + } + + // Go ahead and build the query. + // db_select doesn't support to specify the key, so use getConnection directly. + $query = Database::getConnection($target, $key) + ->select($this->base_table, $this->base_table, $options) + ->addTag('views') + ->addTag('views_' . $this->view->storage->name); + + // Add the tags added to the view itself. + foreach ($this->tags as $tag) { + $query->addTag($tag); + } + + if (!empty($distinct)) { + $query->distinct(); + } + + $joins = $where = $having = $orderby = $groupby = ''; + $fields = $distinct = array(); + + // Add all the tables to the query via joins. We assume all LEFT joins. + foreach ($this->table_queue as $table) { + if (is_object($table['join'])) { + $table['join']->buildJoin($query, $table, $this); + } + } + + // Assemble the groupby clause, if any. + $this->has_aggregate = FALSE; + $non_aggregates = $this->get_non_aggregates(); + if (count($this->having)) { + $this->has_aggregate = TRUE; + } + $groupby = array(); + if ($this->has_aggregate && (!empty($this->groupby) || !empty($non_aggregates))) { + $groupby = array_unique(array_merge($this->groupby, $non_aggregates)); + } + + // Make sure each entity table has the base field added so that the + // entities can be loaded. + $entity_tables = $this->get_entity_tables(); + if ($entity_tables) { + $params = array(); + if ($groupby) { + // Handle grouping, by retrieving the minimum entity_id. + $params = array( + 'function' => 'min', + ); + } + + foreach ($entity_tables as $table_alias => $table) { + $info = entity_get_info($table['entity_type']); + $base_field = empty($table['revision']) ? $info['entity keys']['id'] : $info['entity keys']['revision']; + $this->add_field($table_alias, $base_field, '', $params); + } + } + + // Add all fields to the query. + $this->compile_fields($query); + + // Add groupby. + if ($groupby) { + foreach ($groupby as $field) { + $query->groupBy($field); + } + if (!empty($this->having) && $condition = $this->build_condition('having')) { + $query->havingCondition($condition); + } + } + + if (!$this->get_count_optimized) { + // we only add the orderby if we're not counting. + if ($this->orderby) { + foreach ($this->orderby as $order) { + if ($order['field'] == 'rand_') { + $query->orderRandom(); + } + else { + $query->orderBy($order['field'], $order['direction']); + } + } + } + } + + if (!empty($this->where) && $condition = $this->build_condition('where')) { + $query->condition($condition); + } + + // Add a query comment. + if (!empty($this->options['query_comment'])) { + $query->comment($this->options['query_comment']); + } + + // Add the query tags. + if (!empty($this->options['query_tags'])) { + foreach ($this->options['query_tags'] as $tag) { + $query->addTag($tag); + } + } + + // Add all query substitutions as metadata. + $query->addMetaData('views_substitutions', module_invoke_all('views_query_substitutions', $this)); + + return $query; + } + + /** + * Get the arguments attached to the WHERE and HAVING clauses of this query. + */ + function get_where_args() { + $args = array(); + foreach ($this->where as $group => $where) { + $args = array_merge($args, $where['args']); + } + foreach ($this->having as $group => $having) { + $args = array_merge($args, $having['args']); + } + return $args; + } + + /** + * Let modules modify the query just prior to finalizing it. + */ + function alter(ViewExecutable $view) { + foreach (module_implements('views_query_alter') as $module) { + $function = $module . '_views_query_alter'; + $function($view, $this); + } + } + + /** + * Builds the necessary info to execute the query. + */ + function build(ViewExecutable $view) { + // Make the query distinct if the option was set. + if (!empty($this->options['distinct'])) { + $this->set_distinct(TRUE); + } + + // Store the view in the object to be able to use it later. + $this->view = $view; + + $view->initPager(); + + // Let the pager modify the query to add limits. + $view->pager->query(); + + $view->build_info['query'] = $this->query(); + $view->build_info['count_query'] = $this->query(TRUE); + } + + /** + * Executes the query and fills the associated view object with according + * values. + * + * Values to set: $view->result, $view->total_rows, $view->execute_time, + * $view->current_page. + */ + function execute(ViewExecutable $view) { + $external = FALSE; // Whether this query will run against an external database. + $query = $view->build_info['query']; + $count_query = $view->build_info['count_query']; + + $query->addMetaData('view', $view); + $count_query->addMetaData('view', $view); + + if (empty($this->options['disable_sql_rewrite'])) { + $base_table_data = views_fetch_data($this->base_table); + if (isset($base_table_data['table']['base']['access query tag'])) { + $access_tag = $base_table_data['table']['base']['access query tag']; + $query->addTag($access_tag); + $count_query->addTag($access_tag); + } + } + + $items = array(); + if ($query) { + $additional_arguments = module_invoke_all('views_query_substitutions', $view); + + // Count queries must be run through the preExecute() method. + // If not, then hook_query_node_access_alter() may munge the count by + // adding a distinct against an empty query string + // (e.g. COUNT DISTINCT(1) ...) and no pager will return. + // See pager.inc > PagerDefault::execute() + // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7 + // See http://drupal.org/node/1046170. + $count_query->preExecute(); + + // Build the count query. + $count_query = $count_query->countQuery(); + + // Add additional arguments as a fake condition. + // XXX: this doesn't work... because PDO mandates that all bound arguments + // are used on the query. TODO: Find a better way to do this. + if (!empty($additional_arguments)) { + // $query->where('1 = 1', $additional_arguments); + // $count_query->where('1 = 1', $additional_arguments); + } + + $start = microtime(TRUE); + + try { + if ($view->pager->use_count_query() || !empty($view->get_total_rows)) { + $view->pager->execute_count_query($count_query); + } + + // Let the pager modify the query to add limits. + $view->pager->pre_execute($query); + + if (!empty($this->limit) || !empty($this->offset)) { + // We can't have an offset without a limit, so provide a very large limit instead. + $limit = intval(!empty($this->limit) ? $this->limit : 999999); + $offset = intval(!empty($this->offset) ? $this->offset : 0); + $query->range($offset, $limit); + } + + $result = $query->execute(); + + $view->result = array(); + foreach ($result as $item) { + $view->result[] = $item; + } + + $view->pager->postExecute($view->result); + if ($view->pager->use_count_query() || !empty($view->get_total_rows)) { + $view->total_rows = $view->pager->get_total_items(); + } + + // Load all entities contained in the results. + $this->load_entities($view->result); + } + catch (DatabaseExceptionWrapper $e) { + $view->result = array(); + if (!empty($view->live_preview)) { + drupal_set_message($e->getMessage(), 'error'); + } + else { + throw new DatabaseExceptionWrapper(format_string('Exception in @human_name[@view_name]: @message', array('@human_name' => $view->storage->getHumanName(), '@view_name' => $view->storage->name, '@message' => $e->getMessage()))); + } + } + + } + else { + $start = microtime(TRUE); + } + $view->execute_time = microtime(TRUE) - $start; + } + + /** + * Returns an array of all tables from the query that map to an entity type. + * + * Includes the base table and all relationships, if eligible. + * Available keys for each table: + * - base: The actual base table (i.e. "user" for an author relationship). + * - relationship_id: The id of the relationship, or "none". + * - entity_type: The entity type matching the base table. + * - revision: A boolean that specifies whether the table is a base table or + * a revision table of the entity type. + * + * @return array + * An array of table information, keyed by table alias. + */ + function get_entity_tables() { + // Start with the base table. + $entity_tables = array(); + $base_table_data = views_fetch_data($this->base_table); + if (isset($base_table_data['table']['entity type'])) { + $entity_tables[$this->base_table] = array( + 'base' => $this->base_table, + 'relationship_id' => 'none', + 'entity_type' => $base_table_data['table']['entity type'], + 'revision' => FALSE, + ); + } + // Include all relationships. + foreach ($this->view->relationship as $relationship_id => $relationship) { + $table_data = views_fetch_data($relationship->definition['base']); + if (isset($table_data['table']['entity type'])) { + $entity_tables[$relationship->alias] = array( + 'base' => $relationship->definition['base'], + 'relationship_id' => $relationship_id, + 'entity_type' => $table_data['table']['entity type'], + 'revision' => FALSE, + ); + } + } + + // Determine which of the tables are revision tables. + foreach ($entity_tables as $table_alias => $table) { + $info = entity_get_info($table['entity_type']); + if (isset($info['revision table']) && $info['revision table'] == $table['base']) { + $entity_tables[$table_alias]['revision'] = TRUE; + } + } + + return $entity_tables; + } + + /** + * Loads all entities contained in the passed-in $results. + *. + * If the entity belongs to the base table, then it gets stored in + * $result->_entity. Otherwise, it gets stored in + * $result->_relationship_entities[$relationship_id]; + */ + function load_entities(&$results) { + $entity_tables = $this->get_entity_tables(); + // No entity tables found, nothing else to do here. + if (empty($entity_tables)) { + return; + } + + // Initialize the entity placeholders in $results. + foreach ($results as $index => $result) { + $results[$index]->_entity = FALSE; + $results[$index]->_relationship_entities = array(); + } + + // Assemble a list of entities to load. + $ids_by_table = array(); + foreach ($entity_tables as $table_alias => $table) { + $entity_type = $table['entity_type']; + $info = entity_get_info($entity_type); + $id_key = empty($table['revision']) ? $info['entity keys']['id'] : $info['entity keys']['revision']; + $id_alias = $this->get_field_alias($table_alias, $id_key); + + foreach ($results as $index => $result) { + // Store the entity id if it was found. + if (!empty($result->$id_alias)) { + $ids_by_table[$table_alias][$index] = $result->$id_alias; + } + } + } + + // Load all entities and assign them to the correct result row. + foreach ($ids_by_table as $table_alias => $ids) { + $table = $entity_tables[$table_alias]; + $entity_type = $table['entity_type']; + $relationship_id = $table['relationship_id']; + + // Drupal core currently has no way to load multiple revisions. Sad. + if ($table['revision']) { + $entities = array(); + foreach ($ids as $index => $revision_id) { + $entity = entity_revision_load($entity_type, $revision_id); + if ($entity) { + $entities[$revision_id] = $entity; + } + } + } + else { + $entities = entity_load_multiple($entity_type, $ids); + } + + foreach ($ids as $index => $id) { + $entity = isset($entities[$id]) ? $entities[$id] : FALSE; + + if ($relationship_id == 'none') { + $results[$index]->_entity = $entity; + } + else { + $results[$index]->_relationship_entities[$relationship_id] = $entity; + } + } + } + } + + function add_signature(ViewExecutable $view) { + $view->query->add_field(NULL, "'" . $view->storage->name . ':' . $view->current_display . "'", 'view_name'); + } + + function get_aggregation_info() { + // @todo -- need a way to get database specific and customized aggregation + // functions into here. + return array( + 'group' => array( + 'title' => t('Group results together'), + 'is aggregate' => FALSE, + ), + 'count' => array( + 'title' => t('Count'), + 'method' => 'aggregation_method_simple', + 'handler' => array( + 'argument' => 'groupby_numeric', + 'field' => 'numeric', + 'filter' => 'groupby_numeric', + 'sort' => 'groupby_numeric', + ), + ), + 'count_distinct' => array( + 'title' => t('Count DISTINCT'), + 'method' => 'aggregation_method_distinct', + 'handler' => array( + 'argument' => 'groupby_numeric', + 'field' => 'numeric', + 'filter' => 'groupby_numeric', + 'sort' => 'groupby_numeric', + ), + ), + 'sum' => array( + 'title' => t('Sum'), + 'method' => 'aggregation_method_simple', + 'handler' => array( + 'argument' => 'groupby_numeric', + 'field' => 'numeric', + 'filter' => 'groupby_numeric', + 'sort' => 'groupby_numeric', + ), + ), + 'avg' => array( + 'title' => t('Average'), + 'method' => 'aggregation_method_simple', + 'handler' => array( + 'argument' => 'groupby_numeric', + 'field' => 'numeric', + 'filter' => 'groupby_numeric', + 'sort' => 'groupby_numeric', + ), + ), + 'min' => array( + 'title' => t('Minimum'), + 'method' => 'aggregation_method_simple', + 'handler' => array( + 'argument' => 'groupby_numeric', + 'field' => 'numeric', + 'filter' => 'groupby_numeric', + 'sort' => 'groupby_numeric', + ), + ), + 'max' => array( + 'title' => t('Maximum'), + 'method' => 'aggregation_method_simple', + 'handler' => array( + 'argument' => 'groupby_numeric', + 'field' => 'numeric', + 'filter' => 'groupby_numeric', + 'sort' => 'groupby_numeric', + ), + ), + 'stddev_pop' => array( + 'title' => t('Standard derivation'), + 'method' => 'aggregation_method_simple', + 'handler' => array( + 'argument' => 'groupby_numeric', + 'field' => 'numeric', + 'filter' => 'groupby_numeric', + 'sort' => 'groupby_numeric', + ), + ) + ); + } + + function aggregation_method_simple($group_type, $field) { + return strtoupper($group_type) . '(' . $field . ')'; + } + + function aggregation_method_distinct($group_type, $field) { + $group_type = str_replace('_distinct', '', $group_type); + return strtoupper($group_type) . '(DISTINCT ' . $field . ')'; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/relationship/Broken.php b/core/modules/views/lib/Drupal/views/Plugin/views/relationship/Broken.php new file mode 100644 index 0000000..e5831b6 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/relationship/Broken.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\relationship\Broken. + */ + +namespace Drupal\views\Plugin\views\relationship; + +use Drupal\Core\Annotation\Plugin; + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_relationship_handlers + * + * @Plugin( + * id = "broken" + * ) + */ +class Broken extends RelationshipPluginBase { + + public function adminLabel($short = FALSE) { + return t('Broken/missing handler'); + } + + public function defineOptions() { return array(); } + public function ensureMyTable() { /* No table to ensure! */ } + public function query() { /* No query to run */ } + public function buildOptionsForm(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '<div class="form-item description">' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '</div>', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + public function broken() { return TRUE; } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/relationship/GroupwiseMax.php b/core/modules/views/lib/Drupal/views/Plugin/views/relationship/GroupwiseMax.php new file mode 100644 index 0000000..ef8ffaf --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/relationship/GroupwiseMax.php @@ -0,0 +1,388 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\relationship\GroupwiseMax. + */ + +namespace Drupal\views\Plugin\views\relationship; + +use Drupal\Core\Database\Query\AlterableInterface; +use Drupal\Core\Annotation\Plugin; + +/** + * Relationship handler that allows a groupwise maximum of the linked in table. + * For a definition, see: + * http://dev.mysql.com/doc/refman/5.0/en/example-maximum-column-group-row.html + * In lay terms, instead of joining to get all matching records in the linked + * table, we get only one record, a 'representative record' picked according + * to a given criteria. + * + * Example: + * Suppose we have a term view that gives us the terms: Horse, Cat, Aardvark. + * We wish to show for each term the most recent node of that term. + * What we want is some kind of relationship from term to node. + * But a regular relationship will give us all the nodes for each term, + * giving the view multiple rows per term. What we want is just one + * representative node per term, the node that is the 'best' in some way: + * eg, the most recent, the most commented on, the first in alphabetical order. + * + * This handler gives us that kind of relationship from term to node. + * The method of choosing the 'best' implemented with a sort + * that the user selects in the relationship settings. + * + * So if we want our term view to show the most commented node for each term, + * add the relationship and in its options, pick the 'Comment count' sort. + * + * Relationship definition + * - 'outer field': The outer field to substitute into the correlated subquery. + * This must be the full field name, not the alias. + * Eg: 'term_data.tid'. + * - 'argument table', + * 'argument field': These options define a views argument that the subquery + * must add to itself to filter by the main view. + * Example: the main view shows terms, this handler is being used to get to + * the nodes base table. Your argument must be 'term_node', 'tid', as this + * is the argument that should be added to a node view to filter on terms. + * + * A note on performance: + * This relationship uses a correlated subquery, which is expensive. + * Subsequent versions of this handler could also implement the alternative way + * of doing this, with a join -- though this looks like it could be pretty messy + * to implement. This is also an expensive method, so providing both methods and + * allowing the user to choose which one works fastest for their data might be + * the best way. + * If your use of this relationship handler is likely to result in large + * data sets, you might want to consider storing statistics in a separate table, + * in the same way as node_comment_statistics. + * + * @ingroup views_relationship_handlers + * + * @Plugin( + * id = "groupwise_max" + * ) + */ +class GroupwiseMax extends RelationshipPluginBase { + + /** + * Defines default values for options. + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['subquery_sort'] = array('default' => NULL); + // Descending more useful. + $options['subquery_order'] = array('default' => 'DESC'); + $options['subquery_regenerate'] = array('default' => FALSE, 'bool' => TRUE); + $options['subquery_view'] = array('default' => FALSE); + $options['subquery_namespace'] = array('default' => FALSE); + + return $options; + } + + /** + * Extends the relationship's basic options, allowing the user to pick + * a sort and an order for it. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + // Get the sorts that apply to our base. + $sorts = views_fetch_fields($this->definition['base'], 'sort'); + foreach ($sorts as $sort_id => $sort) { + $sort_options[$sort_id] = "$sort[group]: $sort[title]"; + } + $base_table_data = views_fetch_data($this->definition['base']); + + $form['subquery_sort'] = array( + '#type' => 'select', + '#title' => t('Representative sort criteria'), + // Provide the base field as sane default sort option. + '#default_value' => !empty($this->options['subquery_sort']) ? $this->options['subquery_sort'] : $this->definition['base'] . '.' . $base_table_data['table']['base']['field'], + '#options' => $sort_options, + '#description' => t("The sort criteria is applied to the data brought in by the relationship to determine how a representative item is obtained for each row. For example, to show the most recent node for each user, pick 'Content: Updated date'."), + ); + + $form['subquery_order'] = array( + '#type' => 'radios', + '#title' => t('Representative sort order'), + '#description' => t("The ordering to use for the sort criteria selected above."), + '#options' => array('ASC' => t('Ascending'), 'DESC' => t('Descending')), + '#default_value' => $this->options['subquery_order'], + ); + + $form['subquery_namespace'] = array( + '#type' => 'textfield', + '#title' => t('Subquery namespace'), + '#description' => t('Advanced. Enter a namespace for the subquery used by this relationship.'), + '#default_value' => $this->options['subquery_namespace'], + ); + + + // WIP: This stuff doens't work yet: namespacing issues. + // A list of suitable views to pick one as the subview. + $views = array('' => '<none>'); + $all_views = views_get_all_views(); + foreach ($all_views as $view) { + // Only get views that are suitable: + // - base must the base that our relationship joins towards + // - must have fields. + if ($view->base_table == $this->definition['base'] && !empty($view->display['default']['display_options']['fields'])) { + // TODO: check the field is the correct sort? + // or let users hang themselves at this stage and check later? + if ($view->type == 'Default') { + $views[t('Default Views')][$view->storage->name] = $view->storage->name; + } + else { + $views[t('Existing Views')][$view->storage->name] = $view->storage->name; + } + } + } + + $form['subquery_view'] = array( + '#type' => 'select', + '#title' => t('Representative view'), + '#default_value' => $this->options['subquery_view'], + '#options' => $views, + '#description' => t('Advanced. Use another view to generate the relationship subquery. This allows you to use filtering and more than one sort. If you pick a view here, the sort options above are ignored. Your view must have the ID of its base as its only field, and should have some kind of sorting.'), + ); + + $form['subquery_regenerate'] = array( + '#type' => 'checkbox', + '#title' => t('Generate subquery each time view is run.'), + '#default_value' => $this->options['subquery_regenerate'], + '#description' => t('Will re-generate the subquery for this relationship every time the view is run, instead of only when these options are saved. Use for testing if you are making changes elsewhere. WARNING: seriously impairs performance.'), + ); + } + + /** + * Helper function to create a pseudo view. + * + * We use this to obtain our subquery SQL. + */ + function get_temporary_view() { + $view = new View(array(), 'view'); + $view->vid = 'new'; // @todo: what's this? + $view->base_table = $this->definition['base']; + $view->addDisplay('default'); + return $view; + } + + /** + * When the form is submitted, take sure to clear the subquery string cache. + */ + public function submitOptionsForm(&$form, &$form_state) { + $cid = 'views_relationship_groupwise_max:' . $this->view->storage->name . ':' . $this->view->current_display . ':' . $this->options['id']; + cache('views_data')->delete($cid); + } + + /** + * Generate a subquery given the user options, as set in the options. + * These are passed in rather than picked up from the object because we + * generate the subquery when the options are saved, rather than when the view + * is run. This saves considerable time. + * + * @param $options + * An array of options: + * - subquery_sort: the id of a views sort. + * - subquery_order: either ASC or DESC. + * @return + * The subquery SQL string, ready for use in the main query. + */ + function left_query($options) { + // Either load another view, or create one on the fly. + if ($options['subquery_view']) { + $temp_view = views_get_view($options['subquery_view']); + // Remove all fields from default display + unset($temp_view->display['default']['display_options']['fields']); + } + else { + // Create a new view object on the fly, which we use to generate a query + // object and then get the SQL we need for the subquery. + $temp_view = $this->get_temporary_view(); + + // Add the sort from the options to the default display. + // This is broken, in that the sort order field also gets added as a + // select field. See http://drupal.org/node/844910. + // We work around this further down. + $sort = $options['subquery_sort']; + list($sort_table, $sort_field) = explode('.', $sort); + $sort_options = array('order' => $options['subquery_order']); + $temp_view->addItem('default', 'sort', $sort_table, $sort_field, $sort_options); + } + + // Get the namespace string. + $temp_view->namespace = (!empty($options['subquery_namespace'])) ? '_'. $options['subquery_namespace'] : '_INNER'; + $this->subquery_namespace = (!empty($options['subquery_namespace'])) ? '_'. $options['subquery_namespace'] : 'INNER'; + + // The value we add here does nothing, but doing this adds the right tables + // and puts in a WHERE clause with a placeholder we can grab later. + $temp_view->args[] = '**CORRELATED**'; + + // Add the base table ID field. + $views_data = views_fetch_data($this->definition['base']); + $base_field = $views_data['table']['base']['field']; + $temp_view->addItem('default', 'field', $this->definition['base'], $this->definition['field']); + + // Add the correct argument for our relationship's base + // ie the 'how to get back to base' argument. + // The relationship definition tells us which one to use. + $temp_view->addItem( + 'default', + 'argument', + $this->definition['argument table'], // eg 'term_node', + $this->definition['argument field'] // eg 'tid' + ); + + // Build the view. The creates the query object and produces the query + // string but does not run any queries. + $temp_view->build(); + + // Now take the SelectQuery object the View has built and massage it + // somewhat so we can get the SQL query from it. + $subquery = $temp_view->build_info['query']; + + // Workaround until http://drupal.org/node/844910 is fixed: + // Remove all fields from the SELECT except the base id. + $fields =& $subquery->getFields(); + foreach (array_keys($fields) as $field_name) { + // The base id for this subquery is stored in our definition. + if ($field_name != $this->definition['field']) { + unset($fields[$field_name]); + } + } + + // Make every alias in the subquery safe within the outer query by + // appending a namespace to it, '_inner' by default. + $tables =& $subquery->getTables(); + foreach (array_keys($tables) as $table_name) { + $tables[$table_name]['alias'] .= $this->subquery_namespace; + // Namespace the join on every table. + if (isset($tables[$table_name]['condition'])) { + $tables[$table_name]['condition'] = $this->condition_namespace($tables[$table_name]['condition']); + } + } + // Namespace fields. + foreach (array_keys($fields) as $field_name) { + $fields[$field_name]['table'] .= $this->subquery_namespace; + $fields[$field_name]['alias'] .= $this->subquery_namespace; + } + // Namespace conditions. + $where =& $subquery->conditions(); + $this->alter_subquery_condition($subquery, $where); + // Not sure why, but our sort order clause doesn't have a table. + // TODO: the call to add_item() above to add the sort handler is probably + // wrong -- needs attention from someone who understands it. + // In the meantime, this works, but with a leap of faith... + $orders =& $subquery->getOrderBy(); + foreach ($orders as $order_key => $order) { + // But if we're using a whole view, we don't know what we have! + if ($options['subquery_view']) { + list($sort_table, $sort_field) = explode('.', $order_key); + } + $orders[$sort_table . $this->subquery_namespace . '.' . $sort_field] = $order; + unset($orders[$order_key]); + } + + // The query we get doesn't include the LIMIT, so add it here. + $subquery->range(0, 1); + + // Extract the SQL the temporary view built. + $subquery_sql = $subquery->__toString(); + + // Replace the placeholder with the outer, correlated field. + // Eg, change the placeholder ':users_uid' into the outer field 'users.uid'. + // We have to work directly with the SQL, because putting a name of a field + // into a SelectQuery that it does not recognize (because it's outer) just + // makes it treat it as a string. + $outer_placeholder = ':' . str_replace('.', '_', $this->definition['outer field']); + $subquery_sql = str_replace($outer_placeholder, $this->definition['outer field'], $subquery_sql); + + return $subquery_sql; + } + + /** + * Recursive helper to add a namespace to conditions. + * + * Similar to _views_query_tag_alter_condition(). + * + * (Though why is the condition we get in a simple query 3 levels deep???) + */ + function alter_subquery_condition(AlterableInterface $query, &$conditions) { + foreach ($conditions as $condition_id => &$condition) { + // Skip the #conjunction element. + if (is_numeric($condition_id)) { + if (is_string($condition['field'])) { + $condition['field'] = $this->condition_namespace($condition['field']); + } + elseif (is_object($condition['field'])) { + $sub_conditions =& $condition['field']->conditions(); + $this->alter_subquery_condition($query, $sub_conditions); + } + } + } + } + + /** + * Helper function to namespace query pieces. + * + * Turns 'foo.bar' into 'foo_NAMESPACE.bar'. + */ + function condition_namespace($string) { + return str_replace('.', $this->subquery_namespace . '.', $string); + } + + /** + * Called to implement a relationship in a query. + * This is mostly a copy of our parent's query() except for this bit with + * the join class. + */ + public function query() { + // Figure out what base table this relationship brings to the party. + $table_data = views_fetch_data($this->definition['base']); + $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field']; + + $this->ensureMyTable(); + + $def = $this->definition; + $def['table'] = $this->definition['base']; + $def['field'] = $base_field; + $def['left_table'] = $this->tableAlias; + $def['left_field'] = $this->field; + $def['adjusted'] = TRUE; + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + if ($this->options['subquery_regenerate']) { + // For testing only, regenerate the subquery each time. + $def['left_query'] = $this->left_query($this->options); + } + else { + // Get the stored subquery SQL string. + $cid = 'views_relationship_groupwise_max:' . $this->view->storage->name . ':' . $this->view->current_display . ':' . $this->options['id']; + $cache = cache('views_data')->get($cid); + if (isset($cache->data)) { + $def['left_query'] = $cache->data; + } + else { + $def['left_query'] = $this->left_query($this->options); + cache('views_data')->set($cid, $def['left_query']); + } + } + + if (!empty($def['join_id'])) { + $id = $def['join_id']; + } + else { + $id = 'subquery'; + } + $join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $def); + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, $this->definition['base'], $this->relationship); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/relationship/RelationshipPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/relationship/RelationshipPluginBase.php new file mode 100644 index 0000000..f1641aa --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/relationship/RelationshipPluginBase.php @@ -0,0 +1,169 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\relationship\RelationshipPluginBase. + */ + +namespace Drupal\views\Plugin\views\relationship; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\HandlerBase; +use Drupal\views\Join; +use Drupal\Core\Annotation\Plugin; + +/** + * @defgroup views_relationship_handlers Views relationship handlers + * @{ + * Handlers to tell Views how to create alternate relationships. + */ + +/** + * Simple relationship handler that allows a new version of the primary table + * to be linked in. + * + * The base relationship handler can only handle a single join. Some relationships + * are more complex and might require chains of joins; for those, you must + * utilize a custom relationship handler. + * + * Definition items: + * - base: The new base table this relationship will be adding. This does not + * have to be a declared base table, but if there are no tables that + * utilize this base table, it won't be very effective. + * - base field: The field to use in the relationship; if left out this will be + * assumed to be the primary field. + * - relationship table: The actual table this relationship operates against. + * This is analogous to using a 'table' override. + * - relationship field: The actual field this relationship operates against. + * This is analogous to using a 'real field' override. + * - label: The default label to provide for this relationship, which is + * shown in parentheses next to any field/sort/filter/argument that uses + * the relationship. + * + * @ingroup views_relationship_handlers + */ +abstract class RelationshipPluginBase extends HandlerBase { + + /** + * Init handler to let relationships live on tables other than + * the table they operate on. + */ + public function init(ViewExecutable $view, &$options) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + parent::init($view, $options); + if (isset($this->definition['relationship table'])) { + $this->table = $this->definition['relationship table']; + } + if (isset($this->definition['relationship field'])) { + // Set both realField and field so custom handler can rely on the old + // field value. + $this->realField = $this->field = $this->definition['relationship field']; + } + } + + /** + * Get this field's label. + */ + function label() { + if (!isset($this->options['label'])) { + return $this->adminLabel(); + } + return $this->options['label']; + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + + // Relationships definitions should define a default label, but if they aren't get another default value. + if (!empty($this->definition['label'])) { + $label = $this->definition['label']; + } + else { + $label = !empty($this->definition['field']) ? $this->definition['field'] : $this->definition['base field']; + } + + $options['label'] = array('default' => $label, 'translatable' => TRUE); + $options['required'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Default options form that provides the label widget that all fields + * should have. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Identifier'), + '#default_value' => isset($this->options['label']) ? $this->options['label'] : '', + '#description' => t('Edit the administrative label displayed when referencing this relationship from filters, etc.'), + '#required' => TRUE, + ); + + $form['required'] = array( + '#type' => 'checkbox', + '#title' => t('Require this relationship'), + '#description' => t('Enable to hide items that do not contain this relationship'), + '#default_value' => !empty($this->options['required']), + ); + } + + /** + * Called to implement a relationship in a query. + */ + public function query() { + // Figure out what base table this relationship brings to the party. + $table_data = views_fetch_data($this->definition['base']); + $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field']; + + $this->ensureMyTable(); + + $def = $this->definition; + $def['table'] = $this->definition['base']; + $def['field'] = $base_field; + $def['left_table'] = $this->tableAlias; + $def['left_field'] = $this->realField; + $def['adjusted'] = TRUE; + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + if (!empty($this->definition['extra'])) { + $def['extra'] = $this->definition['extra']; + } + + if (!empty($def['join_id'])) { + $id = $def['join_id']; + } + else { + $id = 'standard'; + } + $join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $def); + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, $this->definition['base'], $this->relationship); + + // Add access tags if the base table provide it. + if (empty($this->query->options['disable_sql_rewrite']) && isset($table_data['table']['base']['access query tag'])) { + $access_tag = $table_data['table']['base']['access query tag']; + $this->query->add_tag($access_tag); + } + } + + /** + * You can't groupby a relationship. + */ + public function usesGroupBy() { + return FALSE; + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/relationship/Standard.php b/core/modules/views/lib/Drupal/views/Plugin/views/relationship/Standard.php new file mode 100644 index 0000000..592c6f2 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/relationship/Standard.php @@ -0,0 +1,23 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\relationship\Standard. + */ + +namespace Drupal\views\Plugin\views\relationship; + +use Drupal\Core\Annotation\Plugin; + +/** + * Default implementation of the base relationship plugin. + * + * @ingroup views_relationship_handlers + * + * @Plugin( + * id = "standard" + * ) + */ +class Standard extends RelationshipPluginBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/row/Fields.php b/core/modules/views/lib/Drupal/views/Plugin/views/row/Fields.php new file mode 100644 index 0000000..842162b --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/row/Fields.php @@ -0,0 +1,109 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\row\Fields. + */ + +namespace Drupal\views\Plugin\views\row; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The basic 'fields' row plugin + * + * This displays fields one after another, giving options for inline + * or not. + * + * @ingroup views_row_plugins + * + * @Plugin( + * id = "fields", + * title = @Translation("Fields"), + * help = @Translation("Displays the fields with an optional template."), + * theme = "views_view_fields", + * type = "normal" + * ) + */ +class Fields extends RowPluginBase { + + /** + * Does the row plugin support to add fields to it's output. + * + * @var bool + */ + protected $usesFields = TRUE; + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['inline'] = array('default' => array()); + $options['separator'] = array('default' => ''); + $options['hide_empty'] = array('default' => FALSE, 'bool' => TRUE); + $options['default_field_elements'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + /** + * Provide a form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $options = $this->displayHandler->getFieldLabels(); + + if (empty($this->options['inline'])) { + $this->options['inline'] = array(); + } + + $form['default_field_elements'] = array( + '#type' => 'checkbox', + '#title' => t('Provide default field wrapper elements'), + '#default_value' => $this->options['default_field_elements'], + '#description' => t('If not checked, fields that are not configured to customize their HTML elements will get no wrappers at all for their field, label and field + label wrappers. You can use this to quickly reduce the amount of markup the view provides by default, at the cost of making it more difficult to apply CSS.'), + ); + + $form['inline'] = array( + '#type' => 'checkboxes', + '#title' => t('Inline fields'), + '#options' => $options, + '#default_value' => $this->options['inline'], + '#description' => t('Inline fields will be displayed next to each other rather than one after another. Note that some fields will ignore this if they are block elements, particularly body fields and other formatted HTML.'), + '#states' => array( + 'visible' => array( + ':input[name="row_options[default_field_elements]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['separator'] = array( + '#title' => t('Separator'), + '#type' => 'textfield', + '#size' => 10, + '#default_value' => isset($this->options['separator']) ? $this->options['separator'] : '', + '#description' => t('The separator may be placed between inline fields to keep them from squishing up next to each other. You can use HTML in this field.'), + '#states' => array( + 'visible' => array( + ':input[name="row_options[default_field_elements]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['hide_empty'] = array( + '#type' => 'checkbox', + '#title' => t('Hide empty fields'), + '#default_value' => $this->options['hide_empty'], + '#description' => t('Do not display fields, labels or markup for fields that are empty.'), + ); + + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitOptionsForm(&$form, &$form_state) { + $form_state['values']['row_options']['inline'] = array_filter($form_state['values']['row_options']['inline']); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/row/RowPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/row/RowPluginBase.php new file mode 100644 index 0000000..2e4fe83 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/row/RowPluginBase.php @@ -0,0 +1,177 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\row\RowPluginBase. + */ + +namespace Drupal\views\Plugin\views\row; + +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\ViewExecutable; + +/** + * @defgroup views_row_plugins Views row plugins + * @{ + * Row plugins control how Views outputs an individual record. + * + * They are tightly coupled to style plugins, in that a style plugin is what + * calls the row plugin. + * + * @see hook_views_plugins() + */ + +/** + * Default plugin to view a single row of a table. This is really just a wrapper around + * a theme function. + */ +abstract class RowPluginBase extends PluginBase { + + /** + * Overrides Drupal\views\Plugin\Plugin::$usesOptions. + */ + protected $usesOptions = TRUE; + + /** + * Does the row plugin support to add fields to it's output. + * + * @var bool + */ + protected $usesFields = FALSE; + + /** + * Initialize the row plugin. + */ + public function init(ViewExecutable $view, &$display, $options = NULL) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->displayHandler = &$display; + + // Overlay incoming options on top of defaults + $this->unpackOptions($this->options, $options); + } + + /** + * Returns the usesFields property. + * + * @return bool + */ + function usesFields() { + return $this->usesFields; + } + + + protected function defineOptions() { + $options = parent::defineOptions(); + if (isset($this->base_table)) { + $options['relationship'] = array('default' => 'none'); + } + + return $options; + } + + /** + * Provide a form for setting options. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + if (isset($this->base_table)) { + $view = &$form_state['view']; + + // A whole bunch of code to figure out what relationships are valid for + // this item. + $relationships = $view->display_handler->getOption('relationships'); + $relationship_options = array(); + + foreach ($relationships as $relationship) { + $relationship_handler = views_get_handler($relationship['table'], $relationship['field'], 'relationship'); + + // If this relationship is valid for this type, add it to the list. + $data = views_fetch_data($relationship['table']); + $base = $data[$relationship['field']]['relationship']['base']; + if ($base == $this->base_table) { + $relationship_handler->init($view, $relationship); + $relationship_options[$relationship['id']] = $relationship_handler->label(); + } + } + + if (!empty($relationship_options)) { + $relationship_options = array_merge(array('none' => t('Do not use a relationship')), $relationship_options); + $rel = empty($this->options['relationship']) ? 'none' : $this->options['relationship']; + if (empty($relationship_options[$rel])) { + // Pick the first relationship. + $rel = key($relationship_options); + } + + $form['relationship'] = array( + '#type' => 'select', + '#title' => t('Relationship'), + '#options' => $relationship_options, + '#default_value' => $rel, + ); + } + else { + $form['relationship'] = array( + '#type' => 'value', + '#value' => 'none', + ); + } + } + } + + /** + * Validate the options form. + */ + public function validateOptionsForm(&$form, &$form_state) { } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + public function submitOptionsForm(&$form, &$form_state) { } + + public function query() { + if (isset($this->base_table)) { + if (isset($this->options['relationship']) && isset($this->view->relationship[$this->options['relationship']])) { + $relationship = $this->view->relationship[$this->options['relationship']]; + $this->field_alias = $this->view->query->add_field($relationship->alias, $this->base_field); + } + else { + $this->field_alias = $this->view->query->add_field($this->base_table, $this->base_field); + } + } + } + + /** + * Allow the style to do stuff before each row is rendered. + * + * @param $result + * The full array of results from the query. + */ + function pre_render($result) { } + + /** + * Render a row object. This usually passes through to a theme template + * of some form, but not always. + * + * @param stdClass $row + * A single row of the query result, so an element of $view->result. + * + * @return string + * The rendered output of a single row, used by the style plugin. + */ + function render($row) { + return theme($this->themeFunctions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $row, + 'field_alias' => isset($this->field_alias) ? $this->field_alias : '', + )); + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/row/RssFields.php b/core/modules/views/lib/Drupal/views/Plugin/views/row/RssFields.php new file mode 100644 index 0000000..4a7a1e4 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/row/RssFields.php @@ -0,0 +1,202 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\row\RssFields. + */ + +namespace Drupal\views\Plugin\views\row; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Renders an RSS item based on fields. + * + * @Plugin( + * id = "rss_fields", + * title = @Translation("Fields"), + * help = @Translation("Display fields as RSS items."), + * theme = "views_view_row_rss", + * type = "feed" + * ) + */ +class RssFields extends RowPluginBase { + + /** + * Does the row plugin support to add fields to it's output. + * + * @var bool + */ + protected $usesFields = TRUE; + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['title_field'] = array('default' => ''); + $options['link_field'] = array('default' => ''); + $options['description_field'] = array('default' => ''); + $options['creator_field'] = array('default' => ''); + $options['date_field'] = array('default' => ''); + $options['guid_field_options']['guid_field'] = array('default' => ''); + $options['guid_field_options']['guid_field_is_permalink'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $initial_labels = array('' => t('- None -')); + $view_fields_labels = $this->displayHandler->getFieldLabels(); + $view_fields_labels = array_merge($initial_labels, $view_fields_labels); + + $form['title_field'] = array( + '#type' => 'select', + '#title' => t('Title field'), + '#description' => t('The field that is going to be used as the RSS item title for each row.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['title_field'], + '#required' => TRUE, + ); + $form['link_field'] = array( + '#type' => 'select', + '#title' => t('Link field'), + '#description' => t('The field that is going to be used as the RSS item link for each row. This must be a drupal relative path.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['link_field'], + '#required' => TRUE, + ); + $form['description_field'] = array( + '#type' => 'select', + '#title' => t('Description field'), + '#description' => t('The field that is going to be used as the RSS item description for each row.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['description_field'], + '#required' => TRUE, + ); + $form['creator_field'] = array( + '#type' => 'select', + '#title' => t('Creator field'), + '#description' => t('The field that is going to be used as the RSS item creator for each row.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['creator_field'], + '#required' => TRUE, + ); + $form['date_field'] = array( + '#type' => 'select', + '#title' => t('Publication date field'), + '#description' => t('The field that is going to be used as the RSS item pubDate for each row. It needs to be in RFC 2822 format.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['date_field'], + '#required' => TRUE, + ); + $form['guid_field_options'] = array( + '#type' => 'fieldset', + '#title' => t('GUID settings'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); + $form['guid_field_options']['guid_field'] = array( + '#type' => 'select', + '#title' => t('GUID field'), + '#description' => t('The globally unique identifier of the RSS item.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['guid_field_options']['guid_field'], + '#required' => TRUE, + ); + $form['guid_field_options']['guid_field_is_permalink'] = array( + '#type' => 'checkbox', + '#title' => t('GUID is permalink'), + '#description' => t('The RSS item GUID is a permalink.'), + '#default_value' => $this->options['guid_field_options']['guid_field_is_permalink'], + ); + } + + public function validate() { + $errors = parent::validate(); + $required_options = array('title_field', 'link_field', 'description_field', 'creator_field', 'date_field'); + foreach ($required_options as $required_option) { + if (empty($this->options[$required_option])) { + $errors[] = t('Row style plugin requires specifying which views fields to use for RSS item.'); + break; + } + } + // Once more for guid. + if (empty($this->options['guid_field_options']['guid_field'])) { + $errors[] = t('Row style plugin requires specifying which views fields to use for RSS item.'); + } + return $errors; + } + + function render($row) { + static $row_index; + if (!isset($row_index)) { + $row_index = 0; + } + if (function_exists('rdf_get_namespaces')) { + // Merge RDF namespaces in the XML namespaces in case they are used + // further in the RSS content. + $xml_rdf_namespaces = array(); + foreach (rdf_get_namespaces() as $prefix => $uri) { + $xml_rdf_namespaces['xmlns:' . $prefix] = $uri; + } + $this->view->style_plugin->namespaces += $xml_rdf_namespaces; + } + + // Create the RSS item object. + $item = new \stdClass(); + $item->title = $this->get_field($row_index, $this->options['title_field']); + $item->link = url($this->get_field($row_index, $this->options['link_field']), array('absolute' => TRUE)); + $item->description = $this->get_field($row_index, $this->options['description_field']); + $item->elements = array( + array('key' => 'pubDate', 'value' => $this->get_field($row_index, $this->options['date_field'])), + array( + 'key' => 'dc:creator', + 'value' => $this->get_field($row_index, $this->options['creator_field']), + 'namespace' => array('xmlns:dc' => 'http://purl.org/dc/elements/1.1/'), + ), + ); + $guid_is_permalink_string = 'false'; + $item_guid = $this->get_field($row_index, $this->options['guid_field_options']['guid_field']); + if ($this->options['guid_field_options']['guid_field_is_permalink']) { + $guid_is_permalink_string = 'true'; + $item_guid = url($item_guid, array('absolute' => TRUE)); + } + $item->elements[] = array( + 'key' => 'guid', + 'value' => $item_guid, + 'attributes' => array('isPermaLink' => $guid_is_permalink_string), + ); + + $row_index++; + + foreach ($item->elements as $element) { + if (isset($element['namespace'])) { + $this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $element['namespace']); + } + } + + return theme($this->themeFunctions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $item, + 'field_alias' => isset($this->field_alias) ? $this->field_alias : '', + )); + } + + /** + * Retrieves a views field value from the style plugin. + * + * @param $index + * The index count of the row as expected by views_plugin_style::get_field(). + * @param $field_id + * The ID assigned to the required field in the display. + */ + function get_field($index, $field_id) { + if (empty($this->view->style_plugin) || !is_object($this->view->style_plugin) || empty($field_id)) { + return ''; + } + return $this->view->style_plugin->get_field($index, $field_id); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/sort/Broken.php b/core/modules/views/lib/Drupal/views/Plugin/views/sort/Broken.php new file mode 100644 index 0000000..4080a04 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/sort/Broken.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\sort\Broken. + */ + +namespace Drupal\views\Plugin\views\sort; + +use Drupal\Core\Annotation\Plugin; + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_sort_handlers + * + * @Plugin( + * id = "broken" + * ) + */ +class Broken extends SortPluginBase { + + public function adminLabel($short = FALSE) { + return t('Broken/missing handler'); + } + + public function defineOptions() { return array(); } + public function ensureMyTable() { /* No table to ensure! */ } + public function query($group_by = FALSE) { /* No query to run */ } + public function buildOptionsForm(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '<div class="form-item description">' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '</div>', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + public function broken() { return TRUE; } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/sort/Date.php b/core/modules/views/lib/Drupal/views/Plugin/views/sort/Date.php new file mode 100644 index 0000000..a00e3d8 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/sort/Date.php @@ -0,0 +1,82 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\sort\Date. + */ + +namespace Drupal\views\Plugin\views\sort; + +use Drupal\Core\Annotation\Plugin; + +/** + * Basic sort handler for dates. + * + * This handler enables granularity, which is the ability to make dates + * equivalent based upon nearness. + * + * @Plugin( + * id = "date" + * ) + */ +class Date extends SortPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['granularity'] = array('default' => 'second'); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['granularity'] = array( + '#type' => 'radios', + '#title' => t('Granularity'), + '#options' => array( + 'second' => t('Second'), + 'minute' => t('Minute'), + 'hour' => t('Hour'), + 'day' => t('Day'), + 'month' => t('Month'), + 'year' => t('Year'), + ), + '#description' => t('The granularity is the smallest unit to use when determining whether two dates are the same; for example, if the granularity is "Year" then all dates in 1999, regardless of when they fall in 1999, will be considered the same date.'), + '#default_value' => $this->options['granularity'], + ); + } + + /** + * Called to add the sort to a query. + */ + public function query() { + $this->ensureMyTable(); + switch ($this->options['granularity']) { + case 'second': + default: + $this->query->add_orderby($this->tableAlias, $this->realField, $this->options['order']); + return; + case 'minute': + $formula = $this->getSQLFormat('YmdHi'); + break; + case 'hour': + $formula = $this->getSQLFormat('YmdH'); + break; + case 'day': + $formula = $this->getSQLFormat('Ymd'); + break; + case 'month': + $formula = $this->getSQLFormat('Ym'); + break; + case 'year': + $formula = $this->getSQLFormat('Y'); + break; + } + + // Add the field. + $this->query->add_orderby(NULL, $formula, $this->options['order'], $this->tableAlias . '_' . $this->field . '_' . $this->options['granularity']); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/sort/GroupByNumeric.php b/core/modules/views/lib/Drupal/views/Plugin/views/sort/GroupByNumeric.php new file mode 100644 index 0000000..e52bc06 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/sort/GroupByNumeric.php @@ -0,0 +1,47 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\sort\GroupByNumeric. + */ + +namespace Drupal\views\Plugin\views\sort; + +use Drupal\Core\Annotation\Plugin; +use Drupal\views\ViewExecutable; + +/** + * Handler for GROUP BY on simple numeric fields. + * + * @Plugin( + * id = "groupby_numeric" + * ) + */ +class GroupByNumeric extends SortPluginBase { + + public function init(ViewExecutable $view, &$options) { + parent::init($view, $options); + + // Initialize the original handler. + $this->handler = views_get_handler($options['table'], $options['field'], 'sort'); + $this->handler->init($view, $options); + } + + /** + * Called to add the field to a query. + */ + public function query() { + $this->ensureMyTable(); + + $params = array( + 'function' => $this->options['group_type'], + ); + + $this->query->add_orderby($this->tableAlias, $this->realField, $this->options['order'], NULL, $params); + } + + public function adminLabel($short = FALSE) { + return $this->getField(parent::adminLabel($short)); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/sort/MenuHierarchy.php b/core/modules/views/lib/Drupal/views/Plugin/views/sort/MenuHierarchy.php new file mode 100644 index 0000000..0b74348 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/sort/MenuHierarchy.php @@ -0,0 +1,69 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\sort\MenuHierarchy. + */ + +namespace Drupal\views\Plugin\views\sort; + +use Drupal\Core\Annotation\Plugin; + + +/** + * Sort in menu hierarchy order. + * + * Given a field name of 'p' this produces an ORDER BY on p1, p2, ..., p9; + * and optionally injects multiple joins to {menu_links} to sort by weight + * and title as well. + * + * This is only really useful for the {menu_links} table. + * + * @Plugin( + * id = "menu_hierarchy" + * ) + */ +class MenuHierarchy extends SortPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['sort_within_level'] = array('default' => FALSE); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['sort_within_level'] = array( + '#type' => 'checkbox', + '#title' => t('Sort within each hierarchy level'), + '#description' => t('Enable this to sort the items within each level of the hierarchy by weight and title. Warning: this may produce a slow query.'), + '#default_value' => $this->options['sort_within_level'], + ); + } + + public function query() { + $this->ensureMyTable(); + $max_depth = isset($this->definition['max depth']) ? $this->definition['max depth'] : MENU_MAX_DEPTH; + for ($i = 1; $i <= $max_depth; ++$i) { + if ($this->options['sort_within_level']) { + $definition = array( + 'table' => 'menu_links', + 'field' => 'mlid', + 'left_table' => $this->tableAlias, + 'left_field' => $this->field . $i + ); + $join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $definition); + + $menu_links = $this->query->add_table('menu_links', NULL, $join); + $this->query->add_orderby($menu_links, 'weight', $this->options['order']); + $this->query->add_orderby($menu_links, 'link_title', $this->options['order']); + } + + // We need this even when also sorting by weight and title, to make sure + // that children of two parents with the same weight and title are + // correctly separated. + $this->query->add_orderby($this->tableAlias, $this->field . $i, $this->options['order']); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/sort/Random.php b/core/modules/views/lib/Drupal/views/Plugin/views/sort/Random.php new file mode 100644 index 0000000..8a53170 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/sort/Random.php @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\sort\Random. + */ + +namespace Drupal\views\Plugin\views\sort; + +use Drupal\Core\Annotation\Plugin; + +/** + * Handle a random sort. + * + * @Plugin( + * id = "random" + * ) + */ +class Random extends SortPluginBase { + + public function query() { + $this->query->add_orderby('rand'); + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['order']['#access'] = FALSE; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/sort/SortPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/sort/SortPluginBase.php new file mode 100644 index 0000000..17f1a5f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/sort/SortPluginBase.php @@ -0,0 +1,221 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\sort\SortPluginBase. + */ + +namespace Drupal\views\Plugin\views\sort; + +use Drupal\views\Plugin\views\HandlerBase; +use Drupal\Core\Annotation\Plugin; + +/** + * @defgroup views_sort_handlers Views sort handlers + * @{ + * Handlers to tell Views how to sort queries. + */ + +/** + * Base sort handler that has no options and performs a simple sort. + * + * @ingroup views_sort_handlers + */ +abstract class SortPluginBase extends HandlerBase { + + /** + * Determine if a sort can be exposed. + */ + public function canExpose() { return TRUE; } + + /** + * Called to add the sort to a query. + */ + public function query() { + $this->ensureMyTable(); + // Add the field. + $this->query->add_orderby($this->tableAlias, $this->realField, $this->options['order']); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['order'] = array('default' => 'ASC'); + $options['exposed'] = array('default' => FALSE, 'bool' => TRUE); + $options['expose'] = array( + 'contains' => array( + 'label' => array('default' => '', 'translatable' => TRUE), + ), + ); + return $options; + } + + /** + * Display whether or not the sort order is ascending or descending + */ + public function adminSummary() { + if (!empty($this->options['exposed'])) { + return t('Exposed'); + } + switch ($this->options['order']) { + case 'ASC': + case 'asc': + default: + return t('asc'); + break; + case 'DESC'; + case 'desc'; + return t('desc'); + break; + } + } + + /** + * Basic options for all sort criteria + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + if ($this->canExpose()) { + $this->showExposeButton($form, $form_state); + } + $form['op_val_start'] = array('#value' => '<div class="clearfix">'); + $this->show_sort_form($form, $form_state); + $form['op_val_end'] = array('#value' => '</div>'); + if ($this->canExpose()) { + $this->showExposeForm($form, $form_state); + } + } + + /** + * Shortcut to display the expose/hide button. + */ + public function showExposeButton(&$form, &$form_state) { + $form['expose_button'] = array( + '#prefix' => '<div class="views-expose clearfix">', + '#suffix' => '</div>', + // Should always come first + '#weight' => -1000, + ); + + // Add a checkbox for JS users, which will have behavior attached to it + // so it can replace the button. + $form['expose_button']['checkbox'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('js-only')), + ); + $form['expose_button']['checkbox']['checkbox'] = array( + '#title' => t('Expose this sort to visitors, to allow them to change it'), + '#type' => 'checkbox', + ); + + // Then add the button itself. + if (empty($this->options['exposed'])) { + $form['expose_button']['markup'] = array( + '#markup' => '<div class="description exposed-description" style="float: left; margin-right:10px">' . t('This sort is not exposed. Expose it to allow the users to change it.') . '</div>', + ); + $form['expose_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Expose sort'), + '#submit' => array('views_ui_config_item_form_expose'), + ); + $form['expose_button']['checkbox']['checkbox']['#default_value'] = 0; + } + else { + $form['expose_button']['markup'] = array( + '#markup' => '<div class="description exposed-description">' . t('This sort is exposed. If you hide it, users will not be able to change it.') . '</div>', + ); + $form['expose_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Hide sort'), + '#submit' => array('views_ui_config_item_form_expose'), + ); + $form['expose_button']['checkbox']['checkbox']['#default_value'] = 1; + } + } + + /** + * Simple validate handler + */ + public function validateOptionsForm(&$form, &$form_state) { + $this->sort_validate($form, $form_state); + if (!empty($this->options['exposed'])) { + $this->validateExposeForm($form, $form_state); + } + + } + + /** + * Simple submit handler + */ + public function submitOptionsForm(&$form, &$form_state) { + unset($form_state['values']['expose_button']); // don't store this. + $this->sort_submit($form, $form_state); + if (!empty($this->options['exposed'])) { + $this->submitExposeForm($form, $form_state); + } + } + + /** + * Shortcut to display the value form. + */ + function show_sort_form(&$form, &$form_state) { + $options = $this->sort_options(); + if (!empty($options)) { + $form['order'] = array( + '#type' => 'radios', + '#options' => $options, + '#default_value' => $this->options['order'], + ); + } + } + + function sort_validate(&$form, &$form_state) { } + + function sort_submit(&$form, &$form_state) { } + + /** + * Provide a list of options for the default sort form. + * Should be overridden by classes that don't override sort_form + */ + function sort_options() { + return array( + 'ASC' => t('Sort ascending'), + 'DESC' => t('Sort descending'), + ); + } + + public function buildExposeForm(&$form, &$form_state) { + // #flatten will move everything from $form['expose'][$key] to $form[$key] + // prior to rendering. That's why the pre_render for it needs to run first, + // so that when the next pre_render (the one for fieldsets) runs, it gets + // the flattened data. + array_unshift($form['#pre_render'], 'views_ui_pre_render_flatten_data'); + $form['expose']['#flatten'] = TRUE; + + $form['expose']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['label'], + '#title' => t('Label'), + '#required' => TRUE, + '#size' => 40, + '#weight' => -1, + ); + } + + /** + * Provide default options for exposed sorts. + */ + public function defaultExposeOptions() { + $this->options['expose'] = array( + 'order' => $this->options['order'], + 'label' => $this->definition['title'], + ); + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/sort/Standard.php b/core/modules/views/lib/Drupal/views/Plugin/views/sort/Standard.php new file mode 100644 index 0000000..d5767fe --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/sort/Standard.php @@ -0,0 +1,23 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\sort\Standard. + */ + +namespace Drupal\views\Plugin\views\sort; + +use Drupal\Core\Annotation\Plugin; + +/** + * Default implementation of the base sort plugin. + * + * @ingroup views_sort_handlers + * + * @Plugin( + * id = "standard" + * ) + */ +class Standard extends SortPluginBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/DefaultStyle.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/DefaultStyle.php new file mode 100644 index 0000000..17c4abd --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/DefaultStyle.php @@ -0,0 +1,43 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\DefaultStyle. + */ + +namespace Drupal\views\Plugin\views\style; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Unformatted style plugin to render rows one after another with no + * decorations. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "default", + * title = @Translation("Unformatted list"), + * help = @Translation("Displays rows one after another."), + * theme = "views_view_unformatted", + * type = "normal" + * ) + */ +class DefaultStyle extends StylePluginBase { + + /** + * Does the style plugin allows to use style plugins. + * + * @var bool + */ + protected $usesRowPlugin = TRUE; + + /** + * Does the style plugin support custom css class for the rows. + * + * @var bool + */ + protected $usesRowClass = TRUE; + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/DefaultSummary.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/DefaultSummary.php new file mode 100644 index 0000000..5cb448f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/DefaultSummary.php @@ -0,0 +1,94 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\StyleSummaryPluginBase. + */ + +namespace Drupal\views\Plugin\views\style; + +use Drupal\views\Plugin\views\style\StylePluginBase; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The default style plugin for summaries. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "default_summary", + * title = @Translation("List"), + * help = @Translation("Displays the default summary as a list."), + * theme = "views_view_summary", + * type = "summary" + * ) + */ +class DefaultSummary extends StylePluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['base_path'] = array('default' => ''); + $options['count'] = array('default' => TRUE, 'bool' => TRUE); + $options['override'] = array('default' => FALSE, 'bool' => TRUE); + $options['items_per_page'] = array('default' => 25); + + return $options; + } + + public function query() { + if (!empty($this->options['override'])) { + $this->view->setItemsPerPage(intval($this->options['items_per_page'])); + } + } + + public function buildOptionsForm(&$form, &$form_state) { + $form['base_path'] = array( + '#type' => 'textfield', + '#title' => t('Base path'), + '#default_value' => $this->options['base_path'], + '#description' => t('Define the base path for links in this summary + view, i.e. http://example.com/<strong>your_view_path/archive</strong>. + Do not include beginning and ending forward slash. If this value + is empty, views will use the first path found as the base path, + in page displays, or / if no path could be found.'), + ); + $form['count'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['count']), + '#title' => t('Display record count with link'), + ); + $form['override'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['override']), + '#title' => t('Override number of items to display'), + ); + + $form['items_per_page'] = array( + '#type' => 'textfield', + '#title' => t('Items to display'), + '#default_value' => $this->options['items_per_page'], + '#states' => array( + 'visible' => array( + ':input[name="options[summary][options][' . $this->definition['id'] . '][override]"]' => array('checked' => TRUE), + ), + ), + ); + } + + function render() { + $rows = array(); + foreach ($this->view->result as $row) { + // @todo: Include separator as an option. + $rows[] = $row; + } + + return theme($this->themeFunctions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'rows' => $rows + )); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/Grid.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/Grid.php new file mode 100644 index 0000000..0cfaa2d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/Grid.php @@ -0,0 +1,91 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\Grid. + */ + +namespace Drupal\views\Plugin\views\style; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Style plugin to render each item in a grid cell. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "grid", + * title = @Translation("Grid"), + * help = @Translation("Displays rows in a grid."), + * theme = "views_view_grid", + * type = "normal" + * ) + */ +class Grid extends StylePluginBase { + + /** + * Does the style plugin allows to use style plugins. + * + * @var bool + */ + protected $usesRowPlugin = TRUE; + + /** + * Does the style plugin support custom css class for the rows. + * + * @var bool + */ + protected $usesRowClass = TRUE; + + /** + * Set default options + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['columns'] = array('default' => '4'); + $options['alignment'] = array('default' => 'horizontal'); + $options['fill_single_line'] = array('default' => TRUE, 'bool' => TRUE); + $options['summary'] = array('default' => ''); + + return $options; + } + + /** + * Render the given style. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['columns'] = array( + '#type' => 'number', + '#title' => t('Number of columns'), + '#default_value' => $this->options['columns'], + '#required' => TRUE, + '#min' => 0, + ); + $form['alignment'] = array( + '#type' => 'radios', + '#title' => t('Alignment'), + '#options' => array('horizontal' => t('Horizontal'), 'vertical' => t('Vertical')), + '#default_value' => $this->options['alignment'], + '#description' => t('Horizontal alignment will place items starting in the upper left and moving right. Vertical alignment will place items starting in the upper left and moving down.'), + ); + + $form['fill_single_line'] = array( + '#type' => 'checkbox', + '#title' => t('Fill up single line'), + '#description' => t('If you disable this option, a grid with only one row will have the same number of table cells (<TD>) as items. Disabling it can cause problems with your CSS.'), + '#default_value' => !empty($this->options['fill_single_line']), + ); + + $form['summary'] = array( + '#type' => 'textfield', + '#title' => t('Table summary'), + '#description' => t('This value will be displayed as table-summary attribute in the html. Set this for better accessiblity of your site.'), + '#default_value' => $this->options['summary'], + ); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/HtmlList.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/HtmlList.php new file mode 100644 index 0000000..a9ca9fd --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/HtmlList.php @@ -0,0 +1,82 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\List. + */ + +namespace Drupal\views\Plugin\views\style; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Style plugin to render each item in an ordered or unordered list. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "html_list", + * title = @Translation("HTML List"), + * help = @Translation("Displays rows as HTML list."), + * theme = "views_view_list", + * type = "normal" + * ) + */ +class HtmlList extends StylePluginBase { + + /** + * Does the style plugin allows to use style plugins. + * + * @var bool + */ + protected $usesRowPlugin = TRUE; + + /** + * Does the style plugin support custom css class for the rows. + * + * @var bool + */ + protected $usesRowClass = TRUE; + + /** + * Set default options + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['type'] = array('default' => 'ul'); + $options['class'] = array('default' => ''); + $options['wrapper_class'] = array('default' => 'item-list'); + + return $options; + } + + /** + * Render the given style. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['type'] = array( + '#type' => 'radios', + '#title' => t('List type'), + '#options' => array('ul' => t('Unordered list'), 'ol' => t('Ordered list')), + '#default_value' => $this->options['type'], + ); + $form['wrapper_class'] = array( + '#title' => t('Wrapper class'), + '#description' => t('The class to provide on the wrapper, outside the list.'), + '#type' => 'textfield', + '#size' => '30', + '#default_value' => $this->options['wrapper_class'], + ); + $form['class'] = array( + '#title' => t('List class'), + '#description' => t('The class to provide on the list element itself.'), + '#type' => 'textfield', + '#size' => '30', + '#default_value' => $this->options['class'], + ); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/Mapping.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/Mapping.php new file mode 100644 index 0000000..8e9649e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/Mapping.php @@ -0,0 +1,143 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\Mapping. + */ + +namespace Drupal\views\Plugin\views\style; + +/** + * Allows fields to be mapped to specific use cases. + * + * @ingroup views_style_plugins + */ +abstract class Mapping extends StylePluginBase { + + /** + * Do not use grouping. + * + * @var bool + */ + protected $usesGrouping = FALSE; + + /** + * Use fields without a row plugin. + * + * @var bool + */ + protected $usesFields = TRUE; + + /** + * Builds the list of field mappings. + * + * @return array + * An associative array, keyed by the field name, containing the following + * key-value pairs: + * - #title: The human-readable label for this field. + * - #default_value: The default value for this field. If not provided, an + * empty string will be used. + * - #description: A description of this field. + * - #required: Whether this field is required. + * - #filter: (optional) A method on the plugin to filter field options. + * - #toggle: (optional) If this select should be toggled by a checkbox. + */ + abstract protected function defineMapping(); + + /** + * Overrides Drupal\views\Plugin\views\style\StylePluginBase::defineOptions(). + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + // Parse the mapping and add a default for each. + foreach ($this->defineMapping() as $key => $value) { + $default = !empty($value['#multiple']) ? array() : ''; + $options['mapping']['contains'][$key] = array( + 'default' => isset($value['#default_value']) ? $value['#default_value'] : $default, + ); + if (!empty($value['#toggle'])) { + $options['mapping']['contains']["toggle_$key"] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + } + } + + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\style\StylePluginBase::buildOptionsForm(). + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + // Get the mapping. + $mapping = $this->defineMapping(); + + // Restrict the list of defaults to the mapping, in case they have changed. + $options = array_intersect_key($this->options['mapping'], $mapping); + + // Get the labels of the fields added to this display. + $field_labels = $this->displayHandler->getFieldLabels(); + + // Provide some default values. + $defaults = array( + '#type' => 'select', + '#required' => FALSE, + '#multiple' => FALSE, + ); + + // For each mapping, add a select element to the form. + foreach ($options as $key => $value) { + // If the field is optional, add a 'None' value to the top of the options. + $field_options = array(); + $required = !empty($mapping[$key]['#required']); + if (!$required && empty($mapping[$key]['#multiple'])) { + $field_options = array('' => t('- None -')); + } + $field_options += $field_labels; + + // Optionally filter the available fields. + if (isset($mapping[$key]['#filter'])) { + $this->view->initHandlers(); + $this::$mapping[$key]['#filter']($field_options); + unset($mapping[$key]['#filter']); + } + + // These values must always be set. + $overrides = array( + '#options' => $field_options, + '#default_value' => $options[$key], + ); + + // Optionally allow the select to be toggleable. + if (!empty($mapping[$key]['#toggle'])) { + $form['mapping']["toggle_$key"] = array( + '#type' => 'checkbox', + '#title' => t('Use a custom %field_name', array('%field_name' => strtolower($mapping[$key]['#title']))), + '#default_value' => $this->options['mapping']["toggle_$key"], + ); + $overrides['#states']['visible'][':input[name="style_options[mapping][' . "toggle_$key" . ']"]'] = array('checked' => TRUE); + } + + $form['mapping'][$key] = $overrides + $mapping[$key] + $defaults; + } + } + + /** + * Overrides Drupal\views\Plugin\views\style\StylePluginBase::render(). + * + * Provides the mapping definition as an available variable. + */ + function render() { + return theme($this->themeFunctions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'rows' => $this->view->result, + 'mapping' => $this->defineMapping(), + )); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/Rss.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/Rss.php new file mode 100644 index 0000000..e4d1e96 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/Rss.php @@ -0,0 +1,145 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\Rss. + */ + +namespace Drupal\views\Plugin\views\style; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Default style plugin to render an RSS feed. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "rss", + * title = @Translation("RSS Feed"), + * help = @Translation("Generates an RSS feed from a view."), + * theme = "views_view_rss", + * type = "feed" + * ) + */ +class Rss extends StylePluginBase { + + /** + * Does the style plugin for itself support to add fields to it's output. + * + * @var bool + */ + protected $usesRowPlugin = TRUE; + + function attach_to($display_id, $path, $title) { + $display = $this->view->displayHandlers[$display_id]; + $url_options = array(); + $input = $this->view->getExposedInput(); + if ($input) { + $url_options['query'] = $input; + } + $url_options['absolute'] = TRUE; + + $url = url($this->view->getUrl(NULL, $path), $url_options); + if ($display->hasPath()) { + if (empty($this->preview)) { + drupal_add_feed($url, $title); + } + } + else { + if (empty($this->view->feed_icon)) { + $this->view->feed_icon = ''; + } + + $this->view->feed_icon .= theme('feed_icon', array('url' => $url, 'title' => $title)); + drupal_add_html_head_link(array( + 'rel' => 'alternate', + 'type' => 'application/rss+xml', + 'title' => $title, + 'href' => $url + )); + } + } + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['description'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['description'] = array( + '#type' => 'textfield', + '#title' => t('RSS description'), + '#default_value' => $this->options['description'], + '#description' => t('This will appear in the RSS feed itself.'), + '#maxlength' => 1024, + ); + } + + /** + * Return an array of additional XHTML elements to add to the channel. + * + * @return + * An array that can be passed to format_xml_elements(). + */ + function get_channel_elements() { + return array(); + } + + /** + * Get RSS feed description. + * + * @return string + * The string containing the description with the tokens replaced. + */ + function get_description() { + $description = $this->options['description']; + + // Allow substitutions from the first row. + $description = $this->tokenize_value($description, 0); + + return $description; + } + + function render() { + if (empty($this->row_plugin)) { + debug('Drupal\views\Plugin\views\style\Rss: Missing row plugin'); + return; + } + $rows = ''; + + // This will be filled in by the row plugin and is used later on in the + // theming output. + $this->namespaces = array('xmlns:dc' => 'http://purl.org/dc/elements/1.1/'); + + // Fetch any additional elements for the channel and merge in their + // namespaces. + $this->channel_elements = $this->get_channel_elements(); + foreach ($this->channel_elements as $element) { + if (isset($element['namespace'])) { + $this->namespaces = array_merge($this->namespaces, $element['namespace']); + } + } + + foreach ($this->view->result as $row_index => $row) { + $this->view->row_index = $row_index; + $rows .= $this->row_plugin->render($row); + } + + $output = theme($this->themeFunctions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'rows' => $rows + )); + unset($this->view->row_index); + return $output; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/StylePluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/StylePluginBase.php new file mode 100644 index 0000000..65c83d8 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/StylePluginBase.php @@ -0,0 +1,690 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\StylePluginBase. + */ + +namespace Drupal\views\Plugin\views\style; + +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\Plugin\views\wizard\WizardInterface; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\ViewExecutable; + +/** + * @defgroup views_style_plugins Views style plugins + * @{ + * Style plugins control how a view is rendered. For example, they + * can choose to display a collection of fields, node_view() output, + * table output, or any kind of crazy output they want. + * + * Many style plugins can have an optional 'row' plugin, that displays + * a single record. Not all style plugins can utilize this, so it is + * up to the plugin to set this up and call through to the row plugin. + * + * @see hook_views_plugins() + */ + +/** + * Base class to define a style plugin handler. + */ +abstract class StylePluginBase extends PluginBase { + + /** + * Overrides Drupal\views\Plugin\Plugin::$usesOptions. + */ + protected $usesOptions = TRUE; + + /** + * Store all available tokens row rows. + */ + var $row_tokens = array(); + + /** + * Contains the row plugin, if it's initialized + * and the style itself supports it. + * + * @var views_plugin_row + */ + var $row_plugin; + + /** + * Does the style plugin allows to use style plugins. + * + * @var bool + */ + protected $usesRowPlugin = FALSE; + + /** + * Does the style plugin support custom css class for the rows. + * + * @var bool + */ + protected $usesRowClass = FALSE; + + /** + * Does the style plugin support grouping of rows. + * + * @var bool + */ + protected $usesGrouping = TRUE; + + /** + * Does the style plugin for itself support to add fields to it's output. + * + * This option only makes sense on style plugins without row plugins, like + * for example table. + * + * @var bool + */ + protected $usesFields = FALSE; + + /** + * Initialize a style plugin. + * + * @param $view + * @param $display + * @param $options + * The style options might come externally as the style can be sourced + * from at least two locations. If it's not included, look on the display. + */ + public function init(ViewExecutable $view, &$display, $options = NULL) { + $this->setOptionDefaults($this->options, $this->defineOptions()); + $this->view = &$view; + $this->displayHandler = &$display; + + $this->unpackOptions($this->options, $options); + + if ($this->usesRowPlugin() && $display->getOption('row')) { + $this->row_plugin = $display->getPlugin('row'); + } + + $this->options += array( + 'grouping' => array(), + ); + + } + + public function destroy() { + parent::destroy(); + + if (isset($this->row_plugin)) { + $this->row_plugin->destroy(); + } + } + + /** + * Returns the usesRowPlugin property. + * + * @return bool + */ + function usesRowPlugin() { + return $this->usesRowPlugin; + + } + + /** + * Returns the usesRowClass property. + * + * @return bool + */ + function usesRowClass() { + return $this->usesRowClass; + } + + /** + * Returns the usesGrouping property. + * + * @return bool + */ + function usesGrouping() { + return $this->usesGrouping; + } + + /** + * Return TRUE if this style also uses fields. + * + * @return bool + */ + function usesFields() { + // If we use a row plugin, ask the row plugin. Chances are, we don't + // care, it does. + $row_uses_fields = FALSE; + if ($this->usesRowPlugin() && !empty($this->row_plugin)) { + $row_uses_fields = $this->row_plugin->usesFields(); + } + // Otherwise, check the definition or the option. + return $row_uses_fields || $this->usesFields || !empty($this->options['uses_fields']); + } + + /** + * Return TRUE if this style uses tokens. + * + * Used to ensure we don't fetch tokens when not needed for performance. + */ + function uses_tokens() { + if ($this->usesRowClass()) { + $class = $this->options['row_class']; + if (strpos($class, '[') !== FALSE || strpos($class, '!') !== FALSE || strpos($class, '%') !== FALSE) { + return TRUE; + } + } + } + + /** + * Return the token replaced row class for the specified row. + */ + function get_row_class($row_index) { + if ($this->usesRowClass()) { + $class = $this->options['row_class']; + if ($this->usesFields() && $this->view->field) { + $class = strip_tags($this->tokenize_value($class, $row_index)); + } + + $classes = explode(' ', $class); + foreach ($classes as &$class) { + $class = drupal_clean_css_identifier($class); + } + return implode(' ', $classes); + } + } + + /** + * Take a value and apply token replacement logic to it. + */ + function tokenize_value($value, $row_index) { + if (strpos($value, '[') !== FALSE || strpos($value, '!') !== FALSE || strpos($value, '%') !== FALSE) { + $fake_item = array( + 'alter_text' => TRUE, + 'text' => $value, + ); + + // Row tokens might be empty, for example for node row style. + $tokens = isset($this->row_tokens[$row_index]) ? $this->row_tokens[$row_index] : array(); + if (!empty($this->view->build_info['substitutions'])) { + $tokens += $this->view->build_info['substitutions']; + } + + if ($tokens) { + $value = strtr($value, $tokens); + } + } + + return $value; + } + + /** + * Should the output of the style plugin be rendered even if it's a empty view. + */ + function even_empty() { + return !empty($this->definition['even empty']); + } + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['grouping'] = array('default' => array()); + if ($this->usesRowClass()) { + $options['row_class'] = array('default' => ''); + $options['default_row_class'] = array('default' => TRUE, 'bool' => TRUE); + $options['row_class_special'] = array('default' => TRUE, 'bool' => TRUE); + } + $options['uses_fields'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + // Only fields-based views can handle grouping. Style plugins can also exclude + // themselves from being groupable by setting their "usesGrouping" property + // to FALSE. + // @TODO: Document "usesGrouping" in docs.php when docs.php is written. + if ($this->usesFields() && $this->usesGrouping()) { + $options = array('' => t('- None -')); + $field_labels = $this->displayHandler->getFieldLabels(TRUE); + $options += $field_labels; + // If there are no fields, we can't group on them. + if (count($options) > 1) { + // This is for backward compability, when there was just a single select form. + if (is_string($this->options['grouping'])) { + $grouping = $this->options['grouping']; + $this->options['grouping'] = array(); + $this->options['grouping'][0]['field'] = $grouping; + } + if (isset($this->options['group_rendered']) && is_string($this->options['group_rendered'])) { + $this->options['grouping'][0]['rendered'] = $this->options['group_rendered']; + unset($this->options['group_rendered']); + } + + $c = count($this->options['grouping']); + // Add a form for every grouping, plus one. + for ($i = 0; $i <= $c; $i++) { + $grouping = !empty($this->options['grouping'][$i]) ? $this->options['grouping'][$i] : array(); + $grouping += array('field' => '', 'rendered' => TRUE, 'rendered_strip' => FALSE); + $form['grouping'][$i]['field'] = array( + '#type' => 'select', + '#title' => t('Grouping field Nr.@number', array('@number' => $i + 1)), + '#options' => $options, + '#default_value' => $grouping['field'], + '#description' => t('You may optionally specify a field by which to group the records. Leave blank to not group.'), + ); + $form['grouping'][$i]['rendered'] = array( + '#type' => 'checkbox', + '#title' => t('Use rendered output to group rows'), + '#default_value' => $grouping['rendered'], + '#description' => t('If enabled the rendered output of the grouping field is used to group the rows.'), + '#states' => array( + 'invisible' => array( + ':input[name="style_options[grouping][' . $i . '][field]"]' => array('value' => ''), + ), + ), + ); + $form['grouping'][$i]['rendered_strip'] = array( + '#type' => 'checkbox', + '#title' => t('Remove tags from rendered output'), + '#default_value' => $grouping['rendered_strip'], + '#states' => array( + 'invisible' => array( + ':input[name="style_options[grouping][' . $i . '][field]"]' => array('value' => ''), + ), + ), + ); + } + } + } + + if ($this->usesRowClass()) { + $form['row_class'] = array( + '#title' => t('Row class'), + '#description' => t('The class to provide on each row.'), + '#type' => 'textfield', + '#default_value' => $this->options['row_class'], + ); + + if ($this->usesFields()) { + $form['row_class']['#description'] .= ' ' . t('You may use field tokens from as per the "Replacement patterns" used in "Rewrite the output of this field" for all fields.'); + } + + $form['default_row_class'] = array( + '#title' => t('Add views row classes'), + '#description' => t('Add the default row classes like views-row-1 to the output. You can use this to quickly reduce the amount of markup the view provides by default, at the cost of making it more difficult to apply CSS.'), + '#type' => 'checkbox', + '#default_value' => $this->options['default_row_class'], + ); + $form['row_class_special'] = array( + '#title' => t('Add striping (odd/even), first/last row classes'), + '#description' => t('Add css classes to the first and last line, as well as odd/even classes for striping.'), + '#type' => 'checkbox', + '#default_value' => $this->options['row_class_special'], + ); + } + + if (!$this->usesFields() || !empty($this->options['uses_fields'])) { + $form['uses_fields'] = array( + '#type' => 'checkbox', + '#title' => t('Force using fields'), + '#description' => t('If neither the row nor the style plugin supports fields, this field allows to enable them, so you can for example use groupby.'), + '#default_value' => $this->options['uses_fields'], + ); + } + } + + public function validateOptionsForm(&$form, &$form_state) { + // Don't run validation on style plugins without the grouping setting. + if (isset($form_state['values']['style_options']['grouping'])) { + // Don't save grouping if no field is specified. + foreach ($form_state['values']['style_options']['grouping'] as $index => $grouping) { + if (empty($grouping['field'])) { + unset($form_state['values']['style_options']['grouping'][$index]); + } + } + } + } + + /** + * Provide a form in the views wizard if this style is selected. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * @param string $type + * The display type, either block or page. + */ + function wizard_form(&$form, &$form_state, $type) { + } + + /** + * Alter the options of a display before they are added to the view. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * @param Drupal\views\Plugin\views\wizard\WizardInterface $wizard + * The current used wizard. + * @param array $display_options + * The options which will be used on the view. The style plugin should + * alter this to its own needs. + * @param string $display_type + * The display type, either block or page. + */ + function wizard_submit(&$form, &$form_state, WizardInterface $wizard, &$display_options, $display_type) { + } + + /** + * Called by the view builder to see if this style handler wants to + * interfere with the sorts. If so it should build; if it returns + * any non-TRUE value, normal sorting will NOT be added to the query. + */ + function build_sort() { return TRUE; } + + /** + * Called by the view builder to let the style build a second set of + * sorts that will come after any other sorts in the view. + */ + function build_sort_post() { } + + /** + * Allow the style to do stuff before each row is rendered. + * + * @param $result + * The full array of results from the query. + */ + function pre_render($result) { + if (!empty($this->row_plugin)) { + $this->row_plugin->pre_render($result); + } + } + + /** + * Render the display in this style. + */ + function render() { + if ($this->usesRowPlugin() && empty($this->row_plugin)) { + debug('Drupal\views\Plugin\views\style\StylePluginBase: Missing row plugin'); + return; + } + + // Group the rows according to the grouping instructions, if specified. + $sets = $this->render_grouping( + $this->view->result, + $this->options['grouping'], + TRUE + ); + + return $this->render_grouping_sets($sets); + } + + /** + * Render the grouping sets. + * + * Plugins may override this method if they wish some other way of handling + * grouping. + * + * @param $sets + * Array containing the grouping sets to render. + * @param $level + * Integer indicating the hierarchical level of the grouping. + * + * @return string + * Rendered output of given grouping sets. + */ + function render_grouping_sets($sets, $level = 0) { + $output = ''; + foreach ($sets as $set) { + $row = reset($set['rows']); + // Render as a grouping set. + if (is_array($row) && isset($row['group'])) { + $output .= theme(views_theme_functions('views_view_grouping', $this->view, $this->view->display_handler->display), + array( + 'view' => $this->view, + 'grouping' => $this->options['grouping'][$level], + 'grouping_level' => $level, + 'rows' => $set['rows'], + 'title' => $set['group']) + ); + } + // Render as a record set. + else { + if ($this->usesRowPlugin()) { + foreach ($set['rows'] as $index => $row) { + $this->view->row_index = $index; + $set['rows'][$index] = $this->row_plugin->render($row); + } + } + + $output .= theme($this->themeFunctions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'grouping_level' => $level, + 'rows' => $set['rows'], + 'title' => $set['group']) + ); + } + } + unset($this->view->row_index); + return $output; + } + + /** + * Group records as needed for rendering. + * + * @param $records + * An array of records from the view to group. + * @param $groupings + * An array of grouping instructions on which fields to group. If empty, the + * result set will be given a single group with an empty string as a label. + * @param $group_rendered + * Boolean value whether to use the rendered or the raw field value for + * grouping. If set to NULL the return is structured as before + * Views 7.x-3.0-rc2. After Views 7.x-3.0 this boolean is only used if + * $groupings is an old-style string or if the rendered option is missing + * for a grouping instruction. + * @return + * The grouped record set. + * A nested set structure is generated if multiple grouping fields are used. + * + * @code + * array( + * 'grouping_field_1:grouping_1' => array( + * 'group' => 'grouping_field_1:content_1', + * 'rows' => array( + * 'grouping_field_2:grouping_a' => array( + * 'group' => 'grouping_field_2:content_a', + * 'rows' => array( + * $row_index_1 => $row_1, + * $row_index_2 => $row_2, + * // ... + * ) + * ), + * ), + * ), + * 'grouping_field_1:grouping_2' => array( + * // ... + * ), + * ) + * @endcode + */ + function render_grouping($records, $groupings = array(), $group_rendered = NULL) { + // This is for backward compability, when $groupings was a string containing + // the ID of a single field. + if (is_string($groupings)) { + $rendered = $group_rendered === NULL ? TRUE : $group_rendered; + $groupings = array(array('field' => $groupings, 'rendered' => $rendered)); + } + + // Make sure fields are rendered + $this->render_fields($this->view->result); + $sets = array(); + if ($groupings) { + foreach ($records as $index => $row) { + // Iterate through configured grouping fields to determine the + // hierarchically positioned set where the current row belongs to. + // While iterating, parent groups, that do not exist yet, are added. + $set = &$sets; + foreach ($groupings as $info) { + $field = $info['field']; + $rendered = isset($info['rendered']) ? $info['rendered'] : $group_rendered; + $rendered_strip = isset($info['rendered_strip']) ? $info['rendered_strip'] : FALSE; + $grouping = ''; + $group_content = ''; + // Group on the rendered version of the field, not the raw. That way, + // we can control any special formatting of the grouping field through + // the admin or theme layer or anywhere else we'd like. + if (isset($this->view->field[$field])) { + $group_content = $this->get_field($index, $field); + if ($this->view->field[$field]->options['label']) { + $group_content = $this->view->field[$field]->options['label'] . ': ' . $group_content; + } + if ($rendered) { + $grouping = $group_content; + if ($rendered_strip) { + $group_content = $grouping = strip_tags(htmlspecialchars_decode($group_content)); + } + } + else { + $grouping = $this->get_field_value($index, $field); + // Not all field handlers return a scalar value, + // e.g. views_handler_field_field. + if (!is_scalar($grouping)) { + $grouping = md5(serialize($grouping)); + } + } + } + + // Create the group if it does not exist yet. + if (empty($set[$grouping])) { + $set[$grouping]['group'] = $group_content; + $set[$grouping]['rows'] = array(); + } + + // Move the set reference into the row set of the group we just determined. + $set = &$set[$grouping]['rows']; + } + // Add the row to the hierarchically positioned row set we just determined. + $set[$index] = $row; + } + } + else { + // Create a single group with an empty grouping field. + $sets[''] = array( + 'group' => '', + 'rows' => $records, + ); + } + + // If this parameter isn't explicitely set modify the output to be fully + // backward compatible to code before Views 7.x-3.0-rc2. + // @TODO Remove this as soon as possible e.g. October 2020 + if ($group_rendered === NULL) { + $old_style_sets = array(); + foreach ($sets as $group) { + $old_style_sets[$group['group']] = $group['rows']; + } + $sets = $old_style_sets; + } + + return $sets; + } + + /** + * Render all of the fields for a given style and store them on the object. + * + * @param $result + * The result array from $view->result + */ + function render_fields($result) { + if (!$this->usesFields()) { + return; + } + + if (!isset($this->rendered_fields)) { + $this->rendered_fields = array(); + $this->view->row_index = 0; + $keys = array_keys($this->view->field); + + // If all fields have a field::access FALSE there might be no fields, so + // there is no reason to execute this code. + if (!empty($keys)) { + foreach ($result as $count => $row) { + $this->view->row_index = $count; + foreach ($keys as $id) { + $this->rendered_fields[$count][$id] = $this->view->field[$id]->theme($row); + } + + $this->row_tokens[$count] = $this->view->field[$id]->get_render_tokens(array()); + } + } + unset($this->view->row_index); + } + + return $this->rendered_fields; + } + + /** + * Get a rendered field. + * + * @param $index + * The index count of the row. + * @param $field + * The id of the field. + */ + function get_field($index, $field) { + if (!isset($this->rendered_fields)) { + $this->render_fields($this->view->result); + } + + if (isset($this->rendered_fields[$index][$field])) { + return $this->rendered_fields[$index][$field]; + } + } + + /** + * Get the raw field value. + * + * @param $index + * The index count of the row. + * @param $field + * The id of the field. + */ + function get_field_value($index, $field) { + $this->view->row_index = $index; + $value = $this->view->field[$field]->get_value($this->view->result[$index]); + unset($this->view->row_index); + return $value; + } + + public function validate() { + $errors = parent::validate(); + + if ($this->usesRowPlugin()) { + $plugin = $this->displayHandler->getPlugin('row'); + if (empty($plugin)) { + $errors[] = t('Style @style requires a row style but the row plugin is invalid.', array('@style' => $this->definition['title'])); + } + else { + $result = $plugin->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + } + } + } + return $errors; + } + + public function query() { + parent::query(); + if (isset($this->row_plugin)) { + $this->row_plugin->query(); + } + } + +} + +/** + * @} + */ diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/Table.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/Table.php new file mode 100644 index 0000000..a7ad190 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/Table.php @@ -0,0 +1,379 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\Table. + */ + +namespace Drupal\views\Plugin\views\style; + +use Drupal\views\Plugin\views\wizard\WizardInterface; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Style plugin to render each item as a row in a table. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "table", + * title = @Translation("Table"), + * help = @Translation("Displays rows in a table."), + * theme = "views_view_table", + * type = "normal" + * ) + */ +class Table extends StylePluginBase { + + /** + * Does the style plugin for itself support to add fields to it's output. + * + * @var bool + */ + protected $usesFields = TRUE; + + /** + * Does the style plugin allows to use style plugins. + * + * @var bool + */ + protected $usesRowPlugin = FALSE; + + /** + * Does the style plugin support custom css class for the rows. + * + * @var bool + */ + protected $usesRowClass = TRUE; + + /** + * Contains the current active sort column. + * @var string + */ + public $active; + + /** + * Contains the current active sort order, either desc or asc. + * @var string + */ + public $order; + + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['columns'] = array('default' => array()); + $options['default'] = array('default' => ''); + $options['info'] = array('default' => array()); + $options['override'] = array('default' => TRUE, 'bool' => TRUE); + $options['sticky'] = array('default' => FALSE, 'bool' => TRUE); + $options['order'] = array('default' => 'asc'); + $options['summary'] = array('default' => '', 'translatable' => TRUE); + $options['empty_table'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Determine if we should provide sorting based upon $_GET inputs + * + * @return bool + */ + function build_sort() { + $order = drupal_container()->get('request')->query->get('order'); + if (!isset($order) && ($this->options['default'] == -1 || empty($this->view->field[$this->options['default']]))) { + return TRUE; + } + + // If a sort we don't know anything about gets through, exit gracefully. + if (isset($order) && empty($this->view->field[$order])) { + return TRUE; + } + + // Let the builder know whether or not we're overriding the default sorts. + return empty($this->options['override']); + } + + /** + * Add our actual sort criteria + */ + function build_sort_post() { + $query = drupal_container()->get('request')->query; + $order = $query->get('order'); + if (!isset($order)) { + // check for a 'default' clicksort. If there isn't one, exit gracefully. + if (empty($this->options['default'])) { + return; + } + $sort = $this->options['default']; + if (!empty($this->options['info'][$sort]['default_sort_order'])) { + $this->order = $this->options['info'][$sort]['default_sort_order']; + } + else { + $this->order = !empty($this->options['order']) ? $this->options['order'] : 'asc'; + } + } + else { + $sort = $order; + // Store the $order for later use. + $request_sort = $query->get('sort'); + $this->order = !empty($request_sort) ? strtolower($request_sort) : 'asc'; + } + + // If a sort we don't know anything about gets through, exit gracefully. + if (empty($this->view->field[$sort])) { + return; + } + + // Ensure $this->order is valid. + if ($this->order != 'asc' && $this->order != 'desc') { + $this->order = 'asc'; + } + + // Store the $sort for later use. + $this->active = $sort; + + // Tell the field to click sort. + $this->view->field[$sort]->click_sort($this->order); + } + + /** + * Normalize a list of columns based upon the fields that are + * available. This compares the fields stored in the style handler + * to the list of fields actually in the view, removing fields that + * have been removed and adding new fields in their own column. + * + * - Each field must be in a column. + * - Each column must be based upon a field, and that field + * is somewhere in the column. + * - Any fields not currently represented must be added. + * - Columns must be re-ordered to match the fields. + * + * @param $columns + * An array of all fields; the key is the id of the field and the + * value is the id of the column the field should be in. + * @param $fields + * The fields to use for the columns. If not provided, they will + * be requested from the current display. The running render should + * send the fields through, as they may be different than what the + * display has listed due to access control or other changes. + * + * @return array + * An array of all the sanitized columns. + */ + function sanitize_columns($columns, $fields = NULL) { + $sanitized = array(); + if ($fields === NULL) { + $fields = $this->displayHandler->getOption('fields'); + } + // Preconfigure the sanitized array so that the order is retained. + foreach ($fields as $field => $info) { + // Set to itself so that if it isn't touched, it gets column + // status automatically. + $sanitized[$field] = $field; + } + + foreach ($columns as $field => $column) { + // first, make sure the field still exists. + if (!isset($sanitized[$field])) { + continue; + } + + // If the field is the column, mark it so, or the column + // it's set to is a column, that's ok + if ($field == $column || $columns[$column] == $column && !empty($sanitized[$column])) { + $sanitized[$field] = $column; + } + // Since we set the field to itself initially, ignoring + // the condition is ok; the field will get its column + // status back. + } + + return $sanitized; + } + + /** + * Render the given style. + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $handlers = $this->displayHandler->getHandlers('field'); + if (empty($handlers)) { + $form['error_markup'] = array( + '#markup' => '<div class="error messages">' . t('You need at least one field before you can configure your table settings') . '</div>', + ); + return; + } + + $form['override'] = array( + '#type' => 'checkbox', + '#title' => t('Override normal sorting if click sorting is used'), + '#default_value' => !empty($this->options['override']), + ); + + $form['sticky'] = array( + '#type' => 'checkbox', + '#title' => t('Enable Drupal style "sticky" table headers (Javascript)'), + '#default_value' => !empty($this->options['sticky']), + '#description' => t('(Sticky header effects will not be active for preview below, only on live output.)'), + ); + + $form['summary'] = array( + '#type' => 'textfield', + '#title' => t('Table summary'), + '#description' => t('This value will be displayed as table-summary attribute in the html. Set this for better accessiblity of your site.'), + '#default_value' => $this->options['summary'], + '#maxlength' => 255, + ); + + // Note: views UI registers this theme handler on our behalf. Your module + // will have to register your theme handlers if you do stuff like this. + $form['#theme'] = 'views_ui_style_plugin_table'; + + $columns = $this->sanitize_columns($this->options['columns']); + + // Create an array of allowed columns from the data we know: + $field_names = $this->displayHandler->getFieldLabels(); + + if (isset($this->options['default'])) { + $default = $this->options['default']; + if (!isset($columns[$default])) { + $default = -1; + } + } + else { + $default = -1; + } + + foreach ($columns as $field => $column) { + $column_selector = ':input[name="style_options[columns][' . $field . ']"]'; + + $form['columns'][$field] = array( + '#type' => 'select', + '#options' => $field_names, + '#default_value' => $column, + ); + if ($handlers[$field]->click_sortable()) { + $form['info'][$field]['sortable'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['info'][$field]['sortable']), + '#states' => array( + 'visible' => array( + $column_selector => array('value' => $field), + ), + ), + ); + $form['info'][$field]['default_sort_order'] = array( + '#type' => 'select', + '#options' => array('asc' => t('Ascending'), 'desc' => t('Descending')), + '#default_value' => !empty($this->options['info'][$field]['default_sort_order']) ? $this->options['info'][$field]['default_sort_order'] : 'asc', + '#states' => array( + 'visible' => array( + $column_selector => array('value' => $field), + ':input[name="style_options[info][' . $field . '][sortable]"]' => array('checked' => TRUE), + ), + ), + ); + // Provide an ID so we can have such things. + $radio_id = drupal_html_id('edit-default-' . $field); + $form['default'][$field] = array( + '#type' => 'radio', + '#return_value' => $field, + '#parents' => array('style_options', 'default'), + '#id' => $radio_id, + // because 'radio' doesn't fully support '#id' =( + '#attributes' => array('id' => $radio_id), + '#default_value' => $default, + '#states' => array( + 'visible' => array( + $column_selector => array('value' => $field), + ), + ), + ); + } + $form['info'][$field]['align'] = array( + '#type' => 'select', + '#default_value' => !empty($this->options['info'][$field]['align']) ? $this->options['info'][$field]['align'] : '', + '#options' => array( + '' => t('None'), + 'views-align-left' => t('Left'), + 'views-align-center' => t('Center'), + 'views-align-right' => t('Right'), + ), + '#states' => array( + 'visible' => array( + $column_selector => array('value' => $field), + ), + ), + ); + $form['info'][$field]['separator'] = array( + '#type' => 'textfield', + '#size' => 10, + '#default_value' => isset($this->options['info'][$field]['separator']) ? $this->options['info'][$field]['separator'] : '', + '#states' => array( + 'visible' => array( + $column_selector => array('value' => $field), + ), + ), + ); + $form['info'][$field]['empty_column'] = array( + '#type' => 'checkbox', + '#default_value' => isset($this->options['info'][$field]['empty_column']) ? $this->options['info'][$field]['empty_column'] : FALSE, + '#states' => array( + 'visible' => array( + $column_selector => array('value' => $field), + ), + ), + ); + $form['info'][$field]['responsive'] = array( + '#type' => 'select', + '#default_value' => isset($this->options['info'][$field]['responsive']) ? $this->options['info'][$field]['responsive'] : '', + '#options' => array('' => t('None'), RESPONSIVE_PRIORITY_MEDIUM => t('Medium'), RESPONSIVE_PRIORITY_LOW => t('Low')), + '#states' => array( + 'visible' => array( + $column_selector => array('value' => $field), + ), + ), + ); + + // markup for the field name + $form['info'][$field]['name'] = array( + '#markup' => $field_names[$field], + ); + } + + // Provide a radio for no default sort + $form['default'][-1] = array( + '#type' => 'radio', + '#return_value' => -1, + '#parents' => array('style_options', 'default'), + '#id' => 'edit-default-0', + '#default_value' => $default, + ); + + $form['empty_table'] = array( + '#type' => 'checkbox', + '#title' => t('Show the empty text in the table'), + '#default_value' => $this->options['empty_table'], + '#description' => t('Per default the table is hidden for an empty view. With this option it is posible to show an empty table with the text in it.'), + ); + + $form['description_markup'] = array( + '#markup' => '<div class="description form-item">' . t('Place fields into columns; you may combine multiple fields into the same column. If you do, the separator in the column specified will be used to separate the fields. Check the sortable box to make that column click sortable, and check the default sort radio to determine which column will be sorted by default, if any. You may control column order and field labels in the fields section.') . '</div>', + ); + } + + function even_empty() { + return parent::even_empty() || !empty($this->options['empty_table']); + } + + function wizard_submit(&$form, &$form_state, WizardInterface $wizard, &$display_options, $display_type) { + // If any of the displays use the table style, take sure that the fields + // always have a labels by unsetting the override. + foreach ($display_options['default']['fields'] as &$field) { + unset($field['label']); + } + } + + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/UnformattedSummary.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/UnformattedSummary.php new file mode 100644 index 0000000..d5ed76f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/UnformattedSummary.php @@ -0,0 +1,49 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\style\UnformattedSummary. + */ + +namespace Drupal\views\Plugin\views\style; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * The default style plugin for summaries. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "unformatted_summary", + * title = @Translation("Unformatted"), + * help = @Translation("Displays the summary unformatted, with option for one after another or inline."), + * theme = "views_view_summary_unformatted", + * type = "summary" + * ) + */ +class UnformattedSummary extends DefaultSummary { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['inline'] = array('default' => FALSE, 'bool' => TRUE); + $options['separator'] = array('default' => ''); + return $options; + } + + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + $form['inline'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['inline']), + '#title' => t('Display items inline'), + ); + $form['separator'] = array( + '#type' => 'textfield', + '#title' => t('Separator'), + '#default_value' => $this->options['separator'], + ); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardException.php b/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardException.php new file mode 100644 index 0000000..e7a70bf --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardException.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\wizard\WizardException. + */ + +namespace Drupal\views\Plugin\views\wizard; + +use Exception; + +/** + * A custom exception class for our errors. + */ +class WizardException extends Exception { + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardInterface.php b/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardInterface.php new file mode 100644 index 0000000..cd340fb --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardInterface.php @@ -0,0 +1,60 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\wizard\WizardInterface. + */ + +namespace Drupal\views\Plugin\views\wizard; + +/** + * Defines a common interface for Views Wizard plugins. + */ +interface WizardInterface { + + /** + * Form callback to build other elements in the "show" form. + * + * This method builds all form elements beside of the selection of the + * base table. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * Returns the changed wizard form. + */ + function build_form(array $form, array &$form_state); + + /** + * Validate form and values. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * An empty array if the view is valid; an array of error strings if it is + * not. + */ + public function validateView(array $form, array &$form_state); + + /** + * Creates a view from values that have already been validated. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return Drupal\views\ViewExecutable + * The created view object. + * + * @throws Drupal\views\Plugin\views\wizard\WizardException + */ + function create_view(array $form, array &$form_state); + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardPluginBase.php new file mode 100644 index 0000000..03f62ee --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/wizard/WizardPluginBase.php @@ -0,0 +1,1088 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Plugin\views\wizard\WizardPluginBase. + */ + +namespace Drupal\views\Plugin\views\wizard; + +use Drupal\views\ViewStorage; +use Drupal\views_ui\ViewUI; +use Drupal\views\Plugin\views\display\DisplayPluginBase; +use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\Plugin\views\wizard\WizardInterface; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * Provides the interface and base class for Views Wizard plugins. + * + * This is a very generic Views Wizard class that can be constructed for any + * base table. + */ +abstract class WizardPluginBase extends PluginBase implements WizardInterface { + + /** + * The base table connected with the wizard. + * + * @var string + */ + protected $base_table; + + /** + * The entity type connected with the wizard. + * + * There might be base tables connected with entity types, if not this would + * be empty. + * + * @var string + */ + protected $entity_type; + + /** + * Contains the information from entity_get_info of the $entity_type. + * + * @var array + */ + protected $entity_info = array(); + + /** + * An array of validated view objects, keyed by a hash. + * + * @var array + */ + protected $validated_views = array(); + + /** + * The table column used for sorting by create date of this wizard. + * + * @var string + */ + protected $createdColumn; + + /** + * A views item configuration array used for a jump-menu field. + * + * @var array + */ + protected $pathField = array(); + + /** + * Additional fields required to generate the pathField. + * + * @var array + */ + protected $pathFieldsSupplemental = array(); + + /** + * Views items configuration arrays for filters added by the wizard. + * + * @var array + */ + protected $filters = array(); + + /** + * Views items configuration arrays for sorts added by the wizard. + * + * @var array + */ + protected $sorts = array(); + + /** + * The available store criteria. + * + * @var array + */ + protected $availableSorts = array(); + + /** + * Default values for filters. + * + * By default, filters are not exposed and added to the first non-reserved + * filter group. + * + * @var array() + */ + protected $filter_defaults = array( + 'id' => NULL, + 'expose' => array('operator' => FALSE), + 'group' => 1, + ); + + /** + * Constructs a WizardPluginBase object. + */ + public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) { + parent::__construct($configuration, $plugin_id, $discovery); + + $this->base_table = $this->definition['base_table']; + + $entities = entity_get_info(); + foreach ($entities as $entity_type => $entity_info) { + if (isset($entity_info['base table']) && $this->base_table == $entity_info['base table']) { + $this->entity_info = $entity_info; + $this->entity_type = $entity_type; + } + } + } + + /** + * Gets the createdColumn property. + * + * @return string + * The name of the column containing the created date. + */ + public function getCreatedColumn() { + return $this->createdColumn; + } + + /** + * Gets the pathField property. + * + * @return array + * The pathField array. + * + * @todo Rename this to be something about jump menus, and/or resolve this + * dependency. + */ + public function getPathField() { + return $this->pathField; + } + + /** + * Gets the pathFieldsSupplemental property. + * + * @return array() + * + * @todo Rename this to be something about jump menus, and/or remove this. + */ + public function getPathFieldsSupplemental() { + return $this->pathFieldsSupplemental; + } + + /** + * Gets the filters property. + * + * @return array + */ + public function getFilters() { + $filters = array(); + + $default = $this->filter_defaults; + + foreach ($this->filters as $name => $info) { + $default['id'] = $name; + $filters[$name] = $info + $default; + } + + return $filters; + } + + /** + * Gets the availableSorts property. + * + * @return array + */ + public function getAvailableSorts() { + return $this->availableSorts; + } + + /** + * Gets the sorts property. + * + * @return array + */ + public function getSorts() { + return $this->sorts; + } + + /** + * Implements Drupal\views\Plugin\views\wizard\WizardInterface::build_form(). + */ + function build_form(array $form, array &$form_state) { + $style_options = views_fetch_plugin_names('style', 'normal', array($this->base_table)); + $feed_row_options = views_fetch_plugin_names('row', 'feed', array($this->base_table)); + $path_prefix = url(NULL, array('absolute' => TRUE)); + + // Add filters and sorts which apply to the view as a whole. + $this->build_filters($form, $form_state); + $this->build_sorts($form, $form_state); + + $form['displays']['page'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('views-attachment', 'fieldset-no-legend')), + '#tree' => TRUE, + ); + $form['displays']['page']['create'] = array( + '#title' => t('Create a page'), + '#type' => 'checkbox', + '#attributes' => array('class' => array('strong')), + '#default_value' => TRUE, + '#id' => 'edit-page-create', + ); + + // All options for the page display are included in this container so they + // can be hidden as a group when the "Create a page" checkbox is unchecked. + $form['displays']['page']['options'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('options-set')), + '#states' => array( + 'visible' => array( + ':input[name="page[create]"]' => array('checked' => TRUE), + ), + ), + '#prefix' => '<div><div id="edit-page-wrapper">', + '#suffix' => '</div></div>', + '#parents' => array('page'), + ); + + $form['displays']['page']['options']['title'] = array( + '#title' => t('Page title'), + '#type' => 'textfield', + ); + $form['displays']['page']['options']['path'] = array( + '#title' => t('Path'), + '#type' => 'textfield', + '#field_prefix' => $path_prefix, + ); + $form['displays']['page']['options']['style'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('container-inline', 'fieldset-no-legend')), + ); + + // Create the dropdown for choosing the display format. + $form['displays']['page']['options']['style']['style_plugin'] = array( + '#title' => t('Display format'), + '#type' => 'select', + '#options' => $style_options, + ); + $style_form = &$form['displays']['page']['options']['style']; + $style_form['style_plugin']['#default_value'] = views_ui_get_selected($form_state, array('page', 'style', 'style_plugin'), 'default', $style_form['style_plugin']); + // Changing this dropdown updates $form['displays']['page']['options'] via + // AJAX. + views_ui_add_ajax_trigger($style_form, 'style_plugin', array('displays', 'page', 'options')); + + $this->build_form_style($form, $form_state, 'page'); + $form['displays']['page']['options']['items_per_page'] = array( + '#title' => t('Items to display'), + '#type' => 'number', + '#default_value' => 10, + '#min' => 0, + ); + $form['displays']['page']['options']['pager'] = array( + '#title' => t('Use a pager'), + '#type' => 'checkbox', + '#default_value' => TRUE, + ); + $form['displays']['page']['options']['link'] = array( + '#title' => t('Create a menu link'), + '#type' => 'checkbox', + '#id' => 'edit-page-link', + ); + $form['displays']['page']['options']['link_properties'] = array( + '#type' => 'container', + '#states' => array( + 'visible' => array( + ':input[name="page[link]"]' => array('checked' => TRUE), + ), + ), + '#prefix' => '<div id="edit-page-link-properties-wrapper">', + '#suffix' => '</div>', + ); + if (module_exists('menu')) { + $menu_options = menu_get_menus(); + } + else { + // These are not yet translated. + $menu_options = menu_list_system_menus(); + foreach ($menu_options as $name => $title) { + $menu_options[$name] = t($title); + } + } + $form['displays']['page']['options']['link_properties']['menu_name'] = array( + '#title' => t('Menu'), + '#type' => 'select', + '#options' => $menu_options, + ); + $form['displays']['page']['options']['link_properties']['title'] = array( + '#title' => t('Link text'), + '#type' => 'textfield', + ); + // Only offer a feed if we have at least one available feed row style. + if ($feed_row_options) { + $form['displays']['page']['options']['feed'] = array( + '#title' => t('Include an RSS feed'), + '#type' => 'checkbox', + '#id' => 'edit-page-feed', + ); + $form['displays']['page']['options']['feed_properties'] = array( + '#type' => 'container', + '#states' => array( + 'visible' => array( + ':input[name="page[feed]"]' => array('checked' => TRUE), + ), + ), + '#prefix' => '<div id="edit-page-feed-properties-wrapper">', + '#suffix' => '</div>', + ); + $form['displays']['page']['options']['feed_properties']['path'] = array( + '#title' => t('Feed path'), + '#type' => 'textfield', + '#field_prefix' => $path_prefix, + ); + // This will almost never be visible. + $form['displays']['page']['options']['feed_properties']['row_plugin'] = array( + '#title' => t('Feed row style'), + '#type' => 'select', + '#options' => $feed_row_options, + '#default_value' => key($feed_row_options), + '#access' => (count($feed_row_options) > 1), + '#states' => array( + 'visible' => array( + ':input[name="page[feed]"]' => array('checked' => TRUE), + ), + ), + '#prefix' => '<div id="edit-page-feed-properties-row-plugin-wrapper">', + '#suffix' => '</div>', + ); + } + + if (!module_exists('block')) { + return $form; + } + + $form['displays']['block'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('views-attachment', 'fieldset-no-legend')), + '#tree' => TRUE, + ); + $form['displays']['block']['create'] = array( + '#title' => t('Create a block'), + '#type' => 'checkbox', + '#attributes' => array('class' => array('strong')), + '#id' => 'edit-block-create', + ); + + // All options for the block display are included in this container so they + // can be hidden as a group when the "Create a page" checkbox is unchecked. + $form['displays']['block']['options'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('options-set')), + '#states' => array( + 'visible' => array( + ':input[name="block[create]"]' => array('checked' => TRUE), + ), + ), + '#prefix' => '<div id="edit-block-wrapper">', + '#suffix' => '</div>', + '#parents' => array('block'), + ); + + $form['displays']['block']['options']['title'] = array( + '#title' => t('Block title'), + '#type' => 'textfield', + ); + $form['displays']['block']['options']['style'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('container-inline', 'fieldset-no-legend')), + ); + + // Create the dropdown for choosing the display format. + $form['displays']['block']['options']['style']['style_plugin'] = array( + '#title' => t('Display format'), + '#type' => 'select', + '#options' => $style_options, + ); + $style_form = &$form['displays']['block']['options']['style']; + $style_form['style_plugin']['#default_value'] = views_ui_get_selected($form_state, array('block', 'style', 'style_plugin'), 'default', $style_form['style_plugin']); + // Changing this dropdown updates $form['displays']['block']['options'] via + // AJAX. + views_ui_add_ajax_trigger($style_form, 'style_plugin', array('displays', 'block', 'options')); + + $this->build_form_style($form, $form_state, 'block'); + $form['displays']['block']['options']['items_per_page'] = array( + '#title' => t('Items per page'), + '#type' => 'number', + '#default_value' => 5, + '#min' => 0, + ); + $form['displays']['block']['options']['pager'] = array( + '#title' => t('Use a pager'), + '#type' => 'checkbox', + '#default_value' => FALSE, + ); + + return $form; + } + + /** + * Adds the style options to the wizard form. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * @param string $type + * The display ID (e.g. 'page' or 'block'). + */ + protected function build_form_style(array &$form, array &$form_state, $type) { + $style_form =& $form['displays'][$type]['options']['style']; + $style = $style_form['style_plugin']['#default_value']; + // @fixme + + $style_plugin = views_get_plugin('style', $style); + if (isset($style_plugin) && $style_plugin->usesRowPlugin()) { + $options = $this->row_style_options(); + $style_form['row_plugin'] = array( + '#type' => 'select', + '#title' => t('of'), + '#options' => $options, + '#access' => count($options) > 1, + ); + // For the block display, the default value should be "titles (linked)", + // if it's available (since that's the most common use case). + $block_with_linked_titles_available = ($type == 'block' && isset($options['titles_linked'])); + $default_value = $block_with_linked_titles_available ? 'titles_linked' : key($options); + $style_form['row_plugin']['#default_value'] = views_ui_get_selected($form_state, array($type, 'style', 'row_plugin'), $default_value, $style_form['row_plugin']); + // Changing this dropdown updates the individual row options via AJAX. + views_ui_add_ajax_trigger($style_form, 'row_plugin', array('displays', $type, 'options', 'style', 'row_options')); + + // This is the region that can be updated by AJAX. The base class doesn't + // add anything here, but child classes can. + $style_form['row_options'] = array( + '#theme_wrappers' => array('container'), + ); + } + elseif ($style_plugin->usesFields()) { + $style_form['row_plugin'] = array('#markup' => '<span>' . t('of fields') . '</span>'); + } + } + + /** + * Retrieves row style plugin names. + * + * @return array + * Returns the plugin names available for the base table of the wizard. + */ + protected function row_style_options() { + // Get all available row plugins by default. + $options = views_fetch_plugin_names('row', 'normal', array($this->base_table)); + return $options; + } + + /** + * Builds the form structure for selecting the view's filters. + * + * By default, this adds "of type" and "tagged with" filters (when they are + * available). + */ + protected function build_filters(&$form, &$form_state) { + // Find all the fields we are allowed to filter by. + $fields = views_fetch_fields($this->base_table, 'filter'); + + $entity_info = $this->entity_info; + // If the current base table support bundles and has more than one (like user). + if (isset($entity_info['bundle keys']) && isset($entity_info['bundles'])) { + // Get all bundles and their human readable names. + $options = array('all' => t('All')); + foreach ($entity_info['bundles'] as $type => $bundle) { + $options[$type] = $bundle['label']; + } + $form['displays']['show']['type'] = array( + '#type' => 'select', + '#title' => t('of type'), + '#options' => $options, + ); + $selected_bundle = views_ui_get_selected($form_state, array('show', 'type'), 'all', $form['displays']['show']['type']); + $form['displays']['show']['type']['#default_value'] = $selected_bundle; + // Changing this dropdown updates the entire content of $form['displays'] + // via AJAX, since each bundle might have entirely different fields + // attached to it, etc. + views_ui_add_ajax_trigger($form['displays']['show'], 'type', array('displays')); + } + } + + /** + * Builds the form structure for selecting the view's sort order. + * + * By default, this adds a "sorted by [date]" filter (when it is available). + */ + protected function build_sorts(&$form, &$form_state) { + $sorts = array( + 'none' => t('Unsorted'), + ); + // Check if we are allowed to sort by creation date. + $created_column = $this->getCreatedColumn(); + if ($created_column) { + $sorts += array( + $created_column . ':DESC' => t('Newest first'), + $created_column . ':ASC' => t('Oldest first'), + ); + } + if ($available_sorts = $this->getAvailableSorts()) { + $sorts += $available_sorts; + } + + foreach ($sorts as &$option) { + if (is_object($option)) { + $option = $option->get(); + } + } + + // If there is no sorts option available continue. + if (!empty($sorts)) { + $form['displays']['show']['sort'] = array( + '#type' => 'select', + '#title' => t('sorted by'), + '#options' => $sorts, + '#default_value' => isset($created_column) ? $created_column . ':DESC' : 'none', + ); + } + } + + /** + * Instantiates a view object from form values. + * + * @return Drupal\views_ui\ViewUI + * The instantiated view UI object. + */ + protected function instantiate_view($form, &$form_state) { + // Build the basic view properties and create the view. + $values = array( + 'name' => $form_state['values']['name'], + 'human_name' => $form_state['values']['human_name'], + 'description' => $form_state['values']['description'], + 'base_table' => $this->base_table, + ); + + $view = views_create_view($values); + + // Build all display options for this view. + $display_options = $this->build_display_options($form, $form_state); + + // Allow the fully built options to be altered. This happens before adding + // the options to the view, so that once they are eventually added we will + // be able to get all the overrides correct. + $this->alter_display_options($display_options, $form, $form_state); + + $this->addDisplays($view, $display_options, $form, $form_state); + + return new ViewUI($view); + } + + /** + * Builds an array of display options for the view. + * + * @return array + * An array whose keys are the names of each display and whose values are + * arrays of options for that display. + */ + protected function build_display_options($form, $form_state) { + // Display: Master + $display_options['default'] = $this->default_display_options(); + $display_options['default'] += array( + 'filters' => array(), + 'sorts' => array(), + ); + $display_options['default']['filters'] += $this->default_display_filters($form, $form_state); + $display_options['default']['sorts'] += $this->default_display_sorts($form, $form_state); + + // Display: Page + if (!empty($form_state['values']['page']['create'])) { + $display_options['page'] = $this->page_display_options($form, $form_state); + + // Display: Feed (attached to the page) + if (!empty($form_state['values']['page']['feed'])) { + $display_options['feed'] = $this->page_feed_display_options($form, $form_state); + } + } + + // Display: Block + if (!empty($form_state['values']['block']['create'])) { + $display_options['block'] = $this->block_display_options($form, $form_state); + } + + return $display_options; + } + + /** + * Alters the full array of display options before they are added to the view. + */ + protected function alter_display_options(&$display_options, $form, $form_state) { + foreach ($display_options as $display_type => $options) { + // Allow style plugins to hook in and provide some settings. + $style_plugin = views_get_plugin('style', $options['style']['type']); + $style_plugin->wizard_submit($form, $form_state, $this, $display_options, $display_type); + } + } + + /** + * Adds the array of display options to the view, with appropriate overrides. + */ + protected function addDisplays(ViewStorage $view, $display_options, $form, $form_state) { + // Display: Master + $default_display = $view->newDisplay('default', 'Master', 'default'); + foreach ($display_options['default'] as $option => $value) { + $default_display->setOption($option, $value); + } + + // Display: Page + if (isset($display_options['page'])) { + $display = $view->newDisplay('page', 'Page', 'page'); + // The page display is usually the main one (from the user's point of + // view). Its options should therefore become the overall view defaults, + // so that new displays which are added later automatically inherit them. + $this->setDefaultOptions($display_options['page'], $display, $default_display); + + // Display: Feed (attached to the page). + if (isset($display_options['feed'])) { + $display = $view->newDisplay('feed', 'Feed', 'feed'); + $this->set_override_options($display_options['feed'], $display, $default_display); + } + } + + // Display: Block. + if (isset($display_options['block'])) { + $display = $view->newDisplay('block', 'Block', 'block'); + // When there is no page, the block display options should become the + // overall view defaults. + if (!isset($display_options['page'])) { + $this->setDefaultOptions($display_options['block'], $display, $default_display); + } + else { + $this->set_override_options($display_options['block'], $display, $default_display); + } + } + } + + /** + * Assembles the default display options for the view. + * + * Most wizards will need to override this method to provide some fields + * or a different row plugin. + * + * @return array + * Returns an array of display options. + */ + protected function default_display_options() { + $display_options = array(); + $display_options['access']['type'] = 'none'; + $display_options['cache']['type'] = 'none'; + $display_options['query']['type'] = 'views_query'; + $display_options['exposed_form']['type'] = 'basic'; + $display_options['pager']['type'] = 'full'; + $display_options['style']['type'] = 'default'; + $display_options['row']['type'] = 'fields'; + + // Add a least one field so the view validates and the user has a preview. + // The base field can provide a default in its base settings; otherwise, + // choose the first field with a field handler. + $data = views_fetch_data($this->base_table); + if (isset($data['table']['base']['defaults']['field'])) { + $field = $data['table']['base']['defaults']['field']; + } + else { + foreach ($data as $field => $field_data) { + if (isset($field_data['field']['id'])) { + break; + } + } + } + $display_options['fields'][$field] = array( + 'table' => $this->base_table, + 'field' => $field, + 'id' => $field, + ); + + return $display_options; + } + + /** + * Retrieves all filter information used by the default display. + * + * Additional to the one provided by the plugin this method takes care about + * adding additional filters based on user input. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * An array of filter arrays keyed by ID. A sort array contains the options + * accepted by a filter handler. + */ + protected function default_display_filters($form, $form_state) { + $filters = array(); + + // Add any filters provided by the plugin. + foreach ($this->getFilters() as $name => $info) { + $filters[$name] = $info; + } + + // Add any filters specified by the user when filling out the wizard. + $filters = array_merge($filters, $this->default_display_filters_user($form, $form_state)); + + return $filters; + } + + /** + * Retrieves filter information based on user input for the default display. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * An array of filter arrays keyed by ID. A sort array contains the options + * accepted by a filter handler. + */ + protected function default_display_filters_user(array $form, array &$form_state) { + $filters = array(); + + if (!empty($form_state['values']['show']['type']) && $form_state['values']['show']['type'] != 'all') { + $bundle_key = $this->entity_info['bundle keys']['bundle']; + // Figure out the table where $bundle_key lives. It may not be the same as + // the base table for the view; the taxonomy vocabulary machine_name, for + // example, is stored in taxonomy_vocabulary, not taxonomy_term_data. + $fields = views_fetch_fields($this->base_table, 'filter'); + if (isset($fields[$this->base_table . '.' . $bundle_key])) { + $table = $this->base_table; + } + else { + foreach ($fields as $field_name => $value) { + if ($pos = strpos($field_name, '.' . $bundle_key)) { + $table = substr($field_name, 0, $pos); + break; + } + } + } + $table_data = views_fetch_data($table); + // If the 'in' operator is being used, map the values to an array. + $handler = $table_data[$bundle_key]['filter']['id']; + $handler_definition = views_get_plugin_definition('filter', $handler); + if ($handler == 'in_operator' || is_subclass_of($handler_definition['class'], 'Drupal\\views\\Plugin\\views\\filter\\InOperator')) { + $value = drupal_map_assoc(array($form_state['values']['show']['type'])); + } + // Otherwise, use just a single value. + else { + $value = $form_state['values']['show']['type']; + } + + $filters[$bundle_key] = array( + 'id' => $bundle_key, + 'table' => $table, + 'field' => $bundle_key, + 'value' => $value, + ); + } + + return $filters; + } + + /** + * Retrieves all sort information used by the default display. + * + * Additional to the one provided by the plugin this method takes care about + * adding additional sorts based on user input. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * An array of sort arrays keyed by ID. A sort array contains the options + * accepted by a sort handler. + */ + protected function default_display_sorts($form, $form_state) { + $sorts = array(); + + // Add any sorts provided by the plugin. + foreach ($this->getSorts() as $name => $info) { + $sorts[$name] = $info; + } + + // Add any sorts specified by the user when filling out the wizard. + $sorts = array_merge($sorts, $this->default_display_sorts_user($form, $form_state)); + + return $sorts; + } + + /** + * Retrieves sort information based on user input for the default display. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * An array of sort arrays keyed by ID. A sort array contains the options + * accepted by a sort handler. + */ + protected function default_display_sorts_user($form, $form_state) { + $sorts = array(); + + // Don't add a sort if there is no form value or the user set the sort to + // 'none'. + if (!empty($form_state['values']['show']['sort']) && $form_state['values']['show']['sort'] != 'none') { + list($column, $sort) = explode(':', $form_state['values']['show']['sort']); + // Column either be a column-name or the table-columnn-ame. + $column = explode('-', $column); + if (count($column) > 1) { + $table = $column[0]; + $column = $column[1]; + } + else { + $table = $this->base_table; + $column = $column[0]; + } + + // If the input is invalid, for example when the #default_value contains + // created from node, but the wizard type is another base table, make + // sure it is not added. This usually don't happen if you have js + // enabled. + $data = views_fetch_data($table); + if (isset($data[$column]['sort'])) { + $sorts[$column] = array( + 'id' => $column, + 'table' => $table, + 'field' => $column, + 'order' => $sort, + ); + } + } + + return $sorts; + } + + /** + * Retrieves the page display options. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * Returns an array of display options. + */ + protected function page_display_options(array $form, array &$form_state) { + $display_options = array(); + $page = $form_state['values']['page']; + $display_options['title'] = $page['title']; + $display_options['path'] = $page['path']; + $display_options['style'] = array('type' => $page['style']['style_plugin']); + // Not every style plugin supports row style plugins. + // Make sure that the selected row plugin is a valid one. + $options = $this->row_style_options(); + $display_options['row'] = array('type' => (isset($page['style']['row_plugin']) && isset($options[$page['style']['row_plugin']])) ? $page['style']['row_plugin'] : 'fields'); + + // If the specific 0 items per page, use no pager. + if (empty($page['items_per_page'])) { + $display_options['pager']['type'] = 'none'; + } + // If the user checked the pager checkbox use a full pager. + elseif (isset($page['pager'])) { + $display_options['pager']['type'] = 'full'; + } + // If the user doesn't have checked the checkbox use the pager which just + // displays a certain amount of items. + else { + $display_options['pager']['type'] = 'some'; + } + $display_options['pager']['options']['items_per_page'] = $page['items_per_page']; + + // Generate the menu links settings if the user checked the link checkbox. + if (!empty($page['link'])) { + $display_options['menu']['type'] = 'normal'; + $display_options['menu']['title'] = $page['link_properties']['title']; + $display_options['menu']['name'] = $page['link_properties']['menu_name']; + } + return $display_options; + } + + /** + * Retrieves the block display options. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * Returns an array of display options. + */ + protected function block_display_options(array $form, array &$form_state) { + $display_options = array(); + $block = $form_state['values']['block']; + $display_options['title'] = $block['title']; + $display_options['style'] = array('type' => $block['style']['style_plugin']); + $display_options['row'] = array('type' => isset($block['style']['row_plugin']) ? $block['style']['row_plugin'] : 'fields'); + $display_options['pager']['type'] = $block['pager'] ? 'full' : (empty($block['items_per_page']) ? 'none' : 'some'); + $display_options['pager']['options']['items_per_page'] = $block['items_per_page']; + return $display_options; + } + + /** + * Retrieves the feed display options. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * + * @return array + * Returns an array of display options. + */ + protected function page_feed_display_options($form, $form_state) { + $display_options = array(); + $display_options['pager']['type'] = 'some'; + $display_options['style'] = array('type' => 'rss'); + $display_options['row'] = array('type' => $form_state['values']['page']['feed_properties']['row_plugin']); + $display_options['path'] = $form_state['values']['page']['feed_properties']['path']; + $display_options['title'] = $form_state['values']['page']['title']; + $display_options['displays'] = array( + 'default' => 'default', + 'page' => 'page', + ); + return $display_options; + } + + /** + * Sets options for a display and makes them the default options if possible. + * + * This function can be used to set options for a display when it is desired + * that the options also become the defaults for the view whenever possible. + * This should be done for the "primary" display created in the view wizard, + * so that new displays which the user adds later will be similar to this + * one. + * + * @param array $options + * An array whose keys are the name of each option and whose values are the + * desired values to set. + * @param Drupal\views\View\plugin\display\DisplayPluginBase $display + * The display handler which the options will be applied to. The default + * display will actually be assigned the options (and this display will + * inherit them) when possible. + * @param Drupal\views\View\plugin\display\DisplayPluginBase $default_display + * The default display handler, which will store the options when possible. + */ + protected function setDefaultOptions($options, DisplayPluginBase $display, DisplayPluginBase $default_display) { + foreach ($options as $option => $value) { + // If the default display supports this option, set the value there. + // Otherwise, set it on the provided display. + $default_value = $default_display->getOption($option); + if (isset($default_value)) { + $default_display->setOption($option, $value); + } + else { + $display->setOption($option, $value); + } + } + } + + /** + * Sets options for a display, inheriting from the defaults when possible. + * + * This function can be used to set options for a display when it is desired + * that the options inherit from the default display whenever possible. This + * avoids setting too many options as overrides, which will be harder for the + * user to modify later. For example, if $this->setDefaultOptions() was + * previously called on a page display and then this function is called on a + * block display, and if the user entered the same title for both displays in + * the views wizard, then the view will wind up with the title stored as the + * default (with the page and block both inheriting from it). + * + * @param array $options + * An array whose keys are the name of each option and whose values are the + * desired values to set. + * @param Drupal\views\View\plugin\display\DisplayPluginBase $display + * The display handler which the options will be applied to. The default + * display will actually be assigned the options (and this display will + * inherit them) when possible. + * @param Drupal\views\View\plugin\display\DisplayPluginBase $default_display + * The default display handler, which will store the options when possible. + */ + protected function set_override_options(array $options, DisplayPluginBase $display, DisplayPluginBase $default_display) { + foreach ($options as $option => $value) { + // Only override the default value if it is different from the value that + // was provided. + $default_value = $default_display->getOption($option); + if (!isset($default_value)) { + $display->setOption($option, $value); + } + elseif ($default_value !== $value) { + $display->overrideOption($option, $value); + } + } + } + + /** + * Retrieves a validated view for a form submission. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * @param bool $unset + * Should the view be removed from the list of validated views. + * + * @return Drupal\views_ui\ViewUI $view + * The validated view object. + */ + protected function retrieve_validated_view(array $form, array &$form_state, $unset = TRUE) { + // @todo Figure out why all this hashing is done. Wouldn't it be easier to + // store a single entry and that's it? + $key = hash('sha256', serialize($form_state['values'])); + $view = (isset($this->validated_views[$key]) ? $this->validated_views[$key] : NULL); + if ($unset) { + unset($this->validated_views[$key]); + } + return $view; + } + + /** + * Stores a validated view from a form submission. + * + * @param array $form + * The full wizard form array. + * @param array $form_state + * The current state of the wizard form. + * @param Drupal\views_ui\ViewUI $view + * The validated view object. + */ + protected function set_validated_view(array $form, array &$form_state, ViewUI $view) { + $key = hash('sha256', serialize($form_state['values'])); + $this->validated_views[$key] = $view; + } + + /** + * Implements Drupal\views\Plugin\views\wizard\WizardInterface::validate(). + * + * Instantiates the view from the form submission and validates its values. + */ + public function validateView(array $form, array &$form_state) { + $view = $this->instantiate_view($form, $form_state); + $errors = $view->validate(); + if (!is_array($errors) || empty($errors)) { + $this->set_validated_view($form, $form_state, $view); + return array(); + } + return $errors; + } + + /** + * Implements Drupal\views\Plugin\views\wizard\WizardInterface::create_view(). + */ + function create_view(array $form, array &$form_state) { + $view = $this->retrieve_validated_view($form, $form_state); + if (empty($view)) { + throw new WizardException('Attempted to create_view with values that have not been validated.'); + } + return $view; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/AnalyzeTest.php b/core/modules/views/lib/Drupal/views/Tests/AnalyzeTest.php new file mode 100644 index 0000000..f5497f2 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/AnalyzeTest.php @@ -0,0 +1,56 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\AnalyzeTest. + */ + +namespace Drupal\views\Tests; + +/** + * Tests the views analyze system. + */ +class AnalyzeTest extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + public static function getInfo() { + return array( + 'name' => 'Views Analyze', + 'description' => 'Tests the views analyze system.', + 'group' => 'Views', + ); + } + + public function setUp() { + parent::setUp(); + + // Add an admin user will full rights; + $this->admin = $this->drupalCreateUser(array('administer views')); + } + + /** + * Tests that analyze works in general. + */ + function testAnalyzeBasic() { + $this->drupalLogin($this->admin); + // Enable the frontpage view and click the analyse button. + $view = views_get_view('frontpage'); + + $this->drupalGet('admin/structure/views/view/frontpage/edit'); + $this->assertLink(t('analyze view')); + + // This redirects the user to the form. + $this->clickLink(t('analyze view')); + $this->assertText(t('View analysis')); + + // This redirects the user back to the main views edit page. + $this->drupalPost(NULL, array(), t('Ok')); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/BasicTest.php b/core/modules/views/lib/Drupal/views/Tests/BasicTest.php new file mode 100644 index 0000000..b9c1d35 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/BasicTest.php @@ -0,0 +1,190 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\BasicTest. + */ + +namespace Drupal\views\Tests; + +/** + * Basic test class for Views query builder tests. + */ +class BasicTest extends ViewTestBase { + + public static function getInfo() { + return array( + 'name' => 'Basic query tests', + 'description' => 'A basic query test for Views.', + 'group' => 'Views' + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Tests a trivial result set. + */ + public function testSimpleResultSet() { + $view = $this->getView(); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->dataSet(), array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + } + + /** + * Tests filtering of the result set. + */ + public function testSimpleFiltering() { + $view = $this->getView(); + + // Add a filter. + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'operator' => '<', + 'value' => array( + 'value' => '28', + 'min' => '', + 'max' => '', + ), + 'group' => '0', + 'exposed' => FALSE, + 'expose' => array( + 'operator' => FALSE, + 'label' => '', + ), + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Build the expected result. + $dataset = array( + array( + 'id' => 1, + 'name' => 'John', + 'age' => 25, + ), + array( + 'id' => 2, + 'name' => 'George', + 'age' => 27, + ), + array( + 'id' => 4, + 'name' => 'Paul', + 'age' => 26, + ), + ); + + // Verify the result. + $this->assertEqual(3, count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultSet($view, $dataset, array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + } + + /** + * Tests simple argument. + */ + public function testSimpleArgument() { + $view = $this->getView(); + + // Add a argument. + $view->displayHandlers['default']->overrideOption('arguments', array( + 'age' => array( + 'default_action' => 'ignore', + 'style_plugin' => 'default_summary', + 'style_options' => array(), + 'wildcard' => 'all', + 'wildcard_substitution' => 'All', + 'title' => '', + 'breadcrumb' => '', + 'default_argument_type' => 'fixed', + 'default_argument' => '', + 'validate' => array( + 'type' => 'none', + 'fail' => 'not found', + ), + 'break_phrase' => 0, + 'not' => 0, + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'validate_user_argument_type' => 'uid', + 'validate_user_roles' => array( + '2' => 0, + ), + 'relationship' => 'none', + 'default_options_div_prefix' => '', + 'default_argument_user' => 0, + 'default_argument_fixed' => '', + 'default_argument_php' => '', + 'validate_argument_node_type' => array( + 'page' => 0, + 'story' => 0, + ), + 'validate_argument_node_access' => 0, + 'validate_argument_nid_type' => 'nid', + 'validate_argument_vocabulary' => array(), + 'validate_argument_type' => 'tid', + 'validate_argument_transform' => 0, + 'validate_user_restrict_roles' => 0, + 'validate_argument_php' => '', + ) + )); + + $saved_view = clone $view; + + // Execute with a view + $view->setArguments(array(27)); + $this->executeView($view); + + // Build the expected result. + $dataset = array( + array( + 'id' => 2, + 'name' => 'George', + 'age' => 27, + ), + ); + + // Verify the result. + $this->assertEqual(1, count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultSet($view, $dataset, array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + + // Test "show all" if no argument is present. + $view = $saved_view->cloneView(); + $this->executeView($view); + + // Build the expected result. + $dataset = $this->dataSet(); + + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultSet($view, $dataset, array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Comment/ArgumentUserUIDTest.php b/core/modules/views/lib/Drupal/views/Tests/Comment/ArgumentUserUIDTest.php new file mode 100644 index 0000000..8410b5d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Comment/ArgumentUserUIDTest.php @@ -0,0 +1,37 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Comment\ArgumentUserUIDTest. + */ + +namespace Drupal\views\Tests\Comment; + +/** + * Tests the argument_comment_user_uid handler. + */ +class ArgumentUserUIDTest extends CommentTestBase { + + public static function getInfo() { + return array( + 'name' => 'Comment: User UID Argument', + 'description' => 'Tests the user posted or commented argument handler.', + 'group' => 'Views Modules', + ); + } + + function testCommentUserUIDTest() { + $this->executeView($this->view, array($this->account->uid)); + $result_set = array( + array( + 'nid' => $this->node_user_posted->nid, + ), + array( + 'nid' => $this->node_user_commented->nid, + ), + ); + $this->column_map = array('nid' => 'nid'); + $this->assertIdenticalResultset($this->view, $result_set, $this->column_map); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Comment/CommentTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Comment/CommentTestBase.php new file mode 100644 index 0000000..a855afe --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Comment/CommentTestBase.php @@ -0,0 +1,52 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Comment\CommentTestBase. + */ + +namespace Drupal\views\Tests\Comment; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Tests the argument_comment_user_uid handler. + */ +abstract class CommentTestBase extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('comment'); + + function setUp() { + parent::setUp(); + + // Add two users, create a node with the user1 as author and another node + // with user2 as author. For the second node add a comment from user1. + $this->account = $this->drupalCreateUser(); + $this->account2 = $this->drupalCreateUser(); + $this->drupalLogin($this->account); + + $this->node_user_posted = $this->drupalCreateNode(); + $this->node_user_commented = $this->drupalCreateNode(array('uid' => $this->account2->uid)); + + $comment = array( + 'uid' => $this->loggedInUser->uid, + 'nid' => $this->node_user_commented->nid, + 'cid' => '', + 'pid' => '', + ); + entity_create('comment', $comment)->save(); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_comment_user_uid'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Comment/DefaultViewRecentComments.php b/core/modules/views/lib/Drupal/views/Tests/Comment/DefaultViewRecentComments.php new file mode 100644 index 0000000..73ddfe0 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Comment/DefaultViewRecentComments.php @@ -0,0 +1,158 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Comment\DefaultViewRecentComments. + */ + +namespace Drupal\views\Tests\Comment; + +use Drupal\entity\DatabaseStorageController; +use Drupal\views\Tests\ViewTestBase; + +class DefaultViewRecentComments extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('comment', 'block'); + + /** + * Number of results for the Master display. + * + * @var int + */ + protected $masterDisplayResults = 5; + + /** + * Number of results for the Block display. + * + * @var int + */ + protected $blockDisplayResults = 5; + + /** + * Number of results for the Page display. + * + * @var int + */ + protected $pageDisplayResults = 5; + + /** + * Will hold the comments created for testing. + * + * @var array + */ + protected $commentsCreated = array(); + + /** + * Contains the node object used for comments of this test. + * + * @var Drupal\node\Node + */ + public $node; + + public static function getInfo() { + return array( + 'name' => 'Default View - Recent Comments', + 'description' => 'Test results for the Recent Comments view shipped with the module', + 'group' => 'Views Config', + ); + } + + public function setUp() { + parent::setUp(); + + // Create a new content type + $content_type = $this->drupalCreateContentType(); + + // Add a node of the new content type. + $node_data = array( + 'type' => $content_type->type, + ); + + $this->node = $this->drupalCreateNode($node_data); + + // Create some comments and attach them to the created node. + for ($i = 0; $i < $this->masterDisplayResults; $i++) { + $comment = entity_create('comment', array()); + $comment->uid = 0; + $comment->nid = $this->node->nid; + $comment->subject = 'Test comment ' . $i; + $comment->node_type = 'comment_node_' . $this->node->type; + $comment->comment_body[LANGUAGE_NOT_SPECIFIED][0]['value'] = 'Test body ' . $i; + $comment->comment_body[LANGUAGE_NOT_SPECIFIED][0]['format'] = 'full_html'; + + comment_save($comment); + } + + // Store all the nodes just created to access their properties on the tests. + $this->commentsCreated = entity_load_multiple('comment'); + } + + /** + * Tests the block defined by the comments_recent view. + */ + public function testBlockDisplay() { + $view = views_get_view('comments_recent'); + $view->setDisplay('block'); + $this->executeView($view); + + $map = array( + 'comment_nid' => 'nid', + 'comment_subject' => 'subject', + 'cid' => 'cid', + 'comment_changed' => 'changed' + ); + $expected_result = array(); + foreach (array_values($this->commentsCreated) as $key => $comment) { + $expected_result[$key]['nid'] = $comment->nid; + $expected_result[$key]['subject'] = $comment->subject; + $expected_result[$key]['cid'] = $comment->cid; + $expected_result[$key]['changed'] = $comment->changed; + } + $this->assertIdenticalResultset($view, $expected_result, $map); + + // Check the number of results given by the display is the expected. + $this->assertEqual(sizeof($view->result), $this->blockDisplayResults, + format_string('There are exactly @results comments. Expected @expected', + array('@results' => count($view->result), '@expected' => $this->blockDisplayResults) + ) + ); + } + + /** + * Tests the page defined by the comments_recent view. + */ + public function testPageDisplay() { + $view = views_get_view('comments_recent'); + $view->setDisplay('page'); + $this->executeView($view); + + $map = array( + 'comment_nid' => 'nid', + 'comment_subject' => 'subject', + 'comment_changed' => 'changed', + 'comment_changed' => 'created', + 'cid' => 'cid' + ); + $expected_result = array(); + foreach (array_values($this->commentsCreated) as $key => $comment) { + $expected_result[$key]['nid'] = $comment->nid; + $expected_result[$key]['subject'] = $comment->subject; + $expected_result[$key]['changed'] = $comment->changed; + $expected_result[$key]['created'] = $comment->created; + $expected_result[$key]['cid'] = $comment->cid; + } + $this->assertIdenticalResultset($view, $expected_result, $map); + + // Check the number of results given by the display is the expected. + $this->assertEqual(count($view->result), $this->pageDisplayResults, + format_string('There are exactly @results comments. Expected @expected', + array('@results' => count($view->result), '@expected' => $this->pageDisplayResults) + ) + ); + } +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Comment/FilterUserUIDTest.php b/core/modules/views/lib/Drupal/views/Tests/Comment/FilterUserUIDTest.php new file mode 100644 index 0000000..00ba08c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Comment/FilterUserUIDTest.php @@ -0,0 +1,48 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Comment\FilterUserUIDTest. + */ + +namespace Drupal\views\Tests\Comment; + +/** + * Tests the filter_comment_user_uid handler. + * + * The actual stuff is done in the parent class. + */ +class FilterUserUIDTest extends CommentTestBase { + + public static function getInfo() { + return array( + 'name' => 'Comment: User UID Filter', + 'description' => 'Tests the user posted or commented filter handler.', + 'group' => 'Views Modules', + ); + } + + function testCommentUserUIDTest() { + $this->view->setItem('default', 'argument', 'uid_touch', NULL); + + $options = array( + 'id' => 'uid_touch', + 'table' => 'node', + 'field' => 'uid_touch', + 'value' => array($this->loggedInUser->uid), + ); + $this->view->addItem('default', 'filter', 'node', 'uid_touch', $options); + $this->executeView($this->view, array($this->account->uid)); + $result_set = array( + array( + 'nid' => $this->node_user_posted->nid, + ), + array( + 'nid' => $this->node_user_commented->nid, + ), + ); + $this->column_map = array('nid' => 'nid'); + $this->assertIdenticalResultset($this->view, $result_set, $this->column_map); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Comment/WizardTest.php b/core/modules/views/lib/Drupal/views/Tests/Comment/WizardTest.php new file mode 100644 index 0000000..1a64d99 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Comment/WizardTest.php @@ -0,0 +1,90 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Comment\WizardTest. + */ + +namespace Drupal\views\Tests\Comment; + +use Drupal\views\Tests\Wizard\WizardTestBase; + +/** + * Tests the comment module integration into the wizard. + * + * @see Views\comment\Plugin\views\wizard\Comment + */ +class WizardTest extends WizardTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('comment'); + + + public static function getInfo() { + return array( + 'name' => 'Comment: Wizard', + 'description' => 'Tests the comment module integration into the wizard.', + 'group' => 'Views Wizard', + ); + } + + /** + * Tests adding a view of comments. + */ + public function testCommentWizard() { + $view = array(); + $view['human_name'] = $this->randomName(16); + $view['name'] = strtolower($this->randomName(16)); + $view['show[wizard_key]'] = 'comment'; + $view['page[path]'] = $this->randomName(16); + + // Just triggering the saving should automatically choose a proper row + // plugin. + $this->drupalPost('admin/structure/views/add', $view, t('Continue & edit')); + $this->assertUrl('admin/structure/views/view/' . $view['name'], array(), 'Make sure the view saving was successful and the browser got redirected to the edit page.'); + + // If we update the type first we should get a selection of comment valid + // row plugins as the select field. + + $this->drupalGet('admin/structure/views/add'); + $this->drupalPost('admin/structure/views/add', $view, t('Update "of type" choice')); + + // Check for available options of the row plugin. + $xpath = $this->constructFieldXpath('name', 'page[style][row_plugin]'); + $fields = $this->xpath($xpath); + $options = array(); + foreach ($fields as $field) { + $items = $this->getAllOptions($field); + foreach ($items as $item) { + $options[] = $item->attributes()->value; + } + } + $expected_options = array('comment', 'fields'); + $this->assertEqual($options, $expected_options); + $this->drupalPost(NULL, $view, t('Continue & edit')); + $this->assertUrl('admin/structure/views/view/' . $view['name'], array(), 'Make sure the view saving was successful and the browser got redirected to the edit page.'); + $this->drupalPost(NULL, array(), t('Save')); + + $view = views_get_view($view['name']); + $view->initHandlers(); + $row = $view->display_handler->getOption('row'); + $this->assertEqual($row['type'], 'comment'); + + // Check for the default filters. + $this->assertEqual($view->filter['status']->table, 'comment'); + $this->assertEqual($view->filter['status']->field, 'status'); + $this->assertTrue($view->filter['status']->value); + $this->assertEqual($view->filter['status_node']->table, 'node'); + $this->assertEqual($view->filter['status_node']->field, 'status'); + $this->assertTrue($view->filter['status_node']->value); + + // Check for the default fields. + $this->assertEqual($view->field['subject']->table, 'comment'); + $this->assertEqual($view->field['subject']->field, 'subject'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/DefaultViewsTest.php b/core/modules/views/lib/Drupal/views/Tests/DefaultViewsTest.php new file mode 100644 index 0000000..126f4a3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/DefaultViewsTest.php @@ -0,0 +1,164 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\DefaultViewsTest. + */ + +namespace Drupal\views\Tests; + +use Drupal\simpletest\WebTestBase; +use Drupal\views\ViewExecutable; + +/** + * Tests for views default views. + */ +class DefaultViewsTest extends WebTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views', 'node', 'search', 'comment', 'taxonomy', 'block'); + + /** + * An array of argument arrays to use for default views. + * + * @var array + */ + protected $viewArgMap = array( + 'backlinks' => array(1), + 'taxonomy_term' => array(1), + 'glossary' => array('all'), + ); + + public static function getInfo() { + return array( + 'name' => 'Default views', + 'description' => 'Tests the default views provided by views', + 'group' => 'Views', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->vocabulary = entity_create('taxonomy_vocabulary', array( + 'name' => $this->randomName(), + 'description' => $this->randomName(), + 'machine_name' => drupal_strtolower($this->randomName()), + 'langcode' => LANGUAGE_NOT_SPECIFIED, + 'help' => '', + 'nodes' => array('page' => 'page'), + 'weight' => mt_rand(0, 10), + )); + taxonomy_vocabulary_save($this->vocabulary); + + // Setup a field and instance. + $this->field_name = drupal_strtolower($this->randomName()); + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'taxonomy_term_reference', + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary->machine_name, + 'parent' => '0', + ), + ), + ) + ); + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'node', + 'bundle' => 'page', + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'full' => array( + 'type' => 'taxonomy_term_reference_link', + ), + ), + ); + field_create_instance($this->instance); + + // Create a time in the past for the archive. + $time = time() - 3600; + + for ($i = 0; $i <= 10; $i++) { + $user = $this->drupalCreateUser(); + $term = $this->createTerm($this->vocabulary); + + $values = array('created' => $time, 'type' => 'page'); + $values[$this->field_name][LANGUAGE_NOT_SPECIFIED][]['tid'] = $term->tid; + + // Make every other node promoted. + if ($i % 2) { + $values['promote'] = TRUE; + } + $values['body'][LANGUAGE_NOT_SPECIFIED][]['value'] = l('Node ' . 1, 'node/' . 1); + + $node = $this->drupalCreateNode($values); + + search_index($node->nid, 'node', $node->body[LANGUAGE_NOT_SPECIFIED][0]['value'], LANGUAGE_NOT_SPECIFIED); + + $comment = array( + 'uid' => $user->uid, + 'nid' => $node->nid, + ); + entity_create('comment', $comment)->save(); + } + } + + /** + * Test that all Default views work as expected. + */ + public function testDefaultViews() { + // Get all default views. + $controller = entity_get_controller('view'); + $views = $controller->load(); + + foreach ($views as $name => $view_storage) { + $view = new ViewExecutable($view_storage); + $view->initDisplay(); + foreach ($view->storage->display as $display_id => $display) { + $view->setDisplay($display_id); + + // Add any args if needed. + if (array_key_exists($name, $this->viewArgMap)) { + $view->preExecute($this->viewArgMap[$name]); + } + + $this->assert(TRUE, format_string('View @view will be executed.', array('@view' => $view->storage->name))); + $view->execute(); + + $tokens = array('@name' => $name, '@display_id' => $display_id); + $this->assertTrue($view->executed, format_string('@name:@display_id has been executed.', $tokens)); + + $count = count($view->result); + $this->assertTrue($count > 0, format_string('@count results returned', array('@count' => $count))); + $view->destroy(); + } + } + } + + /** + * Returns a new term with random properties in vocabulary $vid. + */ + function createTerm($vocabulary) { + $term = entity_create('taxonomy_term', array( + 'name' => $this->randomName(), + 'description' => $this->randomName(), + // Use the first available text format. + 'format' => db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField(), + 'vid' => $vocabulary->vid, + 'langcode' => LANGUAGE_NOT_SPECIFIED, + )); + taxonomy_term_save($term); + return $term; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Entity/FieldEntityTest.php b/core/modules/views/lib/Drupal/views/Tests/Entity/FieldEntityTest.php new file mode 100644 index 0000000..a950621 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Entity/FieldEntityTest.php @@ -0,0 +1,62 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Entity\FieldEntityTest. + */ + +namespace Drupal\views\Tests\Entity; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Tests the field plugin base integration with the entity system. + */ +class FieldEntityTest extends ViewTestBase { + + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('comment'); + + public static function getInfo() { + return array( + 'name' => 'Field: Entity Api Integration', + 'description' => 'Tests the field plugin base integration with the entity system.', + 'group' => 'Views Modules', + ); + } + + /** + * Tests the get_entity method. + */ + public function testGetEntity() { + // The view is a view of comments, their nodes and their authors, so there + // are three layers of entities. + + $account = entity_create('user', array('name' => $this->randomName(), 'bundle' => 'user')); + $account->save(); + $node = entity_create('node', array('uid' => $account->id(), 'type' => 'page')); + $node->save(); + $comment = entity_create('comment', array('uid' => $account->id(), 'nid' => $node->id(), 'node_type' => 'comment_node_page')); + $comment->save(); + + $view = views_get_view('test_field_get_entity'); + $this->executeView($view); + $row = $view->result[0]; + + // Tests entities on the base level. + $entity = $view->field['cid']->get_entity($row); + $this->assertEqual($entity->id(), $comment->id(), 'Make sure the right comment entity got loaded.'); + // Tests entities as relationship on first level. + $entity = $view->field['nid']->get_entity($row); + $this->assertEqual($entity->id(), $node->id(), 'Make sure the right node entity got loaded.'); + // Tests entities as relationships on second level. + $entity = $view->field['uid']->get_entity($row); + $this->assertEqual($entity->id(), $account->id(), 'Make sure the right user entity got loaded.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Field/ApiDataTest.php b/core/modules/views/lib/Drupal/views/Tests/Field/ApiDataTest.php new file mode 100644 index 0000000..abb1ad4 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Field/ApiDataTest.php @@ -0,0 +1,160 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Test\Field\ApiDataTest. + */ + +namespace Drupal\views\Tests\Field; + +/** + * Test the produced views_data. + */ +class ApiDataTest extends FieldTestBase { + + /** + * Stores the fields for this test case. + */ + var $fields; + + public static function getInfo() { + return array( + 'name' => 'Field: Views Data', + 'description' => 'Tests the Field Views data.', + 'group' => 'Views Modules', + ); + } + + function setUp() { + parent::setUp(); + + $langcode = LANGUAGE_NOT_SPECIFIED; + + $field_names = $this->setUpFields(); + + // The first one will be attached to nodes only. + $instance = array( + 'field_name' => $field_names[0], + 'entity_type' => 'node', + 'bundle' => 'page', + ); + field_create_instance($instance); + + // The second one will be attached to users only. + $instance = array( + 'field_name' => $field_names[1], + 'entity_type' => 'user', + 'bundle' => 'user', + ); + field_create_instance($instance); + + // The third will be attached to both nodes and users. + $instance = array( + 'field_name' => $field_names[2], + 'entity_type' => 'node', + 'bundle' => 'page', + ); + field_create_instance($instance); + $instance = array( + 'field_name' => $field_names[2], + 'entity_type' => 'user', + 'bundle' => 'user', + ); + field_create_instance($instance); + + // Now create some example nodes/users for the view result. + for ($i = 0; $i < 5; $i++) { + $edit = array( + // @TODO Write a helper method to create such values. + 'field_name_0' => array($langcode => array((array('value' => $this->randomName())))), + 'field_name_2' => array($langcode => array((array('value' => $this->randomName())))), + ); + $this->nodes[] = $this->drupalCreateNode($edit); + } + + // Reset views data cache. + $this->clearViewsCaches(); + } + + /** + * Unit testing the views data structure. + * + * We check data structure for both node and node revision tables. + */ + function testViewsData() { + $data = views_fetch_data(); + + // Check the table and the joins of the first field. + // Attached to node only. + $field = $this->fields[0]; + $current_table = _field_sql_storage_tablename($field); + $revision_table = _field_sql_storage_revision_tablename($field); + + $this->assertTrue(isset($data[$current_table])); + $this->assertTrue(isset($data[$revision_table])); + // The node field should join against node. + $this->assertTrue(isset($data[$current_table]['table']['join']['node'])); + $this->assertTrue(isset($data[$revision_table]['table']['join']['node_revision'])); + + $expected_join = array( + 'left_field' => 'nid', + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'node'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + $this->assertEqual($expected_join, $data[$current_table]['table']['join']['node']); + $expected_join = array( + 'left_field' => 'vid', + 'field' => 'revision_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'node'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + $this->assertEqual($expected_join, $data[$revision_table]['table']['join']['node_revision']); + + // Check the table and the joins of the second field. + // Attached to both node and user. + $field_2 = $this->fields[2]; + $current_table_2 = _field_sql_storage_tablename($field_2); + $revision_table_2 = _field_sql_storage_revision_tablename($field_2); + + $this->assertTrue(isset($data[$current_table_2])); + $this->assertTrue(isset($data[$revision_table_2])); + // The second field should join against both node and users. + $this->assertTrue(isset($data[$current_table_2]['table']['join']['node'])); + $this->assertTrue(isset($data[$revision_table_2]['table']['join']['node_revision'])); + $this->assertTrue(isset($data[$current_table_2]['table']['join']['users'])); + + $expected_join = array( + 'left_field' => 'nid', + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'node'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ) + ); + $this->assertEqual($expected_join, $data[$current_table_2]['table']['join']['node']); + $expected_join = array( + 'left_field' => 'vid', + 'field' => 'revision_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'node'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ) + ); + $this->assertEqual($expected_join, $data[$revision_table_2]['table']['join']['node_revision']); + $expected_join = array( + 'left_field' => 'uid', + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'user'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ) + ); + $this->assertEqual($expected_join, $data[$current_table_2]['table']['join']['users']); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Field/FieldTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Field/FieldTestBase.php new file mode 100644 index 0000000..7ab6e72 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Field/FieldTestBase.php @@ -0,0 +1,73 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Test\Field\FieldTestBase. + */ + +/** + * @TODO + * - Test on a generic entity not on a node. + * + * What has to be tested: + * - Take sure that every wanted field is added to the according entity type. + * - Take sure the joins are done correct. + * - Use basic fields and take sure that the full wanted object is build. + * - Use relationships between different entity types, for example node and the node author(user). + */ + +namespace Drupal\views\Tests\Field; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Provides some helper methods for testing fieldapi integration into views. + */ +abstract class FieldTestBase extends ViewTestBase { + + /** + * Stores the field definitions used by the test. + * @var array + */ + public $fields; + /** + * Stores the instances of the fields. They have + * the same keys as the fields. + * @var array + */ + public $instances; + + function setUpFields($amount = 3) { + // Create three fields. + $field_names = array(); + for ($i = 0; $i < $amount; $i++) { + $field_names[$i] = 'field_name_' . $i; + $field = array('field_name' => $field_names[$i], 'type' => 'text'); + + $this->fields[$i] = $field = field_create_field($field); + } + return $field_names; + } + + function setUpInstances($bundle = 'page') { + foreach ($this->fields as $key => $field) { + $instance = array( + 'field_name' => $field['field_name'], + 'entity_type' => 'node', + 'bundle' => 'page', + ); + $this->instances[$key] = field_create_instance($instance); + } + } + + /** + * Clear all views caches and static caches which are required for the patch. + */ + function clearViewsCaches() { + // Reset views data cache. + drupal_static_reset('_views_fetch_data_cache'); + drupal_static_reset('_views_fetch_data_recursion_protected'); + drupal_static_reset('_views_fetch_data_fully_loaded'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Field/HandlerFieldFieldTest.php b/core/modules/views/lib/Drupal/views/Tests/Field/HandlerFieldFieldTest.php new file mode 100644 index 0000000..a738062 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Field/HandlerFieldFieldTest.php @@ -0,0 +1,221 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Test\Field\HandlerFieldFieldTest. + */ + +namespace Drupal\views\Tests\Field; + +/** + * Tests the field_field handler. + * @TODO + * Check a entity-type with bundles + * Check a entity-type without bundles + * Check locale:disabled, locale:enabled and locale:enabled with another language + * Check revisions + */ +class HandlerFieldFieldTest extends FieldTestBase { + + public $nodes; + + public static function getInfo() { + return array( + 'name' => 'Field: Field handler', + 'description' => 'Tests the field itself of the Field integration.', + 'group' => 'Views Modules' + ); + } + + protected function setUp() { + parent::setUp(); + + // Setup basic fields. + $this->setUpFields(3); + + // Setup a field with cardinality > 1. + $this->fields[3] = $field = field_create_field(array('field_name' => 'field_name_3', 'type' => 'text', 'cardinality' => FIELD_CARDINALITY_UNLIMITED)); + // Setup a field that will have no value. + $this->fields[4] = $field = field_create_field(array('field_name' => 'field_name_4', 'type' => 'text', 'cardinality' => FIELD_CARDINALITY_UNLIMITED)); + + $this->setUpInstances(); + + $this->clearViewsCaches(); + + // Create some nodes. + $this->nodes = array(); + for ($i = 0; $i < 3; $i++) { + $edit = array('type' => 'page'); + + for ($key = 0; $key < 3; $key++) { + $field = $this->fields[$key]; + $edit[$field['field_name']][LANGUAGE_NOT_SPECIFIED][0]['value'] = $this->randomName(8); + } + for ($j = 0; $j < 5; $j++) { + $edit[$this->fields[3]['field_name']][LANGUAGE_NOT_SPECIFIED][$j]['value'] = $this->randomName(8); + } + // Set this field to be empty. + $edit[$this->fields[4]['field_name']] = array(); + + $this->nodes[$i] = $this->drupalCreateNode($edit); + } + + foreach ($this->fields as $key => $field) { + $this->view->display_handler->display['display_options']['fields'][$field['field_name']]['id'] = $field['field_name']; + $this->view->display_handler->display['display_options']['fields'][$field['field_name']]['table'] = 'field_data_' . $field['field_name']; + $this->view->display_handler->display['display_options']['fields'][$field['field_name']]['field'] = $field['field_name']; + } + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_view_fieldapi'); + } + + public function testFieldRender() { + $this->_testSimpleFieldRender(); + $this->_testFormatterSimpleFieldRender(); + $this->_testMultipleFieldRender(); + } + + public function _testSimpleFieldRender() { + $view = $this->getView(); + $this->executeView($view); + + // Tests that the rendered fields match the actual value of the fields. + for ($i = 0; $i < 3; $i++) { + for ($key = 0; $key < 2; $key++) { + $field = $this->fields[$key]; + $rendered_field = $view->style_plugin->get_field($i, $field['field_name']); + $expected_field = $this->nodes[$i]->{$field['field_name']}[LANGUAGE_NOT_SPECIFIED][0]['value']; + $this->assertEqual($rendered_field, $expected_field); + } + } + } + + /** + * Tests that fields with formatters runs as expected. + */ + public function _testFormatterSimpleFieldRender() { + $view = $this->getView(); + $view->displayHandlers['default']->options['fields'][$this->fields[0]['field_name']]['type'] = 'text_trimmed'; + $view->displayHandlers['default']->options['fields'][$this->fields[0]['field_name']]['settings'] = array( + 'trim_length' => 3, + ); + $this->executeView($view); + + // Take sure that the formatter works as expected. + // @TODO: actually there should be a specific formatter. + for ($i = 0; $i < 2; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $this->fields[0]['field_name']); + $this->assertEqual(strlen($rendered_field), 3); + } + } + + public function _testMultipleFieldRender() { + $view = $this->getView(); + $field_name = $this->fields[3]['field_name']; + + // Test delta limit. + $view->initDisplay(); + $view->displayHandlers['default']->options['fields'][$field_name]['group_rows'] = TRUE; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_limit'] = 3; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $field_name); + $items = array(); + $pure_items = $this->nodes[$i]->{$field_name}[LANGUAGE_NOT_SPECIFIED]; + $pure_items = array_splice($pure_items, 0, 3); + foreach ($pure_items as $j => $item) { + $items[] = $pure_items[$j]['value']; + } + $this->assertEqual($rendered_field, implode(', ', $items), 'Take sure that the amount of items are limited.'); + } + + // Test that an empty field is rendered without error. + $rendered_field = $view->style_plugin->get_field(4, $this->fields[4]['field_name']); + + $view->destroy(); + + // Test delta limit + offset + $view->initDisplay(); + $view->displayHandlers['default']->options['fields'][$field_name]['group_rows'] = TRUE; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_limit'] = 3; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_offset'] = 1; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $field_name); + $items = array(); + $pure_items = $this->nodes[$i]->{$field_name}[LANGUAGE_NOT_SPECIFIED]; + $pure_items = array_splice($pure_items, 1, 3); + foreach ($pure_items as $j => $item) { + $items[] = $pure_items[$j]['value']; + } + $this->assertEqual($rendered_field, implode(', ', $items), 'Take sure that the amount of items are limited.'); + } + $view->destroy(); + + // Test delta limit + reverse. + $view->initDisplay(); + $view->displayHandlers['default']->options['fields'][$field_name]['delta_offset'] = 0; + $view->displayHandlers['default']->options['fields'][$field_name]['group_rows'] = TRUE; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_limit'] = 3; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_reversed'] = TRUE; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $field_name); + $items = array(); + $pure_items = $this->nodes[$i]->{$field_name}[LANGUAGE_NOT_SPECIFIED]; + array_splice($pure_items, 0, -3); + $pure_items = array_reverse($pure_items); + foreach ($pure_items as $j => $item) { + $items[] = $pure_items[$j]['value']; + } + $this->assertEqual($rendered_field, implode(', ', $items), 'Take sure that the amount of items are limited.'); + } + $view->destroy(); + + // Test delta first last. + $view->initDisplay(); + $view->displayHandlers['default']->options['fields'][$field_name]['group_rows'] = TRUE; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_limit'] = 0; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_first_last'] = TRUE; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_reversed'] = FALSE; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $field_name); + $items = array(); + $pure_items = $this->nodes[$i]->{$field_name}[LANGUAGE_NOT_SPECIFIED]; + $items[] = $pure_items[0]['value']; + $items[] = $pure_items[4]['value']; + $this->assertEqual($rendered_field, implode(', ', $items), 'Take sure that the amount of items are limited.'); + } + $view->destroy(); + + // Test delta limit + custom seperator. + $view->initDisplay(); + $view->displayHandlers['default']->options['fields'][$field_name]['delta_first_last'] = FALSE; + $view->displayHandlers['default']->options['fields'][$field_name]['delta_limit'] = 3; + $view->displayHandlers['default']->options['fields'][$field_name]['group_rows'] = TRUE; + $view->displayHandlers['default']->options['fields'][$field_name]['separator'] = ':'; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $field_name); + $items = array(); + $pure_items = $this->nodes[$i]->{$field_name}[LANGUAGE_NOT_SPECIFIED]; + $pure_items = array_splice($pure_items, 0, 3); + foreach ($pure_items as $j => $item) { + $items[] = $pure_items[$j]['value']; + } + $this->assertEqual($rendered_field, implode(':', $items), 'Take sure that the amount of items are limited.'); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/GlossaryTest.php b/core/modules/views/lib/Drupal/views/Tests/GlossaryTest.php new file mode 100644 index 0000000..1b45265 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/GlossaryTest.php @@ -0,0 +1,60 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\GlossaryTest. + */ + +namespace Drupal\views\Tests; + +/** + * Tests glossary view ( summary of arguments ). + */ +class GlossaryTest extends ViewTestBase { + + public static function getInfo() { + return array( + 'name' => 'Glossary tests', + 'description' => 'Tests glossary functionality of views.', + 'group' => 'Views', + ); + } + + /** + * Tests the default glossary view. + */ + public function testGlossaryView() { + // create a contentype and add some nodes, with a non random title. + $type = $this->drupalCreateContentType(); + $nodes_per_char = array( + 'd' => 1, + 'r' => 4, + 'u' => 10, + 'p' => 2, + 'a' => 3, + 'l' => 6, + ); + foreach ($nodes_per_char as $char => $count) { + $setting = array( + 'type' => $type->type + ); + for ($i = 0; $i < $count; $i++) { + $node = $setting; + $node['title'] = $char . $this->randomString(3); + $this->drupalCreateNode($node); + } + } + + // Execute glossary view + $view = views_get_view('glossary'); + $view->setDisplay('attachment'); + $view->executeDisplay('attachment'); + + // Check that the amount of nodes per char. + $result_nodes_per_char = array(); + foreach ($view->result as $item) { + $this->assertEqual($nodes_per_char[$item->title_truncated], $item->num_records); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTest.php new file mode 100644 index 0000000..7249562 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTest.php @@ -0,0 +1,107 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\AreaTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the abstract area handler. + * + * @see Drupal\views\Plugin\views\area\AreaPluginBase + * @see Drupal\views_test\Plugin\views\area\TestExample + */ +class AreaTest extends HandlerTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + public static function getInfo() { + return array( + 'name' => 'Area: Base', + 'description' => 'Test the plugin base of the area handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + protected function viewsData() { + $data = parent::viewsData(); + $data['views']['test_example'] = array( + 'title' => 'Test Example area', + 'help' => 'A area handler which just exists for tests.', + 'area' => array( + 'id' => 'test_example' + ) + ); + + return $data; + } + + + /** + * Tests the generic UI of a area handler. + */ + public function testUI() { + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + + $types = array('header', 'footer', 'empty'); + $labels = array(); + foreach ($types as $type) { + $edit_path = 'admin/structure/views/nojs/config-item/test_example_area/default/' . $type .'/test_example'; + + // First setup an empty label. + $this->drupalPost($edit_path, array(), t('Apply')); + $this->assertText('Test Example area'); + + // Then setup a no empty label. + $labels[$type] = $this->randomName(); + $this->drupalPost($edit_path, array('options[label]' => $labels[$type]), t('Apply')); + // Make sure that the new label appears on the site. + $this->assertText($labels[$type]); + + // Test that the settings (empty/label) are accessible. + $this->drupalGet($edit_path); + $this->assertField('options[label]'); + if ($type !== 'empty') { + $this->assertField('options[empty]'); + } + } + } + + /** + * Tests the rendering of an area. + */ + public function testRenderArea() { + $view = views_get_view('test_example_area'); + $view->initHandlers(); + + // Insert a random string to the test area plugin and see whether it is + // rendered for both header, footer and empty text. + $header_string = $this->randomString(); + $footer_string = $this->randomString(); + $empty_string = $this->randomString(); + $view->header['test_example']->options['string'] = $header_string; + $view->footer['test_example']->options['string'] = $footer_string; + $view->empty['test_example']->options['string'] = $empty_string; + + // Check whether the strings exists in the output. + $output = $view->preview(); + $this->assertTrue(strpos($output, $header_string) !== FALSE); + $this->assertTrue(strpos($output, $footer_string) !== FALSE); + $this->assertTrue(strpos($output, $empty_string) !== FALSE); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTextTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTextTest.php new file mode 100644 index 0000000..6cda26e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/AreaTextTest.php @@ -0,0 +1,61 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\AreaTextTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the text area handler. + * + * @see Drupal\views\Plugin\views\area\Text + */ +class AreaTextTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Area: Text', + 'description' => 'Test the core views_handler_area_text handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + public function testAreaText() { + $view = $this->getView(); + + // add a text header + $string = $this->randomName(); + $view->displayHandlers['default']->overrideOption('header', array( + 'area' => array( + 'id' => 'area', + 'table' => 'views', + 'field' => 'area', + 'content' => $string, + ), + )); + + // Execute the view. + $this->executeView($view); + + $view->display_handler->handlers['header']['area']->options['format'] = $this->randomString(); + $this->assertEqual(NULL, $view->display_handler->handlers['header']['area']->render(), 'Non existant format should return nothing'); + + $view->display_handler->handlers['header']['area']->options['format'] = filter_default_format(); + $this->assertEqual(check_markup($string), $view->display_handler->handlers['header']['area']->render(), 'Existant format should return something'); + + // Empty results, and it shouldn't be displayed . + $this->assertEqual('', $view->display_handler->handlers['header']['area']->render(TRUE), 'No result should lead to no header'); + // Empty results, and it should be displayed. + $view->display_handler->handlers['header']['area']->options['empty'] = TRUE; + $this->assertEqual(check_markup($string), $view->display_handler->handlers['header']['area']->render(TRUE), 'No result, but empty enabled lead to a full header'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/ArgumentNullTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/ArgumentNullTest.php new file mode 100644 index 0000000..a5e26de --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/ArgumentNullTest.php @@ -0,0 +1,81 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\ArgumentNullTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\argument\Null handler. + */ +class ArgumentNullTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Argument: Null', + 'description' => 'Test the core Drupal\views\Plugin\views\argument\Null handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['id']['argument']['id'] = 'null'; + + return $data; + } + + public function testAreaText() { + // Test validation + $view = $this->getView(); + + // Add a null argument. + $string = $this->randomString(); + $view->displayHandlers['default']->overrideOption('arguments', array( + 'null' => array( + 'id' => 'null', + 'table' => 'views', + 'field' => 'null', + ), + )); + + $this->executeView($view); + + // Make sure that the argument is not validated yet. + unset($view->argument['null']->argument_validated); + $this->assertTrue($view->argument['null']->validateArgument(26)); + // test must_not_be option. + unset($view->argument['null']->argument_validated); + $view->argument['null']->options['must_not_be'] = TRUE; + $this->assertFalse($view->argument['null']->validateArgument(26), 'must_not_be returns FALSE, if there is an argument'); + unset($view->argument['null']->argument_validated); + $this->assertTrue($view->argument['null']->validateArgument(NULL), 'must_not_be returns TRUE, if there is no argument'); + + // Test execution. + $view = $this->getView(); + + // Add a argument, which has null as handler. + $string = $this->randomString(); + $view->displayHandlers['default']->overrideOption('arguments', array( + 'id' => array( + 'id' => 'id', + 'table' => 'views_test_data', + 'field' => 'id', + ), + )); + + $this->executeView($view, array(26)); + + // The argument should be ignored, so every result should return. + $this->assertEqual(5, count($view->result)); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/ArgumentStringTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/ArgumentStringTest.php new file mode 100644 index 0000000..d2a2e55 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/ArgumentStringTest.php @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\ArgumentStringTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\argument\String handler. + */ +class ArgumentStringTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Argument: String', + 'description' => 'Test the core Drupal\views\Plugin\views\argument\String handler.', + 'group' => 'Views Handlers', + ); + } + + /** + * Tests the glossary feature. + */ + function testGlossary() { + // Setup some nodes, one with a, two with b and three with c. + $counter = 1; + foreach (array('a', 'b', 'c') as $char) { + for ($i = 0; $i < $counter; $i++) { + $edit = array( + 'title' => $char . $this->randomName(), + ); + $this->drupalCreateNode($edit); + } + } + + $view = $this->createViewFromConfig('test_glossary'); + $this->executeView($view); + + $count_field = 'nid'; + foreach ($view->result as &$row) { + if (strpos($row->node_title, 'a') === 0) { + $this->assertEqual(1, $row->{$count_field}); + } + if (strpos($row->node_title, 'b') === 0) { + $this->assertEqual(2, $row->{$count_field}); + } + if (strpos($row->node_title, 'c') === 0) { + $this->assertEqual(3, $row->{$count_field}); + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldBooleanTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldBooleanTest.php new file mode 100644 index 0000000..b3d3599 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldBooleanTest.php @@ -0,0 +1,86 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FieldBooleanTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\field\Boolean handler. + */ +class FieldBooleanTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: Boolean', + 'description' => 'Test the core Drupal\views\Plugin\views\field\Boolean handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function dataSet() { + // Use default dataset but remove the age from john and paul + $data = parent::dataSet(); + $data[0]['age'] = 0; + $data[3]['age'] = 0; + return $data; + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['age']['field']['id'] = 'boolean'; + return $data; + } + + public function testFieldBoolean() { + $view = $this->getView(); + + $view->displayHandlers['default']->overrideOption('fields', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + $this->executeView($view); + + // This is john, which has no age, there are no custom formats defined, yet. + $this->assertEqual(t('No'), $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual(t('Yes'), $view->field['age']->advanced_render($view->result[1])); + + // Reverse the output. + $view->field['age']->options['not'] = TRUE; + $this->assertEqual(t('Yes'), $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual(t('No'), $view->field['age']->advanced_render($view->result[1])); + + unset($view->field['age']->options['not']); + + // Use another output format. + $view->field['age']->options['type'] = 'true-false'; + $this->assertEqual(t('False'), $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual(t('True'), $view->field['age']->advanced_render($view->result[1])); + + // test awesome unicode. + $view->field['age']->options['type'] = 'unicode-yes-no'; + $this->assertEqual('✖', $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual('✔', $view->field['age']->advanced_render($view->result[1])); + + // Set a custom output format. + $view->field['age']->formats['test'] = array(t('Test-True'), t('Test-False')); + $view->field['age']->options['type'] = 'test'; + $this->assertEqual(t('Test-False'), $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual(t('Test-True'), $view->field['age']->advanced_render($view->result[1])); + + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php new file mode 100644 index 0000000..a39ece6 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php @@ -0,0 +1,90 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FieldCounterTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the Drupal\views\Plugin\views\field\Counter handler. + */ +class FieldCounterTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: Counter', + 'description' => 'Tests the Drupal\views\Plugin\views\field\Counter handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function testSimple() { + $view = $this->getView(); + $view->displayHandlers['default']->overrideOption('fields', array( + 'counter' => array( + 'id' => 'counter', + 'table' => 'views', + 'field' => 'counter', + 'relationship' => 'none', + ), + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + $view->preview(); + + + $counter = $view->style_plugin->rendered_fields[0]['counter']; + $this->assertEqual($counter, 1, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 1, '@counter' => $counter))); + $counter = $view->style_plugin->rendered_fields[1]['counter']; + $this->assertEqual($counter, 2, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 2, '@counter' => $counter))); + $counter = $view->style_plugin->rendered_fields[2]['counter']; + $this->assertEqual($counter, 3, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 3, '@counter' => $counter))); + $view->destroy(); + + $view = $this->getView(); + $rand_start = rand(5, 10); + $view->displayHandlers['default']->overrideOption('fields', array( + 'counter' => array( + 'id' => 'counter', + 'table' => 'views', + 'field' => 'counter', + 'relationship' => 'none', + 'counter_start' => $rand_start + ), + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + $view->preview(); + + $counter = $view->style_plugin->rendered_fields[0]['counter']; + $expected_number = 0 + $rand_start; + $this->assertEqual($counter, $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter))); + $counter = $view->style_plugin->rendered_fields[1]['counter']; + $expected_number = 1 + $rand_start; + $this->assertEqual($counter, $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter))); + $counter = $view->style_plugin->rendered_fields[2]['counter']; + $expected_number = 2 + $rand_start; + $this->assertEqual($counter, $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter))); + } + + // @TODO: Write tests for pager. + function testPager() { + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCustomTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCustomTest.php new file mode 100644 index 0000000..4cc3291 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCustomTest.php @@ -0,0 +1,57 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FieldCustomTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\field\Custom handler. + */ +class FieldCustomTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: Custom', + 'description' => 'Test the core Drupal\views\Plugin\views\field\Custom handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['name']['field']['id'] = 'custom'; + return $data; + } + + public function testFieldCustom() { + $view = $this->getView(); + + // Alter the text of the field to a random string. + $random = $this->randomName(); + $view->displayHandlers['default']->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'alter' => array( + 'text' => $random, + ), + ), + )); + + $this->executeView($view); + + $this->assertEqual($random, $view->style_plugin->get_field(0, 'name')); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldDateTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldDateTest.php new file mode 100644 index 0000000..6040d40 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldDateTest.php @@ -0,0 +1,97 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FieldDateTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\field\Date handler. + */ +class FieldDateTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: Date', + 'description' => 'Test the core Drupal\views\Plugin\views\field\Date handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['created']['field']['id'] = 'date'; + return $data; + } + + public function testFieldDate() { + $view = $this->getView(); + + $view->displayHandlers['default']->overrideOption('fields', array( + 'created' => array( + 'id' => 'created', + 'table' => 'views_test_data', + 'field' => 'created', + 'relationship' => 'none', + // c is iso 8601 date format @see http://php.net/manual/en/function.date.php + 'custom_date_format' => 'c', + ), + )); + $time = gmmktime(0, 0, 0, 1, 1, 2000); + + $this->executeView($view); + + $timezones = array( + NULL, + 'UTC', + 'America/New_York', + ); + foreach ($timezones as $timezone) { + $dates = array( + 'small' => format_date($time, 'small', '', $timezone), + 'medium' => format_date($time, 'medium', '', $timezone), + 'large' => format_date($time, 'large', '', $timezone), + 'custom' => format_date($time, 'custom', 'c', $timezone), + ); + $this->assertRenderedDatesEqual($view, $dates, $timezone); + } + + $intervals = array( + 'raw time ago' => format_interval(REQUEST_TIME - $time, 2), + 'time ago' => t('%time ago', array('%time' => format_interval(REQUEST_TIME - $time, 2))), + // TODO write tests for them +// 'raw time span' => format_interval(REQUEST_TIME - $time, 2), +// 'time span' => t('%time hence', array('%time' => format_interval(REQUEST_TIME - $time, 2))), + ); + $this->assertRenderedDatesEqual($view, $intervals); + } + + protected function assertRenderedDatesEqual($view, $map, $timezone = NULL) { + foreach ($map as $date_format => $expected_result) { + $view->field['created']->options['date_format'] = $date_format; + $t_args = array( + '%value' => $expected_result, + '%format' => $date_format, + ); + if (isset($timezone)) { + $t_args['%timezone'] = $timezone; + $message = t('Value %value in %format format for timezone %timezone matches.', $t_args); + $view->field['created']->options['timezone'] = $timezone; + } + else { + $message = t('Value %value in %format format matches.', $t_args); + } + $actual_result = $view->field['created']->advanced_render($view->result[0]); + $this->assertEqual($expected_result, $actual_result, $message); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldFileSizeTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldFileSizeTest.php new file mode 100644 index 0000000..f8c0dde --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldFileSizeTest.php @@ -0,0 +1,74 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FieldFileSizeTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\field\FileSize handler. + * + * @see CommonXssUnitTest + */ +class FieldFileSizeTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: File size', + 'description' => 'Test the core Drupal\views\Plugin\views\field\FileSize handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function dataSet() { + $data = parent::dataSet(); + $data[0]['age'] = 0; + $data[1]['age'] = 10; + $data[2]['age'] = 1000; + $data[3]['age'] = 10000; + + return $data; + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['age']['field']['id'] = 'file_size'; + + return $data; + } + + public function testFieldFileSize() { + $view = $this->getView(); + + $view->displayHandlers['default']->overrideOption('fields', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + ), + )); + + $this->executeView($view); + + // Test with the formatted option. + $this->assertEqual($view->field['age']->advanced_render($view->result[0]), ''); + $this->assertEqual($view->field['age']->advanced_render($view->result[1]), '10 bytes'); + $this->assertEqual($view->field['age']->advanced_render($view->result[2]), '1000 bytes'); + $this->assertEqual($view->field['age']->advanced_render($view->result[3]), '9.77 KB'); + // Test with the bytes option. + $view->field['age']->options['file_size_display'] = 'bytes'; + $this->assertEqual($view->field['age']->advanced_render($view->result[0]), ''); + $this->assertEqual($view->field['age']->advanced_render($view->result[1]), 10); + $this->assertEqual($view->field['age']->advanced_render($view->result[2]), 1000); + $this->assertEqual($view->field['age']->advanced_render($view->result[3]), 10000); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldTest.php new file mode 100644 index 0000000..f2860d8 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldTest.php @@ -0,0 +1,972 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FieldTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the generic field handler. + * + * @see Drupal\views\Plugin\views\field\FieldPluginBase + */ +use DOMDocument; + +class FieldTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: Base', + 'description' => 'Tests the generic field handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->column_map = array( + 'views_test_data_name' => 'name', + ); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + */ + protected function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['job']['field']['id'] = 'test_field'; + + return $data; + } + + /** + * Tests that the render function is called. + */ + public function testRender() { + $view = views_get_view('test_field_tokens'); + $this->executeView($view); + + $random_text = $this->randomName(); + $view->field['job']->setTestValue($random_text); + $this->assertEqual($view->field['job']->theme($view->result[0]), $random_text, 'Make sure the render method rendered the manual set value.'); + } + + /** + * Tests all things related to the query. + */ + public function testQuery() { + // Tests adding additional fields to the query. + $view = $this->getBasicView(); + $view->initHandlers(); + + $id_field = $view->field['id']; + $id_field->additional_fields['job'] = 'job'; + // Choose also a field alias key which doesn't match to the table field. + $id_field->additional_fields['created_test'] = array('table' => 'views_test_data', 'field' => 'created'); + $view->build(); + + // Make sure the field aliases have the expected value. + $this->assertEqual($id_field->aliases['job'], 'views_test_data_job'); + $this->assertEqual($id_field->aliases['created_test'], 'views_test_data_created'); + + $this->executeView($view); + // Tests the get_value method with and without a field aliases. + foreach ($this->dataSet() as $key => $row) { + $id = $key + 1; + $result = $view->result[$key]; + $this->assertEqual($id_field->get_value($result), $id); + $this->assertEqual($id_field->get_value($result, 'job'), $row['job']); + $this->assertEqual($id_field->get_value($result, 'created_test'), $row['created']); + } + + } + + /** + * Tests the click sorting functionality. + */ + public function testClickSorting() { + $this->drupalGet('test_click_sort'); + $this->assertResponse(200); + // Only the id and name should be click sortable, but not the name. + $this->assertLinkByHref(url('test_click_sort', array('query' => array('order' => 'id', 'sort' => 'asc')))); + $this->assertLinkByHref(url('test_click_sort', array('query' => array('order' => 'name', 'sort' => 'desc')))); + $this->assertNoLinkByHref(url('test_click_sort', array('query' => array('order' => 'created')))); + + // Clicking a click sort should change the order. + $this->clickLink(t('ID')); + $this->assertLinkByHref(url('test_click_sort', array('query' => array('order' => 'id', 'sort' => 'desc')))); + // Check that the output has the expected order (asc). + $ids = $this->clickSortLoadIdsFromOutput(); + $this->assertEqual($ids, range(1, 5)); + + $this->clickLink(t('ID')); + // Check that the output has the expected order (desc). + $ids = $this->clickSortLoadIdsFromOutput(); + $this->assertEqual($ids, range(5, 1, -1)); + } + + /** + * Small helper function to get all ids in the output. + * + * @return array + * A list of beatle ids. + */ + protected function clickSortLoadIdsFromOutput() { + $fields = $this->xpath("//td[contains(@class, 'views-field-id')]"); + $ids = array(); + foreach ($fields as $field) { + $ids[] = (int) $field[0]; + } + return $ids; + } + + /** + * Assertion helper which checks whether a string is part of another string. + * + * @param string $haystack + * The value to search in. + * @param string $needle + * The value to search for. + * @param string $message + * The message to display along with the assertion. + * @param string $group + * The type of assertion - examples are "Browser", "PHP". + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertSubString($haystack, $needle, $message = '', $group = 'Other') { + return $this->assertTrue(strpos($haystack, $needle) !== FALSE, $message, $group); + } + + /** + * Assertion helper which checks whether a string is not part of another string. + * + * @param string $haystack + * The value to search in. + * @param string $needle + * The value to search for. + * @param string $message + * The message to display along with the assertion. + * @param string $group + * The type of assertion - examples are "Browser", "PHP". + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertNotSubString($haystack, $needle, $message = '', $group = 'Other') { + return $this->assertTrue(strpos($haystack, $needle) === FALSE, $message, $group); + } + + /** + * Parse a content and return the html element. + * + * @param string $content + * The html to parse. + * + * @return array + * An array containing simplexml objects. + */ + protected function parseContent($content) { + $htmlDom = new DOMDocument(); + @$htmlDom->loadHTML('<?xml encoding="UTF-8">' . $content); + $elements = simplexml_import_dom($htmlDom); + + return $elements; + } + + /** + * Performs an xpath search on a certain content. + * + * The search is relative to the root element of the $content variable. + * + * @param string $content + * The html to parse. + * @param string $xpath + * The xpath string to use in the search. + * @param array $arguments + * Some arguments for the xpath. + * + * @return array|FALSE + * The return value of the xpath search. For details on the xpath string + * format and return values see the SimpleXML documentation, + * http://php.net/manual/function.simplexml-element-xpath.php. + */ + protected function xpathContent($content, $xpath, array $arguments = array()) { + if ($elements = $this->parseContent($content)) { + $xpath = $this->buildXPathQuery($xpath, $arguments); + $result = $elements->xpath($xpath); + // Some combinations of PHP / libxml versions return an empty array + // instead of the documented FALSE. Forcefully convert any falsish values + // to an empty array to allow foreach(...) constructions. + return $result ? $result : array(); + } + else { + return FALSE; + } + } + + /** + * Tests rewriting the output to a link. + */ + public function testAlterUrl() { + $view = $this->getBasicView(); + $view->initHandlers(); + $this->executeView($view); + $row = $view->result[0]; + $id_field = $view->field['id']; + + // Setup the general settings required to build a link. + $id_field->options['alter']['make_link'] = TRUE; + $id_field->options['alter']['path'] = $path = $this->randomName(); + + // Tests that the suffix/prefix appears on the output. + $id_field->options['alter']['prefix'] = $prefix = $this->randomName(); + $id_field->options['alter']['suffix'] = $suffix = $this->randomName(); + $output = $id_field->theme($row); + $this->assertSubString($output, $prefix); + $this->assertSubString($output, $suffix); + unset($id_field->options['alter']['prefix']); + unset($id_field->options['alter']['suffix']); + + $output = $id_field->theme($row); + $this->assertSubString($output, $path, 'Make sure that the path is part of the output'); + + // Some generic test code adapted from the UrlTest class, which tests + // mostly the different options for the path. + global $base_url, $script_path; + + foreach (array(FALSE, TRUE) as $absolute) { + // Get the expected start of the path string. + $base = ($absolute ? $base_url . '/' : base_path()) . $script_path; + $absolute_string = $absolute ? 'absolute' : NULL; + $alter =& $id_field->options['alter']; + $alter['path'] = 'node/123'; + + $expected_result = url('node/123', array('absolute' => $absolute)); + $alter['absolute'] = $absolute; + $result = $id_field->theme($row); + $this->assertSubString($result, $expected_result); + + $expected_result = url('node/123', array('fragment' => 'foo', 'absolute' => $absolute)); + $alter['path'] = 'node/123#foo'; + $result = $id_field->theme($row); + $this->assertSubString($result, $expected_result); + + $expected_result = url('node/123', array('query' => array('foo' => NULL), 'absolute' => $absolute)); + $alter['path'] = 'node/123?foo'; + $result = $id_field->theme($row); + $this->assertSubString($result, $expected_result); + + $expected_result = url('node/123', array('query' => array('foo' => 'bar', 'bar' => 'baz'), 'absolute' => $absolute)); + $alter['path'] = 'node/123?foo=bar&bar=baz'; + $result = $id_field->theme($row); + $this->assertSubString(decode_entities($result), decode_entities($expected_result)); + + $expected_result = url('node/123', array('query' => array('foo' => NULL), 'fragment' => 'bar', 'absolute' => $absolute)); + $alter['path'] = 'node/123?foo#bar'; + $result = $id_field->theme($row); + // @fixme: The actual result is node/123?foo#bar so views has a bug here. + // $this->assertSubStringExists(decode_entities($result), decode_entities($expected_result)); + + $expected_result = url('<front>', array('absolute' => $absolute)); + $alter['path'] = '<front>'; + $result = $id_field->theme($row); + $this->assertSubString($result, $expected_result); + } + + // Tests the replace spaces with dashes feature. + $id_field->options['alter']['replace_spaces'] = TRUE; + $id_field->options['alter']['path'] = $path = $this->randomName() . ' ' . $this->randomName(); + $output = $id_field->theme($row); + $this->assertSubString($output, str_replace(' ', '-', $path)); + $id_field->options['alter']['replace_spaces'] = FALSE; + $output = $id_field->theme($row); + // The url has a space in it, so to check we have to decode the url output. + $this->assertSubString(urldecode($output), $path); + + // Tests the external flag. + // Switch on the external flag should output an external url as well. + $id_field->options['alter']['external'] = TRUE; + $id_field->options['alter']['path'] = $path = 'drupal.org'; + $output = $id_field->theme($row); + $this->assertSubString($output, 'http://drupal.org'); + + // Setup a not external url, which shouldn't lead to an external url. + $id_field->options['alter']['external'] = FALSE; + $id_field->options['alter']['path'] = $path = 'drupal.org'; + $output = $id_field->theme($row); + $this->assertNotSubString($output, 'http://drupal.org'); + + // Tests the transforming of the case setting. + $id_field->options['alter']['path'] = $path = $this->randomName(); + $id_field->options['alter']['path_case'] = 'none'; + $output = $id_field->theme($row); + $this->assertSubString($output, $path); + + // Switch to uppercase and lowercase. + $id_field->options['alter']['path_case'] = 'upper'; + $output = $id_field->theme($row); + $this->assertSubString($output, strtoupper($path)); + $id_field->options['alter']['path_case'] = 'lower'; + $output = $id_field->theme($row); + $this->assertSubString($output, strtolower($path)); + + // Switch to ucfirst and ucwords. + $id_field->options['alter']['path_case'] = 'ucfirst'; + $id_field->options['alter']['path'] = 'drupal has a great community'; + $output = $id_field->theme($row); + $this->assertSubString($output, drupal_encode_path('Drupal has a great community')); + + $id_field->options['alter']['path_case'] = 'ucwords'; + $output = $id_field->theme($row); + $this->assertSubString($output, drupal_encode_path('Drupal Has A Great Community')); + unset($id_field->options['alter']['path_case']); + + // Tests the linkclass setting and see whether it actuall exists in the output. + $id_field->options['alter']['link_class'] = $class = $this->randomName(); + $output = $id_field->theme($row); + $elements = $this->xpathContent($output, '//a[contains(@class, :class)]', array(':class' => $class)); + $this->assertTrue($elements); + // @fixme link_class, alt, rel cannot be unset, which should be fixed. + $id_field->options['alter']['link_class'] = ''; + + // Tests the alt setting. + $id_field->options['alter']['alt'] = $rel = $this->randomName(); + $output = $id_field->theme($row); + $elements = $this->xpathContent($output, '//a[contains(@title, :alt)]', array(':alt' => $rel)); + $this->assertTrue($elements); + $id_field->options['alter']['alt'] = ''; + + // Tests the rel setting. + $id_field->options['alter']['rel'] = $rel = $this->randomName(); + $output = $id_field->theme($row); + $elements = $this->xpathContent($output, '//a[contains(@rel, :rel)]', array(':rel' => $rel)); + $this->assertTrue($elements); + $id_field->options['alter']['rel'] = ''; + + // Tests the target setting. + $id_field->options['alter']['target'] = $target = $this->randomName(); + $output = $id_field->theme($row); + $elements = $this->xpathContent($output, '//a[contains(@target, :target)]', array(':target' => $target)); + $this->assertTrue($elements); + unset($id_field->options['alter']['target']); + } + + + /** + * Tests general rewriting of the output. + */ + public function testRewrite() { + $view = $this->getBasicView(); + $view->initHandlers(); + $this->executeView($view); + $row = $view->result[0]; + $id_field = $view->field['id']; + + // Don't check the rewrite checkbox, so the text shouldn't appear. + $id_field->options['alter']['text'] = $random_text = $this->randomName(); + $output = $id_field->theme($row); + $this->assertNotSubString($output, $random_text); + + $id_field->options['alter']['alter_text'] = TRUE; + $output = $id_field->theme($row); + $this->assertSubString($output, $random_text); + } + + /** + * Tests the field/label/wrapper classes. + */ + public function testFieldClasses() { + $view = views_get_view('test_field_classes'); + $view->initHandlers(); + + // Tests whether the default field classes are added. + $id_field = $view->field['id']; + + $id_field->options['element_default_classes'] = FALSE; + $output = $view->preview(); + $this->assertFalse($this->xpathContent($output, '//div[contains(@class, :class)]', array(':class' => 'field-content'))); + $this->assertFalse($this->xpathContent($output, '//div[contains(@class, :class)]', array(':class' => 'field-label'))); + + $id_field->options['element_default_classes'] = TRUE; + $output = $view->preview(); + // Per default the label and the element of the field are spans. + $this->assertTrue($this->xpathContent($output, '//span[contains(@class, :class)]', array(':class' => 'field-content'))); + $this->assertTrue($this->xpathContent($output, '//span[contains(@class, :class)]', array(':class' => 'views-label'))); + $this->assertTrue($this->xpathContent($output, '//div[contains(@class, :class)]', array(':class' => 'views-field'))); + + // Tests the element wrapper classes/element. + $random_class = $this->randomName(); + + // Set some common wrapper element types and see whether they appear with and without a custom class set. + foreach (array('h1', 'span', 'p', 'div') as $element_type) { + $id_field->options['element_wrapper_type'] = $element_type; + + // Set a custom wrapper element css class. + $id_field->options['element_wrapper_class'] = $random_class; + $output = $view->preview(); + $this->assertTrue($this->xpathContent($output, "//{$element_type}[contains(@class, :class)]", array(':class' => $random_class))); + + // Set no custom css class. + $id_field->options['element_wrapper_class'] = ''; + $output = $view->preview(); + $this->assertFalse($this->xpathContent($output, "//{$element_type}[contains(@class, :class)]", array(':class' => $random_class))); + $this->assertTrue($this->xpathContent($output, "//li[contains(@class, views-row)]/{$element_type}")); + } + + // Tests the label class/element. + + // Set some common label element types and see whether they appear with and without a custom class set. + foreach (array('h1', 'span', 'p', 'div') as $element_type) { + $id_field->options['element_label_type'] = $element_type; + + // Set a custom label element css class. + $id_field->options['element_label_class'] = $random_class; + $output = $view->preview(); + $this->assertTrue($this->xpathContent($output, "//li[contains(@class, views-row)]//{$element_type}[contains(@class, :class)]", array(':class' => $random_class))); + + // Set no custom css class. + $id_field->options['element_label_class'] = ''; + $output = $view->preview(); + $this->assertFalse($this->xpathContent($output, "//li[contains(@class, views-row)]//{$element_type}[contains(@class, :class)]", array(':class' => $random_class))); + $this->assertTrue($this->xpathContent($output, "//li[contains(@class, views-row)]//{$element_type}")); + } + + // Tests the element classes/element. + + // Set some common element element types and see whether they appear with and without a custom class set. + foreach (array('h1', 'span', 'p', 'div') as $element_type) { + $id_field->options['element_type'] = $element_type; + + // Set a custom label element css class. + $id_field->options['element_class'] = $random_class; + $output = $view->preview(); + $this->assertTrue($this->xpathContent($output, "//li[contains(@class, views-row)]//div[contains(@class, views-field)]//{$element_type}[contains(@class, :class)]", array(':class' => $random_class))); + + // Set no custom css class. + $id_field->options['element_class'] = ''; + $output = $view->preview(); + $this->assertFalse($this->xpathContent($output, "//li[contains(@class, views-row)]//div[contains(@class, views-field)]//{$element_type}[contains(@class, :class)]", array(':class' => $random_class))); + $this->assertTrue($this->xpathContent($output, "//li[contains(@class, views-row)]//div[contains(@class, views-field)]//{$element_type}")); + } + + // Tests the available html elements. + $element_types = $id_field->get_elements(); + $expected_elements = array( + '', + '0', + 'div', + 'span', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'p', + 'strong', + 'em', + 'marquee' + ); + sort($expected_elements); + + $this->assertEqual(array_keys($element_types), $expected_elements); + } + + /** + * Tests the field tokens, row level and field level. + */ + public function testFieldTokens() { + $view = views_get_view('test_field_tokens'); + $this->executeView($view); + $name_field_0 = $view->field['name']; + $name_field_1 = $view->field['name_1']; + $name_field_2 = $view->field['name_2']; + $row = $view->result[0]; + + $name_field_0->options['alter']['alter_text'] = TRUE; + $name_field_0->options['alter']['text'] = '[name]'; + + $name_field_1->options['alter']['alter_text'] = TRUE; + $name_field_1->options['alter']['text'] = '[name_1] [name]'; + + $name_field_2->options['alter']['alter_text'] = TRUE; + $name_field_2->options['alter']['text'] = '[name_2] [name_1]'; + + foreach ($view->result as $row) { + $expected_output_0 = $row->views_test_data_name; + $expected_output_1 = "$row->views_test_data_name $row->views_test_data_name"; + $expected_output_2 = "$row->views_test_data_name $row->views_test_data_name $row->views_test_data_name"; + + $output = $name_field_0->advanced_render($row); + $this->assertEqual($output, $expected_output_0); + + $output = $name_field_1->advanced_render($row); + $this->assertEqual($output, $expected_output_1); + + $output = $name_field_2->advanced_render($row); + $this->assertEqual($output, $expected_output_2); + } + + $job_field = $view->field['job']; + $job_field->options['alter']['alter_text'] = TRUE; + $job_field->options['alter']['text'] = '[test-token]'; + + $random_text = $this->randomName(); + $job_field->setTestValue($random_text); + $output = $job_field->advanced_render($row); + $this->assertSubString($output, $random_text, format_string('Make sure the self token (!value) appears in the output (!output)'. array('!value' => $random_text, '!output' => $output))); + } + + /** + * Tests the exclude setting. + */ + public function testExclude() { + $view = views_get_view('test_field_output'); + $view->initHandlers(); + // Hide the field and see whether it's rendered. + $view->field['name']->options['exclude'] = TRUE; + + $output = $view->preview(); + foreach ($this->dataSet() as $entry) { + $this->assertNotSubString($output, $entry['name']); + } + + // Show and check the field. + $view->field['name']->options['exclude'] = FALSE; + + $output = $view->preview(); + foreach ($this->dataSet() as $entry) { + $this->assertSubString($output, $entry['name']); + } + } + + /** + * Tests trimming/read-more/ellipses. + */ + public function testTextRendering() { + $view = views_get_view('test_field_output'); + $view->initHandlers(); + $name_field = $view->field['name']; + + // Tests stripping of html elements. + $this->executeView($view); + $random_text = $this->randomName(); + $name_field->options['alter']['alter_text'] = TRUE; + $name_field->options['alter']['text'] = $html_text = '<div class="views-test">' . $random_text . '</div>'; + $row = $view->result[0]; + + $name_field->options['alter']['strip_tags'] = TRUE; + $output = $name_field->advanced_render($row); + $this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is enabled.'); + $this->assertNotSubString($output, $html_text, 'Find no text with the html if stripping of views field output is enabled.'); + + // Tests preserving of html tags. + $name_field->options['alter']['preserve_tags'] = '<div>'; + $output = $name_field->advanced_render($row); + $this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is enabled but a div is allowed.'); + $this->assertSubString($output, $html_text, 'Find text with the html if stripping of views field output is enabled but a div is allowed.'); + + $name_field->options['alter']['strip_tags'] = FALSE; + $output = $name_field->advanced_render($row); + $this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is disabled.'); + $this->assertSubString($output, $html_text, 'Find text with the html if stripping of views field output is disabled.'); + + // Tests for removing whitespace and the beginning and the end. + $name_field->options['alter']['alter_text'] = FALSE; + $views_test_data_name = $row->views_test_data_name; + $row->views_test_data_name = ' ' . $views_test_data_name . ' '; + $name_field->options['alter']['trim_whitespace'] = TRUE; + $output = $name_field->advanced_render($row); + + $this->assertSubString($output, $views_test_data_name, 'Make sure the trimmed text can be found if trimming is enabled.'); + $this->assertNotSubString($output, $row->views_test_data_name, 'Make sure the untrimmed text can be found if trimming is enabled.'); + + $name_field->options['alter']['trim_whitespace'] = FALSE; + $output = $name_field->advanced_render($row); + $this->assertSubString($output, $views_test_data_name, 'Make sure the trimmed text can be found if trimming is disabled.'); + $this->assertSubString($output, $row->views_test_data_name, 'Make sure the untrimmed text can be found if trimming is disabled.'); + + + // Tests for trimming to a maximum length. + $name_field->options['alter']['trim'] = TRUE; + $name_field->options['alter']['word_boundary'] = FALSE; + + // Tests for simple trimming by string length. + $row->views_test_data_name = $this->randomName(8); + $name_field->options['alter']['max_length'] = 5; + $trimmed_name = drupal_substr($row->views_test_data_name, 0, 5); + + $output = $name_field->advanced_render($row); + $this->assertSubString($output, $trimmed_name, format_string('Make sure the trimmed output (!trimmed) appears in the rendered output (!output).', array('!trimmed' => $trimmed_name, '!output' => $output))); + $this->assertNotSubString($output, $row->views_test_data_name, format_string("Make sure the untrimmed value (!untrimmed) shouldn't appear in the rendered output (!output).", array('!untrimmed' => $row->views_test_data_name, '!output' => $output))); + + $name_field->options['alter']['max_length'] = 9; + $output = $name_field->advanced_render($row); + $this->assertSubString($output, $trimmed_name, format_string('Make sure the untrimmed (!untrimmed) output appears in the rendered output (!output).', array('!trimmed' => $trimmed_name, '!output' => $output))); + + // Take word_boundary into account for the tests. + $name_field->options['alter']['max_length'] = 5; + $name_field->options['alter']['word_boundary'] = TRUE; + $random_text_2 = $this->randomName(2); + $random_text_4 = $this->randomName(4); + $random_text_8 = $this->randomName(8); + $touples = array( + // Create one string which doesn't fit at all into the limit. + array( + 'value' => $random_text_8, + 'trimmed_value' => '', + 'trimmed' => TRUE + ), + // Create one string with two words which doesn't fit both into the limit. + array( + 'value' => $random_text_8 . ' ' . $random_text_8, + 'trimmed_value' => '', + 'trimmed' => TRUE + ), + // Create one string which contains of two words, of which only the first + // fits into the limit. + array( + 'value' => $random_text_4 . ' ' . $random_text_8, + 'trimmed_value' => $random_text_4, + 'trimmed' => TRUE + ), + // Create one string which contains of two words, of which both fits into + // the limit. + array( + 'value' => $random_text_2 . ' ' . $random_text_2, + 'trimmed_value' => $random_text_2 . ' ' . $random_text_2, + 'trimmed' => FALSE + ) + ); + + foreach ($touples as $touple) { + $row->views_test_data_name = $touple['value']; + $output = $name_field->advanced_render($row); + + if ($touple['trimmed']) { + $this->assertNotSubString($output, $touple['value'], format_string('The untrimmed value (!untrimmed) should not appear in the trimmed output (!output).', array('!untrimmed' => $touple['value'], '!output' => $output))); + } + if (!empty($touble['trimmed_value'])) { + $this->assertSubString($output, $touple['trimmed_value'], format_string('The trimmed value (!trimmed) should appear in the trimmed output (!output).', array('!trimmed' => $touple['trimmed_value'], '!output' => $output))); + } + } + + // Tests for displaying a readmore link when the output got trimmed. + $row->views_test_data_name = $this->randomName(8); + $name_field->options['alter']['max_length'] = 5; + $name_field->options['alter']['more_link'] = TRUE; + $name_field->options['alter']['more_link_text'] = $more_text = $this->randomName(); + $name_field->options['alter']['more_link_path'] = $more_path = $this->randomName(); + + $output = $name_field->advanced_render($row); + $this->assertSubString($output, $more_text, 'Make sure a read more text is displayed if the output got trimmed'); + $this->assertTrue($this->xpathContent($output, '//a[contains(@href, :path)]', array(':path' => $more_path)), 'Make sure the read more link points to the right destination.'); + + $name_field->options['alter']['more_link'] = FALSE; + $output = $name_field->advanced_render($row); + $this->assertNotSubString($output, $more_text, 'Make sure no read more text appears.'); + $this->assertFalse($this->xpathContent($output, '//a[contains(@href, :path)]', array(':path' => $more_path)), 'Make sure no read more link appears.'); + + // Check for the ellipses. + $row->views_test_data_name = $this->randomName(8); + $name_field->options['alter']['max_length'] = 5; + $output = $name_field->advanced_render($row); + $this->assertSubString($output, '...', 'An ellipsis should appear if the output is trimmed'); + $name_field->options['alter']['max_length'] = 10; + $output = $name_field->advanced_render($row); + $this->assertNotSubString($output, '...', 'No ellipsis should appear if the output is not trimmed'); + } + + /** + * Tests everything related to empty output of a field. + */ + function testEmpty() { + $this->_testHideIfEmpty(); + $this->_testEmptyText(); + } + + /** + * Tests the hide if empty functionality. + * + * This tests alters the result to get easier and less coupled results. + */ + function _testHideIfEmpty() { + $view = $this->getView(); + $view->initDisplay(); + $this->executeView($view); + + $column_map_reversed = array_flip($this->column_map); + $view->row_index = 0; + $random_name = $this->randomName(); + $random_value = $this->randomName(); + + // Test when results are not rewritten and empty values are not hidden. + $view->field['name']->options['hide_alter_empty'] = FALSE; + $view->field['name']->options['hide_empty'] = FALSE; + $view->field['name']->options['empty_zero'] = FALSE; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'By default, a string should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'By default, "" should not be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, '0', 'By default, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'By default, "0" should not be treated as empty.'); + + // Test when results are not rewritten and non-zero empty values are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If hide_empty is checked, a string should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If hide_empty is checked, "" should be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, '0', 'If hide_empty is checked, but not empty_zero, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If hide_empty is checked, but not empty_zero, "0" should not be treated as empty.'); + + // Test when results are not rewritten and all empty values are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = TRUE; + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If hide_empty and empty_zero are checked, 0 should be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If hide_empty and empty_zero are checked, "0" should be treated as empty.'); + + // Test when results are rewritten to a valid string and non-zero empty + // results are hidden. + $view->field['name']->options['hide_alter_empty'] = FALSE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = $random_name; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_value; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, it should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "" should not be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "0" should not be treated as empty.'); + + // Test when results are rewritten to an empty string and non-zero empty results are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = ""; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is empty, it should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is empty, "" should be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, '0', 'If the rewritten string is empty, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is empty, "0" should not be treated as empty.'); + + // Test when results are rewritten to zero as a string and non-zero empty + // results are hidden. + $view->field['name']->options['hide_alter_empty'] = FALSE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = "0"; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, the string rewritten as 0 should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "" rewritten as 0 should not be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "0" should not be treated as empty.'); + + // Test when results are rewritten to a valid string and non-zero empty + // results are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = $random_value; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, it should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If either the original or rewritten string is invalid, "" should be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, "0" should not be treated as empty.'); + + // Test when results are rewritten to zero as a string and all empty + // original values and results are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = TRUE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = "0"; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is zero, it should be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is zero, "" should be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is zero, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is zero, "0" should not be treated as empty.'); + } + + /** + * Tests the usage of the empty text. + */ + function _testEmptyText() { + $view = $this->getView(); + $view->initDisplay(); + $this->executeView($view); + + $column_map_reversed = array_flip($this->column_map); + $view->row_index = 0; + + $empty_text = $view->field['name']->options['empty'] = $this->randomName(); + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $empty_text, 'If a field is empty, the empty text should be used for the output.'); + + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If a field is 0 and empty_zero is not checked, the empty text should not be used for the output.'); + + $view->result[0]->{$column_map_reversed['name']} = "0"; + $view->field['name']->options['empty_zero'] = TRUE; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $empty_text, 'If a field is 0 and empty_zero is checked, the empty text should be used for the output.'); + + $view->result[0]->{$column_map_reversed['name']} = ""; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $alter_text = $view->field['name']->options['alter']['text'] = $this->randomName(); + $view->field['name']->options['hide_alter_empty'] = FALSE; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $alter_text, 'If a field is empty, some rewrite text exists, but hide_alter_empty is not checked, render the rewrite text.'); + + $view->field['name']->options['hide_alter_empty'] = TRUE; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $empty_text, 'If a field is empty, some rewrite text exists, and hide_alter_empty is checked, use the empty text.'); + } + + /** + * Tests views_handler_field::is_value_empty(). + */ + function testIsValueEmpty() { + $view = $this->getView(); + $view->initHandlers(); + $field = $view->field['name']; + + $this->assertFalse($field->is_value_empty("not empty", TRUE), 'A normal string is not empty.'); + $this->assertTrue($field->is_value_empty("not empty", TRUE, FALSE), 'A normal string which skips empty() can be seen as empty.'); + + $this->assertTrue($field->is_value_empty("", TRUE), '"" is considered as empty.'); + + $this->assertTrue($field->is_value_empty('0', TRUE), '"0" is considered as empty if empty_zero is TRUE.'); + $this->assertTrue($field->is_value_empty(0, TRUE), '0 is considered as empty if empty_zero is TRUE.'); + $this->assertFalse($field->is_value_empty('0', FALSE), '"0" is considered not as empty if empty_zero is FALSE.'); + $this->assertFalse($field->is_value_empty(0, FALSE), '0 is considered not as empty if empty_zero is FALSE.'); + + $this->assertTrue($field->is_value_empty(NULL, TRUE, TRUE), 'Null should be always seen as empty, regardless of no_skip_empty.'); + $this->assertTrue($field->is_value_empty(NULL, TRUE, FALSE), 'Null should be always seen as empty, regardless of no_skip_empty.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUrlTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUrlTest.php new file mode 100644 index 0000000..0ffbed0 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUrlTest.php @@ -0,0 +1,70 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FieldUrlTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\field\Url handler. + */ +class FieldUrlTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: URL', + 'description' => 'Test the core Drupal\views\Plugin\views\field\Url handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['name']['field']['id'] = 'url'; + return $data; + } + + public function testFieldUrl() { + $view = $this->getView(); + + $view->displayHandlers['default']->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'display_as_link' => FALSE, + ), + )); + + $this->executeView($view); + + $this->assertEqual('John', $view->field['name']->advanced_render($view->result[0])); + + // Make the url a link. + $view->destroy(); + $view = $this->getView(); + + $view->displayHandlers['default']->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + + $this->executeView($view); + + $this->assertEqual(l('John', 'John'), $view->field['name']->advanced_render($view->result[0])); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldXssTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldXssTest.php new file mode 100644 index 0000000..01f95cb --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldXssTest.php @@ -0,0 +1,70 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FieldXssTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\field\Xss handler. + * + * @see CommonXssUnitTest + */ +class FieldXssTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: XSS', + 'description' => 'Test the core Drupal\views\Plugin\views\field\Xss handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function dataHelper() { + $map = array( + 'John' => 'John', + "Foo\xC0barbaz" => '', + 'Fooÿñ' => 'Fooÿñ' + ); + + return $map; + } + + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['name']['field']['id'] = 'xss'; + + return $data; + } + + public function testFieldXss() { + $view = $this->getView(); + + $view->displayHandlers['default']->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + ), + )); + + $this->executeView($view); + + $counter = 0; + foreach ($this->dataHelper() as $input => $expected_result) { + $view->result[$counter]->views_test_data_name = $input; + $this->assertEqual($view->field['name']->advanced_render($view->result[$counter]), $expected_result); + $counter++; + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterCombineTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterCombineTest.php new file mode 100644 index 0000000..eb487a2 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterCombineTest.php @@ -0,0 +1,111 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FilterCombineTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the combine filter handler. + */ +class FilterCombineTest extends HandlerTestBase { + + var $column_map = array(); + + public static function getInfo() { + return array( + 'name' => 'Filter: Combine', + 'description' => 'Tests the combine filter handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->column_map = array( + 'views_test_data_name' => 'name', + 'views_test_data_job' => 'job', + ); + } + + protected function getBasicView() { + $view = parent::getBasicView(); + $view->displayHandlers['default']->display['display_options']['fields']['job'] = array( + 'id' => 'job', + 'table' => 'views_test_data', + 'field' => 'job', + 'relationship' => 'none', + ); + return $view; + } + + public function testFilterCombineContains() { + $view = $this->getView(); + + // Change the filtering. + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'id' => 'combine', + 'table' => 'views', + 'field' => 'combine', + 'relationship' => 'none', + 'operator' => 'contains', + 'fields' => array( + 'name', + 'job', + ), + 'value' => 'ing', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'job' => 'Singer', + ), + array( + 'name' => 'George', + 'job' => 'Singer', + ), + array( + 'name' => 'Ringo', + 'job' => 'Drummer', + ), + array( + 'name' => 'Ginger', + 'job' => NULL, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + /** + * Additional data to test the NULL issue. + */ + protected function dataSet() { + $data_set = parent::dataSet(); + $data_set[] = array( + 'name' => 'Ginger', + 'age' => 25, + 'job' => NULL, + 'created' => gmmktime(0, 0, 0, 1, 2, 2000), + ); + return $data_set; + } + + /** + * Allow {views_test_data}.job to be NULL. + */ + protected function schemaDefinition() { + $schema = parent::schemaDefinition(); + unset($schema['views_test_data']['fields']['job']['not null']); + return $schema; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterDateTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterDateTest.php new file mode 100644 index 0000000..226bce3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterDateTest.php @@ -0,0 +1,153 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FilterDateTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\filter\Date handler. + */ +class FilterDateTest extends HandlerTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + public static function getInfo() { + return array( + 'name' => 'Filter: Date', + 'description' => 'Test the core Drupal\views\Plugin\views\filter\Date handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + // Add some basic test nodes. + $this->nodes = array(); + $this->nodes[] = $this->drupalCreateNode(array('created' => 100000)); + $this->nodes[] = $this->drupalCreateNode(array('created' => 200000)); + $this->nodes[] = $this->drupalCreateNode(array('created' => 300000)); + $this->nodes[] = $this->drupalCreateNode(array('created' => time() + 86400)); + + $this->map = array( + 'nid' => 'nid', + ); + } + + /** + * Test the general offset functionality. + */ + function testOffset() { + $saved_view = $this->createViewFromConfig('test_filter_date_between'); + + // Test offset for simple operator. + $view = $this->getView($saved_view); + $view->initHandlers(); + $view->filter['created']->operator = '>'; + $view->filter['created']->value['type'] = 'offset'; + $view->filter['created']->value['value'] = '+1 hour'; + $view->executeDisplay('default'); + $expected_result = array( + array('nid' => $this->nodes[3]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + + // Test offset for between operator. + $view = $this->getView($saved_view); + $view->initHandlers(); + $view->filter['created']->operator = 'between'; + $view->filter['created']->value['type'] = 'offset'; + $view->filter['created']->value['max'] = '+2 days'; + $view->filter['created']->value['min'] = '+1 hour'; + $view->executeDisplay('default'); + $expected_result = array( + array('nid' => $this->nodes[3]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + } + + /** + * Tests the filter operator between/not between. + */ + function testBetween() { + $saved_view = $this->createViewFromConfig('test_filter_date_between'); + + // Test between with min and max. + $view = $this->getView($saved_view); + $view->initHandlers(); + $view->filter['created']->operator = 'between'; + $view->filter['created']->value['min'] = format_date(150000, 'custom', 'Y-m-d H:s'); + $view->filter['created']->value['max'] = format_date(250000, 'custom', 'Y-m-d H:s'); + $view->executeDisplay('default'); + $expected_result = array( + array('nid' => $this->nodes[1]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + + // Test between with just max. + $view = $this->getView($saved_view); + $view->initHandlers(); + $view->filter['created']->operator = 'between'; + $view->filter['created']->value['max'] = format_date(250000, 'custom', 'Y-m-d H:s'); + $view->executeDisplay('default'); + $expected_result = array( + array('nid' => $this->nodes[0]->nid), + array('nid' => $this->nodes[1]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + + // Test not between with min and max. + $view = $this->getView($saved_view); + $view->initHandlers(); + $view->filter['created']->operator = 'not between'; + $view->filter['created']->value['min'] = format_date(150000, 'custom', 'Y-m-d H:s'); + $view->filter['created']->value['max'] = format_date(250000, 'custom', 'Y-m-d H:s'); + $view->executeDisplay('default'); + $expected_result = array( + array('nid' => $this->nodes[0]->nid), + array('nid' => $this->nodes[2]->nid), + array('nid' => $this->nodes[3]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + + // Test not between with just max. + $view = $this->getView($saved_view); + $view->initHandlers(); + $view->filter['created']->operator = 'not between'; + $view->filter['created']->value['max'] = format_date(150000, 'custom', 'Y-m-d H:s'); + $view->executeDisplay('default'); + $expected_result = array( + array('nid' => $this->nodes[1]->nid), + array('nid' => $this->nodes[2]->nid), + array('nid' => $this->nodes[3]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + } + + /** + * Make sure the validation callbacks works. + */ + function testUiValidation() { + $view = $this->createViewFromConfig('test_filter_date_between'); + $view->save(); + + $this->drupalLogin($this->drupalCreateUser(array('administer views', 'administer site configuration'))); + menu_router_rebuild(); + $this->drupalGet('admin/structure/views/view/test_filter_date_between/edit'); + $this->drupalGet('admin/structure/views/nojs/config-item/test_filter_date_between/default/filter/created'); + + $edit = array(); + // Generate a definitive wrong value, which should be checked by validation. + $edit['options[value][value]'] = $this->randomString() . '-------'; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertText(t('Invalid date format.'), 'Make sure that validation is runned and the invalidate date format is identified.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php new file mode 100644 index 0000000..986937f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php @@ -0,0 +1,180 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FilterEqualityTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\filter\Equality handler. + */ +class FilterEqualityTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Filter: Equality', + 'description' => 'Test the core Drupal\views\Plugin\views\filter\Equality handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->column_map = array( + 'views_test_data_name' => 'name', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['name']['filter']['id'] = 'equality'; + + return $data; + } + + function testEqual() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => '=', + 'value' => array('value' => 'Ringo'), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testEqualGroupedExposed() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: =, Value: Ringo + $filters['name']['group_info']['default_group'] = 1; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testNotEqual() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testEqualGroupedNotExposed() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: !=, Value: Ringo + $filters['name']['group_info']['default_group'] = 2; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + protected function getGroupedExposedFilters() { + $filters = array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'group' => 1, + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'name_op', + 'label' => 'name', + 'identifier' => 'name', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'name', + 'identifier' => 'name', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Name is equal to Ringo', + 'operator' => '=', + 'value' => array('value' => 'Ringo'), + ), + 2 => array( + 'title' => 'Name is not equal to Ringo', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + ), + ), + ), + ); + return $filters; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php new file mode 100644 index 0000000..6854592 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php @@ -0,0 +1,204 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FilterInOperatorTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\filter\InOperator handler. + */ +class FilterInOperatorTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Filter: In-operator', + 'description' => 'Test the core Drupal\views\Plugin\views\filter\InOperator handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['age']['filter']['id'] = 'in_operator'; + + return $data; + } + + public function testFilterInOperatorSimple() { + $view = $this->getView(); + + // Add a in_operator ordering. + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'id' => 'age', + 'field' => 'age', + 'table' => 'views_test_data', + 'value' => array(26, 30), + 'operator' => 'in', + ), + )); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + + $this->assertEqual(2, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + + $view = $this->getView(); + + // Add a in_operator ordering. + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'id' => 'age', + 'field' => 'age', + 'table' => 'views_test_data', + 'value' => array(26, 30), + 'operator' => 'not in', + ), + )); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + + $this->assertEqual(3, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + } + + public function testFilterInOperatorGroupedExposedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: in, Value: 26, 30 + $filters['age']['group_info']['default_group'] = 1; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + + $this->assertEqual(2, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + } + + public function testFilterNotInOperatorGroupedExposedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: in, Value: 26, 30 + $filters['age']['group_info']['default_group'] = 2; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + + $this->assertEqual(3, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + } + + protected function getGroupedExposedFilters() { + $filters = array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'age_op', + 'label' => 'age', + 'identifier' => 'age', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'age', + 'identifier' => 'age', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Age is one of 26, 30', + 'operator' => 'in', + 'value' => array(26, 30), + ), + 2 => array( + 'title' => 'Age is not one of 26, 30', + 'operator' => 'not in', + 'value' => array(26, 30), + ), + ), + ), + ), + ); + return $filters; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterNumericTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterNumericTest.php new file mode 100644 index 0000000..c43150e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterNumericTest.php @@ -0,0 +1,414 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FilterNumericTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the numeric filter handler. + */ +class FilterNumericTest extends HandlerTestBase { + + var $column_map = array(); + + public static function getInfo() { + return array( + 'name' => 'Filter: Numeric', + 'description' => 'Tests the numeric filter handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->column_map = array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['age']['filter']['allow empty'] = TRUE; + $data['views_test_data']['id']['filter']['allow empty'] = FALSE; + + return $data; + } + + public function testFilterNumericSimple() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => '=', + 'value' => array('value' => 28), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: =, Value: 28 + $filters['age']['group_info']['default_group'] = 1; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericBetween() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => 'between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + + // test not between + $view->destroy(); + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => 'not between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedBetween() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: between, Value: 26 and 29 + $filters['age']['group_info']['default_group'] = 2; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedNotBetween() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: between, Value: 26 and 29 + $filters['age']['group_info']['default_group'] = 3; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + public function testFilterNumericEmpty() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => 'empty', + ), + )); + + $this->executeView($view); + $resultset = array( + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + + $view->destroy(); + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => 'not empty', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + public function testFilterNumericExposedGroupedEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: empty, Value: + $filters['age']['group_info']['default_group'] = 4; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + + $this->executeView($view); + $resultset = array( + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedNotEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: empty, Value: + $filters['age']['group_info']['default_group'] = 5; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + public function testAllowEmpty() { + $view = $this->getView(); + + $view->displayHandlers['default']->overrideOption('filters', array( + 'id' => array( + 'id' => 'id', + 'table' => 'views_test_data', + 'field' => 'id', + 'relationship' => 'none', + ), + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + $view->initHandlers(); + + $id_operators = $view->filter['id']->operators(); + $age_operators = $view->filter['age']->operators(); + + $this->assertFalse(isset($id_operators['empty'])); + $this->assertFalse(isset($id_operators['not empty'])); + $this->assertTrue(isset($age_operators['empty'])); + $this->assertTrue(isset($age_operators['not empty'])); + } + + protected function getGroupedExposedFilters() { + $filters = array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'age_op', + 'label' => 'age', + 'identifier' => 'age', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'age', + 'identifier' => 'age', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Age is 28', + 'operator' => '=', + 'value' => array('value' => 28), + ), + 2 => array( + 'title' => 'Age is between 26 and 29', + 'operator' => 'between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + 3 => array( + 'title' => 'Age is not between 26 and 29', + 'operator' => 'not between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + 4 => array( + 'title' => 'Age is empty', + 'operator' => 'empty', + ), + 5 => array( + 'title' => 'Age is not empty', + 'operator' => 'not empty', + ), + ), + ), + ), + ); + return $filters; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterStringTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterStringTest.php new file mode 100644 index 0000000..1ff40e0 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterStringTest.php @@ -0,0 +1,816 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\FilterStringTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the core Drupal\views\Plugin\views\filter\String handler. + */ +class FilterStringTest extends HandlerTestBase { + + var $column_map = array(); + + public static function getInfo() { + return array( + 'name' => 'Filter: String', + 'description' => 'Tests the core Drupal\views\Plugin\views\filter\String handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->column_map = array( + 'views_test_data_name' => 'name', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['name']['filter']['allow empty'] = TRUE; + $data['views_test_data']['job']['filter']['allow empty'] = FALSE; + $data['views_test_data']['description'] = $data['views_test_data']['name']; + + return $data; + } + + protected function schemaDefinition() { + $schema = parent::schemaDefinition(); + $schema['views_test_data']['fields']['description'] = array( + 'description' => "A person's description", + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + ); + + return $schema; + } + + /** + * An extended test dataset. + */ + protected function dataSet() { + $dataset = parent::dataSet(); + $dataset[0]['description'] = 'John Winston Ono Lennon, MBE (9 October 1940 – 8 December 1980) was an English musician and singer-songwriter who rose to worldwide fame as one of the founding members of The Beatles, one of the most commercially successful and critically acclaimed acts in the history of popular music. Along with fellow Beatle Paul McCartney, he formed one of the most successful songwriting partnerships of the 20th century.'; + $dataset[1]['description'] = 'George Harrison,[1] MBE (25 February 1943 – 29 November 2001)[2] was an English rock guitarist, singer-songwriter, actor and film producer who achieved international fame as lead guitarist of The Beatles.'; + $dataset[2]['description'] = 'Richard Starkey, MBE (born 7 July 1940), better known by his stage name Ringo Starr, is an English musician, singer-songwriter, and actor who gained worldwide fame as the drummer for The Beatles.'; + $dataset[3]['description'] = 'Sir James Paul McCartney, MBE (born 18 June 1942) is an English musician, singer-songwriter and composer. Formerly of The Beatles (1960–1970) and Wings (1971–1981), McCartney is the most commercially successful songwriter in the history of popular music, according to Guinness World Records.[1]'; + $dataset[4]['description'] = NULL; + + return $dataset; + } + + protected function getBasicView() { + $view = parent::getBasicView(); + $view->displayHandlers['default']->options['fields']['description'] = array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + ); + return $view; + } + + function testFilterStringEqual() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => '=', + 'value' => 'Ringo', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedEqual() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: =, Value: Ringo + $filters['name']['group_info']['default_group'] = 1; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNotEqual() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedNotEqual() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: !=, Value: Ringo + $filters['name']['group_info']['default_group'] = '2'; + + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringContains() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => 'contains', + 'value' => 'ing', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringGroupedExposedContains() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: contains, Value: ing + $filters['name']['group_info']['default_group'] = '3'; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringWord() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'word', + 'value' => 'actor', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + $view->destroy(); + + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'allwords', + 'value' => 'Richard Starkey', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringGroupedExposedWord() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: contains, Value: ing + $filters['name']['group_info']['default_group'] = '3'; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + $view->destroy(); + + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: contains, Value: actor + $filters['description']['group_info']['default_group'] = '1'; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringStarts() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'starts', + 'value' => 'George', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedStarts() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: starts, Value: George + $filters['description']['group_info']['default_group'] = 2; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'George', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNotStarts() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'not_starts', + 'value' => 'George', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Ringo', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedNotStarts() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: not_starts, Value: George + $filters['description']['group_info']['default_group'] = 3; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Ringo', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringEnds() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'ends', + 'value' => 'Beatles.', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedEnds() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Descriptino, Operator: ends, Value: Beatles + $filters['description']['group_info']['default_group'] = 4; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNotEnds() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'not_ends', + 'value' => 'Beatles.', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedNotEnds() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: not_ends, Value: Beatles + $filters['description']['group_info']['default_group'] = 5; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNot() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'not', + 'value' => 'Beatles.', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringGroupedExposedNot() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: not (does not contains), Value: Beatles + $filters['description']['group_info']['default_group'] = 6; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + + } + + function testFilterStringShorter() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => 'shorterthan', + 'value' => 5, + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedShorter() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: shorterthan, Value: 5 + $filters['name']['group_info']['default_group'] = 4; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringLonger() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => 'longerthan', + 'value' => 7, + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedLonger() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: longerthan, Value: 4 + $filters['name']['group_info']['default_group'] = 5; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringEmpty() { + $view = $this->getView(); + + // Change the filtering + $view->displayHandlers['default']->overrideOption('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'empty', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: empty, Value: + $filters['description']['group_info']['default_group'] = 7; + $view->setDisplay('page_1'); + $view->displayHandlers['page_1']->overrideOption('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + protected function getGroupedExposedFilters() { + $filters = array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'name_op', + 'label' => 'name', + 'identifier' => 'name', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'name', + 'identifier' => 'name', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Is Ringo', + 'operator' => '=', + 'value' => 'Ringo', + ), + 2 => array( + 'title' => 'Is not Ringo', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + 3 => array( + 'title' => 'Contains ing', + 'operator' => 'contains', + 'value' => 'ing', + ), + 4 => array( + 'title' => 'Shorter than 5 letters', + 'operator' => 'shorterthan', + 'value' => 5, + ), + 5 => array( + 'title' => 'Longer than 7 letters', + 'operator' => 'longerthan', + 'value' => 7, + ), + ), + ), + ), + 'description' => array( + 'id' => 'description', + 'table' => 'views_test_data', + 'field' => 'description', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'description_op', + 'label' => 'description', + 'identifier' => 'description', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'description', + 'identifier' => 'description', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Contains the word: Actor', + 'operator' => 'word', + 'value' => 'actor', + ), + 2 => array( + 'title' => 'Starts with George', + 'operator' => 'starts', + 'value' => 'George', + ), + 3 => array( + 'title' => 'Not Starts with: George', + 'operator' => 'not_starts', + 'value' => 'George', + ), + 4 => array( + 'title' => 'Ends with: Beatles', + 'operator' => 'ends', + 'value' => 'Beatles.', + ), + 5 => array( + 'title' => 'Not Ends with: Beatles', + 'operator' => 'not_ends', + 'value' => 'Beatles.', + ), + 6 => array( + 'title' => 'Does not contain: Beatles', + 'operator' => 'not', + 'value' => 'Beatles.', + ), + 7 => array( + 'title' => 'Empty description', + 'operator' => 'empty', + 'value' => '', + ), + ), + ), + ), + ); + return $filters; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAliasTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAliasTest.php new file mode 100644 index 0000000..6be08fa --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAliasTest.php @@ -0,0 +1,88 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\HandlerAliasTest. + */ + +namespace Drupal\views\Tests\Handler; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Tests abstract handlers of views. + */ +class HandlerAliasTest extends ViewTestBase { + + public static function getInfo() { + return array( + 'name' => 'Handler alias tests', + 'description' => 'Tests handler table and field aliases.', + 'group' => 'Views', + ); + } + + protected function setUp() { + parent::setUp(); + + // Create a new user for the 'real table'. + $this->user = $this->drupalCreateUser(); + + $this->enableViewsTestModule(); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + */ + protected function viewsData() { + $data = parent::viewsData(); + // User the existing test_filter plugin. + $data['views_test_data_alias']['table']['real table'] = 'views_test_data'; + $data['views_test_data_alias']['name_alias']['filter']['id'] = 'test_filter'; + $data['views_test_data_alias']['name_alias']['filter']['real field'] = 'name'; + + return $data; + } + + public function testPluginAliases() { + $view = views_get_view('test_filter'); + $view->initDisplay(); + + // Change the filtering. + $view->displayHandlers['default']->overrideOption('filters', array( + 'test_filter' => array( + 'id' => 'test_filter', + 'table' => 'views_test_data_alias', + 'field' => 'name_alias', + 'operator' => '=', + 'value' => 'John', + 'group' => 0, + ), + )); + + $this->executeView($view); + + $filter = $view->filter['test_filter']; + + // Check the definition values are present. + $this->assertIdentical($filter->definition['real table'], 'views_test_data'); + $this->assertIdentical($filter->definition['real field'], 'name'); + + $this->assertIdentical($filter->table, 'views_test_data'); + $this->assertIdentical($filter->realField, 'name'); + + // Test an existing user uid field. + $view = views_get_view('test_alias'); + $view->initDisplay(); + $this->executeView($view); + + $filter = $view->filter['uid_raw']; + + $this->assertIdentical($filter->definition['real field'], 'uid'); + + $this->assertIdentical($filter->field, 'uid_raw'); + $this->assertIdentical($filter->table, 'users'); + $this->assertIdentical($filter->realField, 'uid'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAllTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAllTest.php new file mode 100644 index 0000000..8b5bb3a --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerAllTest.php @@ -0,0 +1,106 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\HandlerAllTest. + */ + +namespace Drupal\views\Tests\Handler; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\HandlerBase; +use Drupal\views\Plugin\views\filter\InOperator; + +/** + * Creates views with instances of all handlers... + */ +class HandlerAllTest extends HandlerTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array( + 'aggregator', + 'book', + 'block', + 'comment', + 'contact', + 'field', + 'filter', + 'file', + 'language', + 'locale', + 'node', + 'search', + 'statistics', + 'system', + 'taxonomy', + 'translation', + 'user' + ); + + public static function getInfo() { + return array( + 'name' => 'Handlers: All', + 'description' => 'Test instances of all handlers.', + 'group' => 'Views Handlers', + ); + } + + /** + * Tests most of the handlers. + */ + public function testHandlers() { + $object_types = array_keys(ViewExecutable::viewsHandlerTypes()); + foreach (views_fetch_data() as $base_table => $info) { + if (!isset($info['table']['base'])) { + continue; + } + + $view = views_new_view(); + $view->base_table = $base_table; + $view = new ViewExecutable($view); + + // @todo The groupwise relationship is currently broken. + $exclude[] = 'taxonomy_term_data:tid_representative'; + $exclude[] = 'users:uid_representative'; + + // Go through all fields and there through all handler types. + foreach ($info as $field => $field_info) { + // Table is a reserved key for the metainformation. + if ($field != 'table' && !in_array("$base_table:$field", $exclude)) { + foreach ($object_types as $type) { + if (isset($field_info[$type]['id'])) { + $options = array(); + if ($type == 'filter') { + $handler = views_get_handler($base_table, $field, $type); + if ($handler instanceof InOperator) { + $options['value'] = array(1); + } + } + $view->addItem('default', $type, $base_table, $field, $options); + } + } + } + } + + // Go through each step invidiually to see whether some parts are failing. + $view->build(); + $view->preExecute(); + $view->execute(); + $view->render(); + + // Make sure all handlers extend the HandlerBase. + foreach ($object_types as $type) { + if (isset($view->{$type})) { + foreach ($view->{$type} as $handler) { + $this->assertTrue($handler instanceof HandlerBase); + } + } + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerTest.php new file mode 100644 index 0000000..ad2c30a --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerTest.php @@ -0,0 +1,340 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\HandlerTest. + */ + +namespace Drupal\views\Tests\Handler; + +use Drupal\views\ViewExecutable; +use Drupal\views\Tests\ViewTestBase; +use Drupal\views\Plugin\views\HandlerBase; + +/** + * Tests abstract handlers of views. + */ +class HandlerTest extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui', 'comment', 'node'); + + public static function getInfo() { + return array( + 'name' => 'Handler: Base', + 'description' => 'Tests abstract handler definitions.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + */ + protected function viewsData() { + $data = parent::viewsData(); + // Override the name handler to be able to call placeholder() from outside. + $data['views_test_data']['name']['field']['id'] = 'test_field'; + + // Setup one field with an access callback and one with an access callback + // and arguments. + $data['views_test_data']['access_callback'] = $data['views_test_data']['id']; + $data['views_test_data']['access_callback_arguments'] = $data['views_test_data']['id']; + foreach (ViewExecutable::viewsHandlerTypes() as $type => $info) { + if (isset($data['views_test_data']['access_callback'][$type]['id'])) { + $data['views_test_data']['access_callback'][$type]['access callback'] = 'views_test_data_handler_test_access_callback'; + + $data['views_test_data']['access_callback_arguments'][$type]['access callback'] = 'views_test_data_handler_test_access_callback_argument'; + $data['views_test_data']['access_callback_arguments'][$type]['access arguments'] = array(TRUE); + } + } + + return $data; + } + + /** + * @todo + * This should probably moved to a filter related test. + */ + function testFilterInOperatorUi() { + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + menu_router_rebuild(); + + $path = 'admin/structure/views/nojs/config-item/test_filter_in_operator_ui/default/filter/type'; + $this->drupalGet($path); + $this->assertFieldByName('options[expose][reduce]', FALSE); + + $edit = array( + 'options[expose][reduce]' => TRUE, + ); + $this->drupalPost($path, $edit, t('Apply')); + $this->drupalGet($path); + $this->assertFieldByName('options[expose][reduce]', TRUE); + } + + /** + * Tests the breakPhraseString() method. + */ + function testBreakPhraseString() { + $empty_stdclass = new \stdClass(); + $empty_stdclass->operator = 'or'; + $empty_stdclass->value = array(); + + // check defaults + $null = NULL; + $this->assertEqual($empty_stdclass, HandlerBase::breakPhraseString('', $null)); + + $handler = views_get_handler('node', 'title', 'argument'); + $this->assertEqual($handler, HandlerBase::breakPhraseString('', $handler), 'The breakPhraseString() method works correctly.'); + + // test ors + $handler = HandlerBase::breakPhraseString('word1 word2+word'); + $this->assertEqualValue(array('word1', 'word2', 'word'), $handler); + $this->assertEqual('or', $handler->operator); + $handler = HandlerBase::breakPhraseString('word1+word2+word'); + $this->assertEqualValue(array('word1', 'word2', 'word'), $handler); + $this->assertEqual('or', $handler->operator); + $handler = HandlerBase::breakPhraseString('word1 word2 word'); + $this->assertEqualValue(array('word1', 'word2', 'word'), $handler); + $this->assertEqual('or', $handler->operator); + $handler = HandlerBase::breakPhraseString('word-1+word-2+word'); + $this->assertEqualValue(array('word-1', 'word-2', 'word'), $handler); + $this->assertEqual('or', $handler->operator); + $handler = HandlerBase::breakPhraseString('wõrd1+wõrd2+wõrd'); + $this->assertEqualValue(array('wõrd1', 'wõrd2', 'wõrd'), $handler); + $this->assertEqual('or', $handler->operator); + + // test ands. + $handler = HandlerBase::breakPhraseString('word1,word2,word'); + $this->assertEqualValue(array('word1', 'word2', 'word'), $handler); + $this->assertEqual('and', $handler->operator); + $handler = HandlerBase::breakPhraseString('word1 word2,word'); + $this->assertEqualValue(array('word1 word2', 'word'), $handler); + $this->assertEqual('and', $handler->operator); + $handler = HandlerBase::breakPhraseString('word1,word2 word'); + $this->assertEqualValue(array('word1', 'word2 word'), $handler); + $this->assertEqual('and', $handler->operator); + $handler = HandlerBase::breakPhraseString('word-1,word-2,word'); + $this->assertEqualValue(array('word-1', 'word-2', 'word'), $handler); + $this->assertEqual('and', $handler->operator); + $handler = HandlerBase::breakPhraseString('wõrd1,wõrd2,wõrd'); + $this->assertEqualValue(array('wõrd1', 'wõrd2', 'wõrd'), $handler); + $this->assertEqual('and', $handler->operator); + + // test a single word + $handler = HandlerBase::breakPhraseString('word'); + $this->assertEqualValue(array('word'), $handler); + $this->assertEqual('and', $handler->operator); + } + + /** + * Tests Drupal\views\Plugin\views\HandlerBase::breakPhrase() function. + */ + function testBreakPhrase() { + $empty_stdclass = new \stdClass(); + $empty_stdclass->operator = 'or'; + $empty_stdclass->value = array(); + + $null = NULL; + // check defaults + $this->assertEqual($empty_stdclass, HandlerBase::breakPhrase('', $null)); + + $handler = views_get_handler('node', 'title', 'argument'); + $this->assertEqual($handler, HandlerBase::breakPhrase('', $handler), 'The breakPhrase() method works correctly.'); + + // Generate three random numbers which can be used below; + $n1 = rand(0, 100); + $n2 = rand(0, 100); + $n3 = rand(0, 100); + // test ors + $this->assertEqualValue(array($n1, $n2, $n3), HandlerBase::breakPhrase("$n1 $n2+$n3", $handler)); + $this->assertEqual('or', $handler->operator); + $this->assertEqualValue(array($n1, $n2, $n3), HandlerBase::breakPhrase("$n1+$n2+$n3", $handler)); + $this->assertEqual('or', $handler->operator); + $this->assertEqualValue(array($n1, $n2, $n3), HandlerBase::breakPhrase("$n1 $n2 $n3", $handler)); + $this->assertEqual('or', $handler->operator); + $this->assertEqualValue(array($n1, $n2, $n3), HandlerBase::breakPhrase("$n1 $n2++$n3", $handler)); + $this->assertEqual('or', $handler->operator); + + // test ands. + $this->assertEqualValue(array($n1, $n2, $n3), HandlerBase::breakPhrase("$n1,$n2,$n3", $handler)); + $this->assertEqual('and', $handler->operator); + $this->assertEqualValue(array($n1, $n2, $n3), HandlerBase::breakPhrase("$n1,,$n2,$n3", $handler)); + $this->assertEqual('and', $handler->operator); + } + + /** + * Check to see if a value is the same as the value on a certain handler. + * + * @param $first + * The first value to check. + * @param Drupal\views\Plugin\views\HandlerBase $handler + * The handler that has the $handler->value property to compare with first. + * @param string $message + * The message to display along with the assertion. + * @param string $group + * The type of assertion - examples are "Browser", "PHP". + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertEqualValue($first, $handler, $message = '', $group = 'Other') { + return $this->assert($first == $handler->value, $message ? $message : t('First value is equal to second value'), $group); + } + + /** + * Tests the relationship ui for field/filter/argument/relationship. + */ + public function testRelationshipUI() { + $views_admin = $this->drupalCreateUser(array('administer views')); + $this->drupalLogin($views_admin); + + // Make sure the link to the field options exists. + $handler_options_path = 'admin/structure/views/nojs/config-item/test_handler_relationships/default/field/title'; + $view_edit_path = 'admin/structure/views/view/test_handler_relationships/edit'; + $this->drupalGet($view_edit_path); + $this->assertLinkByHref($handler_options_path); + + // The test view has a relationship to node_revision so the field should + // show a relationship selection. + + $this->drupalGet($handler_options_path); + $relationship_name = 'options[relationship]'; + $this->assertFieldByName($relationship_name); + + // Check for available options. + $xpath = $this->constructFieldXpath('name', $relationship_name); + $fields = $this->xpath($xpath); + $options = array(); + foreach ($fields as $field) { + $items = $this->getAllOptions($field); + foreach ($items as $item) { + $options[] = $item->attributes()->value; + } + } + $expected_options = array('none', 'nid'); + $this->assertEqual($options, $expected_options); + + // Remove the relationship and take sure no relationship option appears. + $this->drupalPost('admin/structure/views/nojs/config-item/test_handler_relationships/default/relationship/nid', array(), t('Remove')); + $this->drupalGet($handler_options_path); + $this->assertNoFieldByName($relationship_name, 'Make sure that no relationship option is available'); + } + + /** + * Tests the relationship method on the base class. + */ + public function testSetRelationship() { + $view = $this->createViewFromConfig('test_handler_relationships'); + // Setup a broken relationship. + $view->addItem('default', 'relationship', $this->randomName(), $this->randomName(), array(), 'broken_relationship'); + // Setup a valid relationship. + $view->addItem('default', 'relationship', 'comment', 'nid', array('relationship' => 'cid'), 'valid_relationship'); + $view->initHandlers(); + $field = $view->field['title']; + + $field->options['relationship'] = NULL; + $field->setRelationship(); + $this->assertFalse($field->relationship, 'Make sure that an empty relationship does not create a relationship on the field.'); + + $field->options['relationship'] = $this->randomName(); + $field->setRelationship(); + $this->assertFalse($field->relationship, 'Make sure that a random relationship does not create a relationship on the field.'); + + $field->options['relationship'] = 'broken_relationship'; + $field->setRelationship(); + $this->assertFalse($field->relationship, 'Make sure that a broken relationship does not create a relationship on the field.'); + + $field->options['relationship'] = 'valid_relationship'; + $field->setRelationship(); + $this->assertFalse(!empty($field->relationship), 'Make sure that the relationship alias was not set without building a views query before.'); + + // Remove the invalid relationship. + unset($view->relationship['broken_relationship']); + + $view->build(); + $field->setRelationship(); + $this->assertEqual($field->relationship, $view->relationship['valid_relationship']->alias, 'Make sure that a valid relationship does create the right relationship query alias.'); + } + + /** + * Tests the placeholder function. + * + * @see Drupal\views\Plugin\views\HandlerBase::placeholder() + */ + public function testPlaceholder() { + $view = $this->getView(); + $view->initHandlers(); + $view->initQuery(); + + $handler = $view->field['name']; + $table = $handler->table; + $field = $handler->field; + $string = ':' . $table . '_' . $field; + + // Make sure the placeholder variables are like expected. + $this->assertEqual($handler->getPlaceholder(), $string); + $this->assertEqual($handler->getPlaceholder(), $string . 1); + $this->assertEqual($handler->getPlaceholder(), $string . 2); + + // Set another table/field combination and make sure there are new + // placeholders. + $table = $handler->table = $this->randomName(); + $field = $handler->field = $this->randomName(); + $string = ':' . $table . '_' . $field; + + // Make sure the placeholder variables are like expected. + $this->assertEqual($handler->getPlaceholder(), $string); + $this->assertEqual($handler->getPlaceholder(), $string . 1); + $this->assertEqual($handler->getPlaceholder(), $string . 2); + } + + /** + * Tests access to a handler. + * + * @see views_test_data_handler_test_access_callback + */ + public function testAccess() { + $view = views_get_view('test_handler_test_access'); + $views_data = $this->viewsData(); + $views_data = $views_data['views_test_data']; + + // Enable access to callback only field and deny for callback + arguments. + config('views_test_data.tests')->set('handler_access_callback', TRUE)->save(); + config('views_test_data.tests')->set('handler_access_callback_argument', FALSE)->save(); + $view->initDisplay(); + $view->initHandlers(); + + foreach ($views_data['access_callback'] as $type => $info) { + if (!in_array($type, array('title', 'help'))) { + $this->assertTrue($view->field['access_callback'] instanceof HandlerBase, 'Make sure the user got access to the access_callback field '); + $this->assertFalse(isset($view->field['access_callback_arguments']), 'Make sure the user got no access to the access_callback_arguments field '); + } + } + + // Enable access to the callback + argument handlers and deny for callback. + config('views_test_data.tests')->set('handler_access_callback', FALSE)->save(); + config('views_test_data.tests')->set('handler_access_callback_argument', TRUE)->save(); + $view->destroy(); + $view->initDisplay(); + $view->initHandlers(); + + foreach ($views_data['access_callback'] as $type => $info) { + if (!in_array($type, array('title', 'help'))) { + $this->assertFalse(isset($view->field['access_callback']), 'Make sure the user got no access to the access_callback field '); + $this->assertTrue($view->field['access_callback_arguments'] instanceof HandlerBase, 'Make sure the user got access to the access_callback_arguments field '); + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerTestBase.php new file mode 100644 index 0000000..ce20f78 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/HandlerTestBase.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\HandlerTestBase. + */ + +namespace Drupal\views\Tests\Handler; + +use Drupal\views\Tests\ViewTestBase; + +/** + * @todo. + */ +abstract class HandlerTestBase extends ViewTestBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/RelationshipTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/RelationshipTest.php new file mode 100644 index 0000000..5358eda --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/RelationshipTest.php @@ -0,0 +1,181 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\RelationshipTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests the base relationship handler. + * + * @see Drupal\views\Plugin\views\relationship\RelationshipPluginBase + */ +class RelationshipTest extends HandlerTestBase { + + /** + * Maps between the key in the expected result and the query result. + * + * @var array + */ + protected $columnMap = array( + 'views_test_data_name' => 'name', + 'users_views_test_data_uid' => 'uid', + ); + + public static function getInfo() { + return array( + 'name' => 'Relationship: Standard', + 'description' => 'Tests the base relationship handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + + /** + * Overrides Drupal\views\Tests\ViewTestBase::schemaDefinition(). + * + * Adds a uid column to test the relationships. + * + * @return array + */ + protected function schemaDefinition() { + $schema = parent::schemaDefinition(); + + $schema['views_test_data']['fields']['uid'] = array( + 'description' => "The {users}.uid of the author of the beatle entry.", + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0 + ); + + return $schema; + } + + + /** + * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + * + * Adds a relationship for the uid column. + * + * @return array + */ + protected function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['uid'] = array( + 'title' => t('UID'), + 'help' => t('The test data UID'), + 'relationship' => array( + 'id' => 'standard', + 'base' => 'users', + 'base field' => 'uid' + ) + ); + + return $data; + } + + /** + * Tests the query result of a view with a relationship. + */ + public function testRelationshipQuery() { + // Set the first entry to have the admin as author. + db_query("UPDATE {views_test_data} SET uid = 1 WHERE id = 1"); + db_query("UPDATE {views_test_data} SET uid = 2 WHERE id <> 1"); + + $view = $this->getBasicView(); + + $view->displayHandlers['default']->overrideOption('relationships', array( + 'uid' => array( + 'id' => 'uid', + 'table' => 'views_test_data', + 'field' => 'uid', + ), + )); + + $view->displayHandlers['default']->overrideOption('filters', array( + 'uid' => array( + 'id' => 'uid', + 'table' => 'users', + 'field' => 'uid', + 'relationship' => 'uid', + ), + )); + + $fields = $view->displayHandlers['default']->getOption('fields'); + $view->displayHandlers['default']->overrideOption('fields', $fields + array( + 'uid' => array( + 'id' => 'uid', + 'table' => 'users', + 'field' => 'uid', + 'relationship' => 'uid', + ), + )); + + $view->initHandlers(); + + // Check for all beatles created by admin. + $view->filter['uid']->value = array(1); + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'John', + 'uid' => 1 + ) + ); + $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); + $view->destroy(); + + // Check for all beatles created by another user, which so doesn't exist. + $view->initHandlers(); + $view->filter['uid']->value = array(3); + $this->executeView($view); + $expected_result = array(); + $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); + $view->destroy(); + + // Set the relationship to required, so only results authored by the admin + // should return. + $view->initHandlers(); + $view->relationship['uid']->options['required'] = TRUE; + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'John', + 'uid' => 1 + ) + ); + $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); + $view->destroy(); + + // Set the relationship to optional should cause to return all beatles. + $view->initHandlers(); + $view->relationship['uid']->options['required'] = FALSE; + $this->executeView($view); + + $expected_result = $this->dataSet(); + // Alter the expected result to contain the right uids. + foreach ($expected_result as $key => &$row) { + // Only John has an existing author. + if ($row['name'] == 'John') { + $row['uid'] = 1; + } + else { + // The LEFT join should set an empty {users}.uid field. + $row['uid'] = NULL; + } + } + + $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); + } +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/SortDateTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/SortDateTest.php new file mode 100644 index 0000000..2861be3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/SortDateTest.php @@ -0,0 +1,208 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\SortDateTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests for core Drupal\views\Plugin\views\sort\Date handler. + */ +class SortDateTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Sort: Date', + 'description' => 'Test the core Drupal\views\Plugin\views\sort\Date handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + protected function expectedResultSet($granularity, $reverse = TRUE) { + $expected = array(); + if (!$reverse) { + switch ($granularity) { + case 'second': + $expected = array( + array('name' => 'John'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + array('name' => 'Ringo'), + array('name' => 'George'), + ); + break; + case 'minute': + $expected = array( + array('name' => 'John'), + array('name' => 'Paul'), + array('name' => 'Ringo'), + array('name' => 'Meredith'), + array('name' => 'George'), + ); + break; + case 'hour': + $expected = array( + array('name' => 'John'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + array('name' => 'George'), + ); + break; + case 'day': + $expected = array( + array('name' => 'John'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + array('name' => 'George'), + ); + break; + case 'month': + $expected = array( + array('name' => 'John'), + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + case 'year': + $expected = array( + array('name' => 'John'), + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + } + } + else { + switch ($granularity) { + case 'second': + $expected = array( + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Meredith'), + array('name' => 'Paul'), + array('name' => 'John'), + ); + break; + case 'minute': + $expected = array( + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Meredith'), + array('name' => 'Paul'), + array('name' => 'John'), + ); + break; + case 'hour': + $expected = array( + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + array('name' => 'John'), + ); + break; + case 'day': + $expected = array( + array('name' => 'George'), + array('name' => 'John'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + case 'month': + $expected = array( + array('name' => 'John'), + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + case 'year': + $expected = array( + array('name' => 'John'), + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + } + } + + return $expected; + } + + /** + * Tests numeric ordering of the result set. + */ + public function testDateOrdering() { + foreach (array('second', 'minute', 'hour', 'day', 'month', 'year') as $granularity) { + foreach (array(FALSE, TRUE) as $reverse) { + $view = $this->getView(); + + // Change the fields. + $view->displayHandlers['default']->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + 'created' => array( + 'id' => 'created', + 'table' => 'views_test_data', + 'field' => 'created', + 'relationship' => 'none', + ), + )); + + // Change the ordering + $view->displayHandlers['default']->overrideOption('sorts', array( + 'created' => array( + 'id' => 'created', + 'table' => 'views_test_data', + 'field' => 'created', + 'relationship' => 'none', + 'granularity' => $granularity, + 'order' => $reverse ? 'DESC' : 'ASC', + ), + 'id' => array( + 'id' => 'id', + 'table' => 'views_test_data', + 'field' => 'id', + 'relationship' => 'none', + 'order' => 'ASC', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->expectedResultSet($granularity, $reverse), array( + 'views_test_data_name' => 'name', + ), t('Result is returned correctly when ordering by granularity @granularity, @reverse.', array('@granularity' => $granularity, '@reverse' => $reverse ? t('reverse') : t('forward')))); + $view->destroy(); + unset($view); + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/SortRandomTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/SortRandomTest.php new file mode 100644 index 0000000..a6d2a2f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/SortRandomTest.php @@ -0,0 +1,99 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\SortRandomTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests for core Drupal\views\Plugin\views\sort\Random handler. + */ +class SortRandomTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Sort: Random', + 'description' => 'Test the core Drupal\views\Plugin\views\sort\Random handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Add more items to the test set, to make the order tests more robust. + */ + protected function dataSet() { + $data = parent::dataSet(); + for ($i = 0; $i < 50; $i++) { + $data[] = array( + 'name' => 'name_' . $i, + 'age' => $i, + 'job' => 'job_' . $i, + 'created' => rand(0, time()), + ); + } + return $data; + } + + /** + * Return a basic view with random ordering. + */ + protected function getBasicRandomView() { + $view = $this->getView(); + + // Add a random ordering. + $view->displayHandlers['default']->overrideOption('sorts', array( + 'random' => array( + 'id' => 'random', + 'field' => 'random', + 'table' => 'views', + ), + )); + + return $view; + } + + /** + * Tests random ordering of the result set. + * + * @see DatabaseSelectTestCase::testRandomOrder() + */ + public function testRandomOrdering() { + // Execute a basic view first. + $view = $this->getView(); + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->dataSet(), array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + + // Execute a random view, we expect the result set to be different. + $view_random = $this->getBasicRandomView(); + $this->executeView($view_random); + $this->assertEqual(count($this->dataSet()), count($view_random->result), t('The number of returned rows match.')); + $this->assertNotIdenticalResultset($view_random, $view->result, array( + 'views_test_data_name' => 'views_test_data_name', + 'views_test_data_age' => 'views_test_data_name', + )); + + // Execute a second random view, we expect the result set to be different again. + $view_random_2 = $this->getBasicRandomView(); + $this->executeView($view_random_2); + $this->assertEqual(count($this->dataSet()), count($view_random_2->result), t('The number of returned rows match.')); + $this->assertNotIdenticalResultset($view_random, $view->result, array( + 'views_test_data_name' => 'views_test_data_name', + 'views_test_data_age' => 'views_test_data_name', + )); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/SortTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/SortTest.php new file mode 100644 index 0000000..351f769 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/SortTest.php @@ -0,0 +1,131 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Handler\SortTest. + */ + +namespace Drupal\views\Tests\Handler; + +/** + * Tests for core Drupal\views\Plugin\views\sort\SortPluginBase handler. + */ +class SortTest extends HandlerTestBase { + + public static function getInfo() { + return array( + 'name' => 'Sort: Generic', + 'description' => 'Test the core Drupal\views\Plugin\views\sort\SortPluginBase handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Tests numeric ordering of the result set. + */ + public function testNumericOrdering() { + $view = $this->getView(); + + // Change the ordering + $view->displayHandlers['default']->overrideOption('sorts', array( + 'age' => array( + 'order' => 'ASC', + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->orderResultSet($this->dataSet(), 'age'), array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + + $view = $this->getView(); + + // Reverse the ordering + $view->displayHandlers['default']->overrideOption('sorts', array( + 'age' => array( + 'order' => 'DESC', + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->orderResultSet($this->dataSet(), 'age', TRUE), array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + } + + /** + * Tests string ordering of the result set. + */ + public function testStringOrdering() { + $view = $this->getView(); + + // Change the ordering + $view->displayHandlers['default']->overrideOption('sorts', array( + 'name' => array( + 'order' => 'ASC', + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->orderResultSet($this->dataSet(), 'name'), array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + + $view = $this->getView(); + + // Reverse the ordering + $view->displayHandlers['default']->overrideOption('sorts', array( + 'name' => array( + 'order' => 'DESC', + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->orderResultSet($this->dataSet(), 'name', TRUE), array( + 'views_test_data_name' => 'name', + 'views_test_data_age' => 'age', + )); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Language/ArgumentLanguage.php b/core/modules/views/lib/Drupal/views/Tests/Language/ArgumentLanguage.php new file mode 100644 index 0000000..73490af --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Language/ArgumentLanguage.php @@ -0,0 +1,47 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Language\ArgumentLanguage. + */ + +namespace Drupal\views\Tests\Language; + +use Drupal\Core\Language\Language; + +/** + * Tests the argument language handler. + * + * @see Views\language\Plugin\views\argument\Language.php + */ +class ArgumentLanguage extends LanguageTestBase { + + public static function getInfo() { + return array( + 'name' => 'Argument: Language', + 'description' => 'Tests the argument language handler.', + 'group' => 'Views Handlers' + ); + } + + public function testFilter() { + foreach (array('en' => 'John', 'xx-lolspeak' => 'George') as $langcode => $name) { + $view = $this->getView(); + $view->displayHandlers['default']->overrideOption('arguments', array( + 'langcode' => array( + 'id' => 'langcode', + 'table' => 'views_test_data', + 'field' => 'langcode', + ), + )); + $this->executeView($view, array($langcode)); + + $expected = array(array( + 'name' => $name, + )); + $this->assertIdenticalResultset($view, $expected, array('views_test_data_name' => 'name')); + $view->destroy(); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Language/FieldLanguage.php b/core/modules/views/lib/Drupal/views/Tests/Language/FieldLanguage.php new file mode 100644 index 0000000..0d282c6 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Language/FieldLanguage.php @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Language\FieldLanguage. + */ + +namespace Drupal\views\Tests\Language; + +use Drupal\Core\Language\Language; + +/** + * Tests the field language handler. + * + * @see Views\language\Plugin\views\field\Language + */ +class FieldLanguage extends LanguageTestBase { + + public static function getInfo() { + return array( + 'name' => 'Field: Language', + 'description' => 'Tests the field language handler.', + 'group' => 'Views Handlers', + ); + } + + public function testField() { + $view = $this->getView(); + $view->displayHandlers['default']->overrideOption('fields', array( + 'langcode' => array( + 'id' => 'langcode', + 'table' => 'views_test_data', + 'field' => 'langcode', + ), + )); + $this->executeView($view); + + $this->assertEqual($view->field['langcode']->advanced_render($view->result[0]), 'English'); + $this->assertEqual($view->field['langcode']->advanced_render($view->result[1]), 'Lolspeak'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Language/FilterLanguage.php b/core/modules/views/lib/Drupal/views/Tests/Language/FilterLanguage.php new file mode 100644 index 0000000..5d7b548 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Language/FilterLanguage.php @@ -0,0 +1,48 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Language\FilterLanguage. + */ + +namespace Drupal\views\Tests\Language; + +use Drupal\Core\Language\Language; + +/** + * Tests the filter language handler. + * + * @see Views\language\Plugin\views\filter\Language + */ +class FilterLanguage extends LanguageTestBase { + + public static function getInfo() { + return array( + 'name' => 'Filter: Language', + 'description' => 'Tests the filter language handler.', + 'group' => 'Views Handlers' + ); + } + + public function testFilter() { + foreach (array('en' => 'John', 'xx-lolspeak' => 'George') as $langcode => $name) { + $view = $this->getView(); + $view->displayHandlers['default']->overrideOption('filters', array( + 'langcode' => array( + 'id' => 'langcode', + 'table' => 'views_test_data', + 'field' => 'langcode', + 'value' => array($langcode), + ), + )); + $this->executeView($view); + + $expected = array(array( + 'name' => $name, + )); + $this->assertIdenticalResultset($view, $expected, array('views_test_data_name' => 'name')); + $view->destroy(); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Language/LanguageTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Language/LanguageTestBase.php new file mode 100644 index 0000000..5cd0bfd --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Language/LanguageTestBase.php @@ -0,0 +1,78 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Language\LanguageTestBase. + */ + +namespace Drupal\views\Tests\Language; + +use Drupal\views\Tests\ViewTestBase; +use Drupal\Core\Language\Language; + +/** + * Base class for all Language handler tests. + */ +abstract class LanguageTestBase extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('language'); + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + // Create another language beside english. + $language = new Language(array('langcode' => 'xx-lolspeak', 'name' => 'Lolspeak')); + language_save($language); + } + + + protected function schemaDefinition() { + $schema = parent::schemaDefinition(); + $schema['views_test_data']['fields']['langcode'] = array( + 'description' => 'The {language}.langcode of this beatle.', + 'type' => 'varchar', + 'length' => 12, + 'default' => '', + ); + + return $schema; + } + + protected function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['langcode'] = array( + 'title' => t('Langcode'), + 'help' => t('Langcode'), + 'field' => array( + 'id' => 'language', + ), + 'argument' => array( + 'id' => 'language', + ), + 'filter' => array( + 'id' => 'language', + ), + ); + + return $data; + } + + protected function dataSet() { + $data = parent::dataSet(); + $data[0]['langcode'] = 'en'; + $data[1]['langcode'] = 'xx-lolspeak'; + $data[2]['langcode'] = ''; + $data[3]['langcode'] = ''; + $data[4]['langcode'] = ''; + + return $data; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/ModuleTest.php b/core/modules/views/lib/Drupal/views/Tests/ModuleTest.php new file mode 100644 index 0000000..9998cbd --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/ModuleTest.php @@ -0,0 +1,243 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\ModuleTest. + */ + +namespace Drupal\views\Tests; + +/** + * Tests basic functions from the Views module. + */ +class ModuleTest extends ViewTestBase { + + public static function getInfo() { + return array( + 'name' => 'Views Module tests', + 'description' => 'Tests some basic functions of views.module.', + 'group' => 'Views', + ); + } + + public function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + drupal_theme_rebuild(); + } + + public function viewsData() { + $data = parent::viewsData(); + $data['views_test_data_previous'] = array(); + $data['views_test_data_previous']['id']['field']['moved to'] = array('views_test_data', 'id'); + $data['views_test_data_previous']['id']['filter']['moved to'] = array('views_test_data', 'id'); + $data['views_test_data']['age_previous']['field']['moved to'] = array('views_test_data', 'age'); + $data['views_test_data']['age_previous']['sort']['moved to'] = array('views_test_data', 'age'); + $data['views_test_data_previous']['name_previous']['field']['moved to'] = array('views_test_data', 'name'); + $data['views_test_data_previous']['name_previous']['argument']['moved to'] = array('views_test_data', 'name'); + + return $data; + } + + public function test_views_trim_text() { + // Test unicode, @see http://drupal.org/node/513396#comment-2839416 + $text = array( + 'Tuy nhiên, những hi vá»ng', + 'Giả sá»­ chúng tôi có 3 Apple', + 'siêu nhá» này là bá»™ xá»­ lý', + 'Di Ä‘á»™ng của nhà sản xuất Phần Lan', + 'khoảng cách từ đại lí đến', + 'của hãng bao gồm ba dòng', + 'Ñд аÑд аÑд аÑ', + 'аÑд аÑд аÑд аÑ' + ); + // Just test maxlength without word boundry. + $alter = array( + 'max_length' => 10, + ); + $expect = array( + 'Tuy nhiên,', + 'Giả sá»­ chú', + 'siêu nhá» n', + 'Di Ä‘á»™ng củ', + 'khoảng các', + 'của hãng b', + 'Ñд аÑд аÑд', + 'аÑд аÑд аÑ', + ); + + foreach ($text as $key => $line) { + $result_text = views_trim_text($alter, $line); + $this->assertEqual($result_text, $expect[$key]); + } + + // Test also word_boundary + $alter['word_boundary'] = TRUE; + $expect = array( + 'Tuy nhiên', + 'Giả sá»­', + 'siêu nhá»', + 'Di Ä‘á»™ng', + 'khoảng', + 'của hãng', + 'Ñд аÑд', + 'аÑд аÑд', + ); + + foreach ($text as $key => $line) { + $result_text = views_trim_text($alter, $line); + $this->assertEqual($result_text, $expect[$key]); + } + } + + /** + * Tests the views_get_handler method. + */ + function testviews_get_handler() { + $types = array('field', 'area', 'filter'); + foreach ($types as $type) { + $handler = views_get_handler($this->randomName(), $this->randomName(), $type); + $this->assertEqual('Drupal\views\Plugin\views\\' . $type . '\Broken', get_class($handler), t('Make sure that a broken handler of type: @type are created', array('@type' => $type))); + } + + $views_data = $this->viewsData(); + $test_tables = array('views_test_data' => array('id', 'name')); + foreach ($test_tables as $table => $fields) { + foreach ($fields as $field) { + $data = $views_data[$table][$field]; + foreach ($data as $id => $field_data) { + if (!in_array($id, array('title', 'help'))) { + $handler = views_get_handler($table, $field, $id); + $this->assertInstanceHandler($handler, $table, $field, $id); + } + } + } + } + + // Test the automatic conversion feature. + + // Test the automatic table renaming. + $handler = views_get_handler('views_test_data_previous', 'id', 'field'); + $this->assertInstanceHandler($handler, 'views_test_data', 'id', 'field'); + $handler = views_get_handler('views_test_data_previous', 'id', 'filter'); + $this->assertInstanceHandler($handler, 'views_test_data', 'id', 'filter'); + + // Test the automatic field renaming. + $handler = views_get_handler('views_test_data', 'age_previous', 'field'); + $this->assertInstanceHandler($handler, 'views_test_data', 'age', 'field'); + $handler = views_get_handler('views_test_data', 'age_previous', 'sort'); + $this->assertInstanceHandler($handler, 'views_test_data', 'age', 'sort'); + + // Test the automatic table and field renaming. + $handler = views_get_handler('views_test_data_previous', 'name_previous', 'field'); + $this->assertInstanceHandler($handler, 'views_test_data', 'name', 'field'); + $handler = views_get_handler('views_test_data_previous', 'name_previous', 'argument'); + $this->assertInstanceHandler($handler, 'views_test_data', 'name', 'argument'); + + // Test the override handler feature. + $handler = views_get_handler('views_test_data', 'job', 'filter', 'string'); + $this->assertEqual('Drupal\\views\\Plugin\\views\\filter\\String', get_class($handler)); + } + + /** + * Tests the load wrapper/helper functions. + */ + public function testLoadFunctions() { + $controller = entity_get_controller('view'); + + // Test views_view_is_enabled/disabled. + $load = $controller->load(array('archive')); + $archive = reset($load); + $this->assertTrue(views_view_is_disabled($archive), 'views_view_is_disabled works as expected.'); + // Enable the view and check this. + $archive->enable(); + $this->assertTrue(views_view_is_enabled($archive), ' views_view_is_enabled works as expected.'); + + // We can store this now, as we have enabled/disabled above. + $all_views = $controller->load(); + + // Test views_get_all_views(). + $this->assertIdentical(array_keys($all_views), array_keys(views_get_all_views()), 'views_get_all_views works as expected.'); + + // Test views_get_enabled_views(). + $expected_enabled = array_filter($all_views, function($view) { + return views_view_is_enabled($view); + }); + $this->assertIdentical(array_keys($expected_enabled), array_keys(views_get_enabled_views()), 'Expected enabled views returned.'); + + // Test views_get_disabled_views(). + $expected_disabled = array_filter($all_views, function($view) { + return views_view_is_disabled($view); + }); + $this->assertIdentical(array_keys($expected_disabled), array_keys(views_get_disabled_views()), 'Expected disabled views returned.'); + + // Test views_get_views_as_options(). + // Test the $views_only parameter. + $this->assertIdentical(array_keys($all_views), array_keys(views_get_views_as_options(TRUE)), 'Expected option keys for all views were returned.'); + $expected_options = array(); + foreach ($all_views as $id => $view) { + $expected_options[$id] = $view->getHumanName(); + } + $this->assertIdentical($expected_options, views_get_views_as_options(TRUE), 'Expected options array was returned.'); + + // Test the default. + $this->assertIdentical($this->formatViewOptions($all_views), views_get_views_as_options(), 'Expected options array for all views was returned.'); + // Test enabled views. + $this->assertIdentical($this->formatViewOptions($expected_enabled), views_get_views_as_options(FALSE, 'enabled'), 'Expected enabled options array was returned.'); + // Test disabled views. + $this->assertIdentical($this->formatViewOptions($expected_disabled), views_get_views_as_options(FALSE, 'disabled'), 'Expected disabled options array was returned.'); + + // Test the sort parameter. + $all_views_sorted = $all_views; + ksort($all_views_sorted); + $this->assertIdentical(array_keys($all_views_sorted), array_keys(views_get_views_as_options(TRUE, 'all', NULL, FALSE, TRUE)), 'All view id keys returned in expected sort order'); + + // Test $exclude_view parameter. + $this->assertFalse(array_key_exists('archive', views_get_views_as_options(TRUE, 'all', 'archive')), 'View excluded from options based on name'); + $this->assertFalse(array_key_exists('archive:default', views_get_views_as_options(FALSE, 'all', 'archive:default')), 'View display excluded from options based on name'); + $this->assertFalse(array_key_exists('archive', views_get_views_as_options(TRUE, 'all', $archive->getExecutable())), 'View excluded from options based on object'); + + // Test the $opt_group parameter. + $expected_opt_groups = array(); + foreach ($all_views as $id => $view) { + foreach ($view->display as $display_id => $display) { + $expected_opt_groups[$view->id()][$view->id() . ':' . $display['id']] = t('@view : @display', array('@view' => $view->id(), '@display' => $display['id'])); + } + } + $this->assertIdentical($expected_opt_groups, views_get_views_as_options(FALSE, 'all', NULL, TRUE), 'Expected option array for an option group returned.'); + } + + /** + * Helper to return an expected views option array. + * + * @param array $views + * An array of Drupal\views\ViewStorage objects to create an options array + * for. + * + * @return array + * A formatted options array that matches the expected output. + */ + protected function formatViewOptions(array $views = array()) { + $expected_options = array(); + foreach ($views as $id => $view) { + foreach ($view->display as $display_id => $display) { + $expected_options[$view->id() . ':' . $display['id']] = t('View: @view - Display: @display', + array('@view' => $view->name, '@display' => $display['id'])); + } + } + + return $expected_options; + } + + /** + * Ensure that a certain handler is a instance of a certain table/field. + */ + function assertInstanceHandler($handler, $table, $field, $id) { + $table_data = views_fetch_data($table); + $field_data = $table_data[$field][$id]; + + $this->assertEqual($field_data['id'], $handler->getPluginId()); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Node/FieldTypeTest.php b/core/modules/views/lib/Drupal/views/Tests/Node/FieldTypeTest.php new file mode 100644 index 0000000..301e4be --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Node/FieldTypeTest.php @@ -0,0 +1,47 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Node\FieldTypeTest. + */ + +namespace Drupal\views\Tests\Node; + +/** + * Tests the Views\node\Plugin\views\field\Type handler. + */ +class FieldTypeTest extends NodeTestBase { + + public static function getInfo() { + return array( + 'name' => 'Node: Node Type field', + 'description' => 'Tests the Views\node\Plugin\views\field\Type handler.', + 'group' => 'Views Modules', + ); + } + + public function testFieldType() { + $node = $this->drupalCreateNode(); + $expected_result[] = array( + 'nid' => $node->id(), + 'node_type' => $node->bundle(), + ); + $column_map = array( + 'nid' => 'nid', + 'node_type' => 'node_type', + ); + + $view = $this->getView(); + $view->preview(); + $this->executeView($view); + $this->assertIdenticalResultset($view, $expected_result, $column_map, 'The correct node type was displayed.'); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_field_type'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Node/FilterUidRevisionTest.php b/core/modules/views/lib/Drupal/views/Tests/Node/FilterUidRevisionTest.php new file mode 100644 index 0000000..2b27181 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Node/FilterUidRevisionTest.php @@ -0,0 +1,57 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Comment\FilterUidRevisionTest. + */ + +namespace Drupal\views\Tests\Node; + +/** + * Tests the node_uid_revision handler. + */ +class FilterUidRevisionTest extends NodeTestBase { + + public static function getInfo() { + return array( + 'name' => 'Node: User has revision Filter', + 'description' => 'Tests the node_uid_revision handler.', + 'group' => 'Views Modules', + ); + } + + + /** + * Tests the node_uid_revision filter. + */ + public function testFilter() { + $author = $this->drupalCreateUser(); + $no_author = $this->drupalCreateUser(); + + $expected_result = array(); + // Create one node, with the author as the node author. + $node = $this->drupalCreateNode(array('uid' => $author->id())); + $expected_result[] = array('nid' => $node->id()); + // Create one node of which an additional revision author will be the + // author. + $node = $this->drupalCreateNode(array('uid' => $no_author->id())); + $expected_result[] = array('nid' => $node->id()); + $revision = clone $node; + // Force to add a new revision. + $revision->set('vid', NULL); + $revision->set('revision_uid', $author->id()); + $revision->save(); + + // Create one node on which the author has neither authorship of revisions + // or the main node. + $node = $this->drupalCreateNode(array('uid' => $no_author->id())); + + $view = views_get_view('test_filter_node_uid_revision'); + $view->initHandlers(); + $view->filter['uid_revision']->value = array($author->uid); + + $this->executeView($view); + $this->assertIdenticalResultset($view, $expected_result, array('nid' => 'nid'), 'Make sure that the view only returns nodes which match either the node or the revision author.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Node/NodeTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Node/NodeTestBase.php new file mode 100644 index 0000000..17bbc79 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Node/NodeTestBase.php @@ -0,0 +1,16 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Node\NodeTestBase. + */ + +namespace Drupal\views\Tests\Node; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Base class for all node tests. + */ +abstract class NodeTestBase extends ViewTestBase { +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Node/RevisionRelationships.php b/core/modules/views/lib/Drupal/views/Tests/Node/RevisionRelationships.php new file mode 100644 index 0000000..ce06687 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Node/RevisionRelationships.php @@ -0,0 +1,69 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Node\RevisionRelationships. + */ +namespace Drupal\views\Tests\Node; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Tests basic node_revision table integration into views. + */ +class RevisionRelationships extends ViewTestBase { + + public static function getInfo() { + return array( + 'name' => 'Node: Revision integration', + 'description' => 'Tests the integration of node_revision table of node module', + 'group' => 'Views Modules', + ); + } + + /** + * Create a node with revision and rest result count for both views. + */ + public function testNodeRevisionRelationship() { + $node = $this->drupalCreateNode(); + // Create revision of the node. + $node_revision = clone $node; + $node_revision->revision = 1; + $node->save(); + $column_map = array( + 'vid' => 'vid', + 'node_revision_nid' => 'node_revision_nid', + 'node_node_revision_nid' => 'node_node_revision_nid', + ); + + // Here should be two rows. + $view_nid = $this->createViewFromConfig('test_node_revision_nid'); + $this->executeView($view_nid, array($node->nid)); + $resultset_nid = array( + array( + 'vid' => '1', + 'node_revision_nid' => '1', + 'node_node_revision_nid' => '1', + ), + array( + 'vid' => '2', + 'node_revision_nid' => '1', + 'node_node_revision_nid' => '1', + ), + ); + $this->assertIdenticalResultset($view_nid, $resultset_nid, $column_map); + + // There should be only one row with active revision 2. + $view_vid = $this->createViewFromConfig('test_node_revision_vid'); + $this->executeView($view_vid, array($node->nid)); + $resultset_vid = array( + array( + 'vid' => '2', + 'node_revision_nid' => '1', + 'node_node_revision_nid' => '1', + ), + ); + $this->assertIdenticalResultset($view_vid, $resultset_vid, $column_map); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/AccessTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/AccessTest.php new file mode 100644 index 0000000..1a2d034 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/AccessTest.php @@ -0,0 +1,150 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\AccessTest + */ + +namespace Drupal\views\Tests\Plugin; + +/** + * Basic test for pluggable access. + * + * @todo It probably make sense to split the test up by one for role/perm/none + * and the two generic ones. + */ +class AccessTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Access', + 'description' => 'Tests pluggable access for views.', + 'group' => 'Views Plugins' + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->admin_user = $this->drupalCreateUser(array('access all views')); + $this->web_user = $this->drupalCreateUser(); + $this->web_role = current($this->web_user->roles); + + $this->normal_role = $this->drupalCreateRole(array()); + $this->normal_user = $this->drupalCreateUser(array('views_test_data test permission')); + $this->normal_user->roles[$this->normal_role] = $this->normal_role; + // @todo when all the plugin information is cached make a reset function and + // call it here. + } + + /** + * Tests none access plugin. + */ + function testAccessNone() { + $view = $this->createViewFromConfig('test_access_none'); + + $this->assertTrue($view->display_handler->access($this->admin_user), t('Admin-Account should be able to access the view everytime')); + $this->assertTrue($view->display_handler->access($this->web_user)); + $this->assertTrue($view->display_handler->access($this->normal_user)); + } + + /** + * Tests perm access plugin. + */ + function testAccessPerm() { + $view = $this->createViewFromConfig('test_access_perm'); + + $access_plugin = $view->display_handler->getPlugin('access'); + + $this->assertTrue($view->display_handler->access($this->admin_user), t('Admin-Account should be able to access the view everytime')); + $this->assertFalse($view->display_handler->access($this->web_user)); + $this->assertTrue($view->display_handler->access($this->normal_user)); + } + + /** + * Tests role access plugin. + */ + function testAccessRole() { + $view = $this->createViewFromConfig('test_access_role'); + + $view->displayHandlers['default']->options['access']['options']['role'] = array( + $this->normal_role => $this->normal_role, + ); + + $access_plugin = $view->display_handler->getPlugin('access'); + + $this->assertTrue($view->display_handler->access($this->admin_user), t('Admin-Account should be able to access the view everytime')); + $this->assertFalse($view->display_handler->access($this->web_user)); + $this->assertTrue($view->display_handler->access($this->normal_user)); + } + + /** + * @todo Test abstract access plugin. + */ + + /** + * Tests static access check. + * + * @see Drupal\views_test\Plugin\views\access\StaticTest + */ + function testStaticAccessPlugin() { + $view = $this->createViewFromConfig('test_access_static'); + + $access_plugin = $view->display_handler->getPlugin('access'); + + $this->assertFalse($access_plugin->access($this->normal_user)); + + $access_plugin->options['access'] = TRUE; + $this->assertTrue($access_plugin->access($this->normal_user)); + + // FALSE comes from hook_menu caching. + $expected_hook_menu = array( + 'views_test_data_test_static_access_callback', array(FALSE) + ); + $hook_menu = $view->executeHookMenu('page_1'); + $this->assertEqual($expected_hook_menu, $hook_menu['test_access_static']['access arguments'][0]); + + $expected_hook_menu = array( + 'views_test_data_test_static_access_callback', array(TRUE) + ); + $this->assertTrue(views_access($expected_hook_menu)); + } + + /** + * Tests dynamic access plugin. + * + * @see Drupal\views_test\Plugin\views\access\DyamicTest + */ + function testDynamicAccessPlugin() { + $view = $this->createViewFromConfig('test_access_dynamic'); + $argument1 = $this->randomName(); + $argument2 = $this->randomName(); + variable_set('test_dynamic_access_argument1', $argument1); + variable_set('test_dynamic_access_argument2', $argument2); + + $access_plugin = $view->display_handler->getPlugin('access'); + + $this->assertFalse($access_plugin->access($this->normal_user)); + + $access_plugin->options['access'] = TRUE; + $this->assertFalse($access_plugin->access($this->normal_user)); + + $view->setArguments(array($argument1, $argument2)); + $this->assertTrue($access_plugin->access($this->normal_user)); + + // FALSE comes from hook_menu caching. + $expected_hook_menu = array( + 'views_test_data_test_dynamic_access_callback', array(FALSE, 1, 2) + ); + $hook_menu = $view->executeHookMenu('page_1'); + $this->assertEqual($expected_hook_menu, $hook_menu['test_access_dynamic']['access arguments'][0]); + + $expected_hook_menu = array( + 'views_test_data_test_dynamic_access_callback', array(TRUE, 1, 2) + ); + $this->assertTrue(views_access($expected_hook_menu, $argument1, $argument2)); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/ArgumentDefaultTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/ArgumentDefaultTest.php new file mode 100644 index 0000000..6c9e120 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/ArgumentDefaultTest.php @@ -0,0 +1,149 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\ArgumentDefaultTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views_test_data\Plugin\views\argument_default\ArgumentDefaultTest as ArgumentDefaultTestPlugin; + + +/** + * Basic test for pluggable argument default. + */ +class ArgumentDefaultTest extends PluginTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + /** + * A random string used in the default views. + * + * @var string + */ + protected $random; + + public static function getInfo() { + return array( + 'name' => 'Argument default', + 'description' => 'Tests pluggable argument_default for views.', + 'group' => 'Views Plugins' + ); + } + + protected function setUp() { + $this->random = $this->randomString(); + + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Tests the argument default test plugin. + * + * @see Drupal\views_test_data\Plugin\views\argument_default\ArgumentDefaultTest + */ + public function testArgumentDefaultPlugin() { + $view = views_get_view('test_view'); + + // Add a new argument and set the test plugin for the argument_default. + $options = array( + 'default_argument_type' => 'argument_default_test', + 'default_argument_options' => array( + 'value' => 'John' + ), + 'default_action' => 'default' + ); + $id = $view->addItem('default', 'argument', 'views_test_data', 'name', $options); + $view->initHandlers(); + $plugin = $view->argument[$id]->get_plugin('argument_default'); + $this->assertTrue($plugin instanceof ArgumentDefaultTestPlugin, 'The correct argument default plugin is used.'); + + // Check that the value of the default argument is as expected. + $this->assertEqual($view->argument[$id]->get_default_argument(), 'John', 'The correct argument default value is returned.'); + // Don't pass in a value for the default argument and make sure the query + // just returns John. + $this->executeView($view); + $this->assertEqual($view->argument[$id]->get_value(), 'John', 'The correct argument value is used.'); + $expected_result = array(array('name' => 'John')); + $this->assertIdenticalResultset($view, $expected_result, array('views_test_data_name' => 'name')); + + // Pass in value as argument to be sure that not the default value is used. + $view->destroy(); + $this->executeView($view, array('George')); + $this->assertEqual($view->argument[$id]->get_value(), 'George', 'The correct argument value is used.'); + $expected_result = array(array('name' => 'George')); + $this->assertIdenticalResultset($view, $expected_result, array('views_test_data_name' => 'name')); + } + + + /** + * Tests the use of a default argument plugin that provides no options. + */ + function testArgumentDefaultNoOptions() { + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + + // The current_user plugin has no options form, and should pass validation. + $argument_type = 'current_user'; + $edit = array( + 'options[default_argument_type]' => $argument_type, + ); + $this->drupalPost('admin/structure/views/nojs/config-item/test_argument_default_current_user/default/argument/uid', $edit, t('Apply')); + + // Note, the undefined index error has two spaces after it. + $error = array( + '%type' => 'Notice', + '!message' => 'Undefined index: ' . $argument_type, + '%function' => 'views_handler_argument->validateOptionsForm()', + ); + $message = t('%type: !message in %function', $error); + $this->assertNoRaw($message, t('Did not find error message: !message.', array('!message' => $message))); + } + + /** + * Tests fixed default argument. + */ + function testArgumentDefaultFixed() { + $view = $this->getView(); + $view->preExecute(); + $view->initHandlers(); + + $this->assertEqual($view->argument['null']->get_default_argument(), $this->random, 'Fixed argument should be used by default.'); + + // Make sure that a normal argument provided is used + $view = $this->getView(); + + $random_string = $this->randomString(); + $view->executeDisplay('default', array($random_string)); + + $this->assertEqual($view->args[0], $random_string, 'Provided argument should be used.'); + } + + /** + * @todo Test php default argument. + */ + //function testArgumentDefaultPhp() {} + + /** + * @todo Test node default argument. + */ + //function testArgumentDefaultNode() {} + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + $view = $this->createViewFromConfig('test_argument_default_fixed'); + $view->displayHandlers['default']->display['display_options']['arguments']['null']['default_argument_options']['argument'] = $this->random; + return $view; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/ArgumentValidatorTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/ArgumentValidatorTest.php new file mode 100644 index 0000000..fa9541f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/ArgumentValidatorTest.php @@ -0,0 +1,46 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\ArgumentValidatorTest. + */ + +namespace Drupal\views\Tests\Plugin; + +/** + * Tests Views argument validators. + */ +class ArgumentValidatorTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Argument validator', + 'group' => 'Views Plugins', + 'description' => 'Test argument validator tests.', + ); + } + + function testArgumentValidatePhp() { + $string = $this->randomName(); + $view = $this->createViewFromConfig('test_view_argument_validate_php'); + $view->displayHandlers['default']->options['arguments']['null']['validate_options']['code'] = 'return $argument == \''. $string .'\';'; + + $view->preExecute(); + $view->initHandlers(); + $this->assertTrue($view->argument['null']->validateArgument($string)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + $this->assertFalse($view->argument['null']->validateArgument($this->randomName())); + } + + function testArgumentValidateNumeric() { + $view = $this->createViewFromConfig('test_view_argument_validate_numeric'); + $view->preExecute(); + $view->initHandlers(); + $this->assertFalse($view->argument['null']->validateArgument($this->randomString())); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + $this->assertTrue($view->argument['null']->validateArgument(12)); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/CacheTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/CacheTest.php new file mode 100644 index 0000000..683f630 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/CacheTest.php @@ -0,0 +1,218 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\CacheTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views\ViewExecutable; + +/** + * Basic test for pluggable caching. + * + * @see views_plugin_cache + */ +class CacheTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Cache', + 'description' => 'Tests pluggable caching for views.', + 'group' => 'Views Plugins' + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Build and return a basic view of the views_test_data table. + * + * @return Drupal\views\ViewExecutable + */ + protected function getBasicView() { + // Create the basic view. + $view = $this->createViewFromConfig('test_view'); + $view->storage->addDisplay('default'); + $view->storage->base_table = 'views_test_data'; + + // Set up the fields we need. + $display = $view->storage->newDisplay('default', 'Master', 'default'); + $display->overrideOption('fields', array( + 'id' => array( + 'id' => 'id', + 'table' => 'views_test_data', + 'field' => 'id', + 'relationship' => 'none', + ), + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Set up the sort order. + $display->overrideOption('sorts', array( + 'id' => array( + 'order' => 'ASC', + 'id' => 'id', + 'table' => 'views_test_data', + 'field' => 'id', + 'relationship' => 'none', + ), + )); + + return $view; + } + + /** + * Tests time based caching. + * + * @see views_plugin_cache_time + */ + function testTimeCaching() { + // Create a basic result which just 2 results. + $view = $this->getView(); + $view->display_handler->overrideOption('cache', array( + 'type' => 'time', + 'options' => array( + 'results_lifespan' => '3600', + 'output_lifespan' => '3600' + ) + )); + + $this->executeView($view); + // Verify the result. + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + + // Add another man to the beatles. + $record = array( + 'name' => 'Rod Davis', + 'age' => 29, + 'job' => 'Banjo', + ); + drupal_write_record('views_test_data', $record); + + // The Result should be the same as before, because of the caching. + $view = $this->getView(); + $view->display_handler->overrideOption('cache', array( + 'type' => 'time', + 'options' => array( + 'results_lifespan' => '3600', + 'output_lifespan' => '3600' + ) + )); + + $this->executeView($view); + // Verify the result. + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + } + + /** + * Tests no caching. + * + * @see views_plugin_cache_time + */ + function testNoneCaching() { + // Create a basic result which just 2 results. + $view = $this->getView(); + $view->display_handler->overrideOption('cache', array( + 'type' => 'none', + 'options' => array() + )); + + $this->executeView($view); + // Verify the result. + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + + // Add another man to the beatles. + $record = array( + 'name' => 'Rod Davis', + 'age' => 29, + 'job' => 'Banjo', + ); + + drupal_write_record('views_test_data', $record); + + // The Result changes, because the view is not cached. + $view = $this->getView(); + $view->display_handler->overrideOption('cache', array( + 'type' => 'none', + 'options' => array() + )); + + $this->executeView($view); + // Verify the result. + $this->assertEqual(6, count($view->result), t('The number of returned rows match.')); + } + + /** + * Tests css/js storage and restoring mechanism. + */ + function testHeaderStorage() { + // Create a view with output caching enabled. + // Some hook_views_pre_render in views_test_data.module adds the test css/js file. + // so they should be added to the css/js storage. + $view = $this->getView(); + $view->storage->name = 'test_cache_header_storage'; + $view->display_handler->overrideOption('cache', array( + 'type' => 'time', + 'options' => array( + 'output_lifespan' => '3600' + ) + )); + + $view->preview(); + unset($view->pre_render_called); + drupal_static_reset('drupal_add_css'); + drupal_static_reset('drupal_add_js'); + + $view = $this->getView($view); + $view->preview(); + $css = drupal_add_css(); + $css_path = drupal_get_path('module', 'views_test_data') . '/views_cache.test.css'; + $js_path = drupal_get_path('module', 'views_test_data') . '/views_cache.test.js'; + $js = drupal_add_js(); + + $this->assertTrue(isset($css[$css_path]), 'Make sure the css is added for cached views.'); + $this->assertTrue(isset($js[$js_path]), 'Make sure the js is added for cached views.'); + $this->assertFalse(!empty($view->build_info['pre_render_called']), 'Make sure hook_views_pre_render is not called for the cached view.'); + + // Now add some css/jss before running the view. + // Make sure that this css is not added when running the cached view. + $view->storage->name = 'test_cache_header_storage_2'; + + $system_css_path = drupal_get_path('module', 'system') . '/system.maintenance.css'; + drupal_add_css($system_css_path); + $system_js_path = drupal_get_path('module', 'system') . '/system.cron.js'; + drupal_add_js($system_js_path); + + $view = $this->getView($view); + $view->preview(); + drupal_static_reset('drupal_add_css'); + drupal_static_reset('drupal_add_js'); + + $view = $this->getView($view); + $view->preview(); + + $css = drupal_add_css(); + $js = drupal_add_js(); + + $this->assertFalse(isset($css[$system_css_path]), 'Make sure that unrelated css is not added.'); + $this->assertFalse(isset($js[$system_js_path]), 'Make sure that unrelated js is not added.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayExtenderTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayExtenderTest.php new file mode 100644 index 0000000..90eee06 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayExtenderTest.php @@ -0,0 +1,54 @@ +<?php + + /** + * @file + * Definition of Drupal\views\Tests\Plugin\DisplayExtenderTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views\Tests\Plugin\PluginTestBase; + +/** + * Tests the display extender plugins. + * + * @see Drupal\views_test_data\Plugin\views\display_extender\DisplayExtenderTest + */ +class DisplayExtenderTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Display extender', + 'description' => 'Tests the display extender plugins.', + 'group' => 'Views Plugins', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + + /** + * Test display extenders. + */ + public function testDisplayExtenders() { + config('views.settings')->set('display_extenders', array('display_extender_test'))->save(); + $this->assertEqual(count(views_get_enabled_display_extenders()), 1, 'Make sure that there is only one enabled display extender.'); + + $view = $this->getBasicView(); + $view->initDisplay(); + + $this->assertEqual(count($view->display_handler->extender), 1, 'Make sure that only one extender is initialized.'); + + $display_extender = $view->display_handler->extender['display_extender_test']; + $this->assertTrue($display_extender instanceof \Drupal\views_test_data\Plugin\views\display_extender\DisplayExtenderTest, 'Make sure the right class got initialized.'); + + $view->preExecute(); + $this->assertTrue($display_extender->testState['pre_execute'], 'Make sure the display extender was able to react on preExecute.'); + $view->execute(); + $this->assertTrue($display_extender->testState['query'], 'Make sure the display extender was able to react on query.'); + } +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayFeedTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayFeedTest.php new file mode 100644 index 0000000..6bc2297 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayFeedTest.php @@ -0,0 +1,64 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\DisplayFeedTest. + */ + +namespace Drupal\views\Tests\Plugin; + +/** + * Tests the feed display plugin. + * + * @see Drupal\views\Plugin\views\display\Feed + */ +class DisplayFeedTest extends PluginTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + public static function getInfo() { + return array( + 'name' => 'Display: Feed plugin', + 'description' => 'Tests the feed display plugin.', + 'group' => 'Views Plugins', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + } + + /** + * Tests feed display admin ui. + */ + public function testFeedUI() { + $this->drupalGet('admin/structure/views'); + + // Check the attach TO interface. + $this->drupalGet('admin/structure/views/nojs/display/test_feed_display/feed/displays'); + + // Load all the options of the checkbox. + $result = $this->xpath('//div[@id="edit-displays"]/div'); + $options = array(); + foreach ($result as $value) { + foreach ($value->input->attributes() as $attribute => $value) { + if ($attribute == 'value') { + $options[] = (string) $value; + } + } + } + + $this->assertEqual($options, array('default', 'feed', 'page'), 'Make sure all displays appears as expected.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php new file mode 100644 index 0000000..417126c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php @@ -0,0 +1,44 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\DisplayPageTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views\Tests\Plugin\PluginTestBase; + +/** + * Tests the page display plugin. + * + * @see Drupal\views\Plugin\display\Page + */ +class DisplayPageTest extends PluginTestBase { + + public static function getInfo() { + return array( + 'name' => 'Display: Page plugin', + 'description' => 'Tests the page display plugin.', + 'group' => 'Views Plugins', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Checks the behavior of the page for access denied/not found behaviours. + */ + public function testPageResponses() { + $view = views_get_view('test_page_display'); + $this->drupalGet('test_page_display_403'); + $this->assertResponse(403); + $this->drupalGet('test_page_display_404'); + $this->assertResponse(404); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayTest.php new file mode 100644 index 0000000..9b369b3 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayTest.php @@ -0,0 +1,122 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\DisplayTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views_test_data\Plugin\views\display\DisplayTest as DisplayTestPlugin; + +/** + * Tests the basic display plugin. + */ +class DisplayTest extends PluginTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + public static function getInfo() { + return array( + 'name' => 'Display', + 'description' => 'Tests the basic display plugin.', + 'group' => 'Views Plugins', + ); + } + + public function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->adminUser = $this->drupalCreateUser(array('administer views')); + $this->drupalLogin($this->adminUser); + + // Create 10 nodes. + for ($i = 0; $i <= 10; $i++) { + $this->drupalCreateNode(array('promote' => TRUE)); + } + } + + /** + * Tests the display test plugin. + * + * @see Drupal\views_test_data\Plugin\views\display\DisplayTest + */ + function testDisplayPlugin() { + $view = views_get_view('frontpage'); + + // Add a new 'display_test' display and test it's there. + $view->storage->addDisplay('display_test'); + + $this->assertTrue(isset($view->storage->display['display_test_1']), 'Added display has been assigned to "display_test_1"'); + + // Check the the display options are like expected. + $options = array( + 'display_options' => array(), + 'display_plugin' => 'display_test', + 'id' => 'display_test_1', + 'display_title' => 'Display test', + 'position' => NULL, + ); + $this->assertEqual($view->storage->display['display_test_1'], $options); + + $view->setDisplay('display_test_1'); + + $this->assertTrue($view->display_handler instanceof DisplayTestPlugin, 'The correct display handler instance is on the view object.'); + + // Check the test option. + $this->assertIdentical($view->display_handler->getOption('test_option'), ''); + + $output = $view->preview(); + + $this->assertTrue(strpos($output, '<h1></h1>') !== FALSE, 'An empty value for test_option found in output.'); + + // Change this option and check the title of out output. + $view->display_handler->overrideOption('test_option', 'Test option title'); + + $view->save(); + $output = $view->preview(); + + // Test we have our custom <h1> tag in the output of the view. + $this->assertTrue(strpos($output, '<h1>Test option title</h1>') !== FALSE, 'The test_option value found in display output title.'); + + // Test that the display category/summary is in the UI. + $this->drupalGet('admin/structure/views/view/frontpage/edit/display_test_1'); + $this->assertText('Display test settings'); + + $this->clickLink('Test option title'); + + $this->randomString = $this->randomString(); + $this->drupalPost(NULL, array('test_option' => $this->randomString), t('Apply')); + + // Check the new value has been saved by checking the UI summary text. + $this->drupalGet('admin/structure/views/view/frontpage/edit/display_test_1'); + $this->assertRaw($this->randomString); + + // Test the enable/disable status of a display. + $view->display_handler->setOption('enabled', FALSE); + $this->assertFalse($view->display_handler->isEnabled(), 'Make sure that isEnabled returns FALSE on a disabled display.'); + $view->display_handler->setOption('enabled', TRUE); + $this->assertTrue($view->display_handler->isEnabled(), 'Make sure that isEnabled returns TRUE on a disabled display.'); + } + + /** + * Tests the overriding of filter_groups. + */ + public function testFilterGroupsOverriding() { + $view = $this->createViewFromConfig('test_filter_groups'); + $view->initDisplay(); + + // mark is as overridden, yes FALSE, means overridden. + $view->displayHandlers['page']->setOverride('filter_groups', FALSE); + $this->assertFalse($view->displayHandlers['page']->isDefaulted('filter_groups'), "Take sure that 'filter_groups' is marked as overridden."); + $this->assertFalse($view->displayHandlers['page']->isDefaulted('filters'), "Take sure that 'filters'' is marked as overridden."); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/ExposedFormTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/ExposedFormTest.php new file mode 100644 index 0000000..f415a25 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/ExposedFormTest.php @@ -0,0 +1,182 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\ExposedFormTest. + */ + +namespace Drupal\views\Tests\Plugin; + +/** + * Tests exposed forms. + */ +class ExposedFormTest extends PluginTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + public static function getInfo() { + return array( + 'name' => 'Exposed forms', + 'description' => 'Test exposed forms functionality.', + 'group' => 'Views Plugins', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->drupalCreateContentType(array('type' => 'article')); + $this->drupalCreateContentType(array('type' => 'page')); + } + + /** + * Tests, whether and how the reset button can be renamed. + */ + public function testRenameResetButton() { + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + // Create some random nodes. + for ($i = 0; $i < 5; $i++) { + $this->drupalCreateNode(); + } + // Look at the page and check the label "reset". + $this->drupalGet('test_rename_reset_button'); + // Rename the label of the reset button. + $view = views_get_view('test_rename_reset_button'); + $view->setDisplay(); + + $exposed_form = $view->display_handler->getOption('exposed_form'); + $exposed_form['options']['reset_button_label'] = $expected_label = $this->randomName(); + $exposed_form['options']['reset_button'] = TRUE; + $view->display_handler->setOption('exposed_form', $exposed_form); + $view->save(); + + views_invalidate_cache(); + + // Look whether ther reset button label changed. + $this->drupalGet('test_rename_reset_button'); + $this->assertResponse(200); + + $this->helperButtonHasLabel('edit-reset', $expected_label); + } + + /** + * Tests the admin interface of exposed filter and sort items. + */ + function testExposedAdminUi() { + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + menu_router_rebuild(); + $edit = array(); + + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + // Be sure that the button is called exposed. + $this->helperButtonHasLabel('edit-options-expose-button-button', t('Expose filter')); + + // The first time the filter UI is displayed, the operator and the + // value forms should be shown. + $this->assertFieldById('edit-options-operator-in', '', 'Operator In exists'); + $this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists'); + $this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists'); + $this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists'); + + // Click the Expose filter button. + $this->drupalPost('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type', $edit, t('Expose filter')); + // Check the label of the expose button. + $this->helperButtonHasLabel('edit-options-expose-button-button', t('Hide filter')); + // Check the label of the grouped exposed button + $this->helperButtonHasLabel('edit-options-group-button-button', t('Grouped filters')); + + // After exposing the filter, Operator and Value should be still here. + $this->assertFieldById('edit-options-operator-in', '', 'Operator In exists'); + $this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists'); + $this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists'); + $this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists'); + + // Check the validations of the filter handler. + $edit = array(); + $edit['options[expose][identifier]'] = ''; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertText(t('The identifier is required if the filter is exposed.')); + + $edit = array(); + $edit['options[expose][identifier]'] = 'value'; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertText(t('This identifier is not allowed.')); + + // Now check the sort criteria. + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/sort/created'); + $this->helperButtonHasLabel('edit-options-expose-button-button', t('Expose sort')); + $this->assertNoFieldById('edit-options-expose-label', '', t('Make sure no label field is shown')); + + // Click the Grouped Filters button. + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $this->drupalPost(NULL, array(), t('Grouped filters')); + + // After click on 'Grouped Filters', the standard operator and value should + // not be displayed. + $this->assertNoFieldById('edit-options-operator-in', '', 'Operator In not exists'); + $this->assertNoFieldById('edit-options-operator-not-in', '', 'Operator Not In not exists'); + $this->assertNoFieldById('edit-options-value-page', '', 'Checkbox for Page not exists'); + $this->assertNoFieldById('edit-options-value-article', '', 'Checkbox for Article not exists'); + + // Check that after click on 'Grouped Filters', a new button is shown to + // add more items to the list. + $this->helperButtonHasLabel('edit-options-group-info-add-group', t('Add another item')); + + // Create a grouped filter + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $edit = array(); + $edit["options[group_info][group_items][1][title]"] = 'Is Article'; + $edit["options[group_info][group_items][1][value][article]"] = 'article'; + + $edit["options[group_info][group_items][2][title]"] = 'Is Page'; + $edit["options[group_info][group_items][2][value][page]"] = TRUE; + + $edit["options[group_info][group_items][3][title]"] = 'Is Page and Article'; + $edit["options[group_info][group_items][3][value][article]"] = TRUE; + $edit["options[group_info][group_items][3][value][page]"] = TRUE; + $this->drupalPost(NULL, $edit, t('Apply')); + + // Validate that all the titles are defined for each group + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $edit = array(); + $edit["options[group_info][group_items][1][title]"] = 'Is Article'; + $edit["options[group_info][group_items][1][value][article]"] = TRUE; + + // This should trigger an error + $edit["options[group_info][group_items][2][title]"] = ''; + $edit["options[group_info][group_items][2][value][page]"] = TRUE; + + $edit["options[group_info][group_items][3][title]"] = 'Is Page and Article'; + $edit["options[group_info][group_items][3][value][article]"] = TRUE; + $edit["options[group_info][group_items][3][value][page]"] = TRUE; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertRaw(t('The title is required if value for this item is defined.'), t('Group items should have a title')); + + // Un-expose the filter. + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $this->drupalPost(NULL, array(), t('Hide filter')); + + // After Un-exposing the filter, Operator and Value should be shown again. + $this->assertFieldById('edit-options-operator-in', '', 'Operator In exists after hide filter'); + $this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists after hide filter'); + $this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists after hide filter'); + $this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists after hide filter'); + + // Click the Expose sort button. + $edit = array(); + $this->drupalPost('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/sort/created', $edit, t('Expose sort')); + // Check the label of the expose button. + $this->helperButtonHasLabel('edit-options-expose-button-button', t('Hide sort')); + $this->assertFieldById('edit-options-expose-label', '', t('Make sure a label field is shown')); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/FilterTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/FilterTest.php new file mode 100644 index 0000000..ad6ee1d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/FilterTest.php @@ -0,0 +1,144 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\FilterTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views_test_data\Plugin\views\filter\FilterTest as FilterPlugin; + +/** + * Tests general filter plugin functionality. + * + * @see Drupal\views\Plugin\views\filter\FilterPluginBase + */ +class FilterTest extends PluginTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + public static function getInfo() { + return array( + 'name' => 'Filter: General', + 'description' => 'Tests general filter plugin functionality.', + 'group' => 'Views Plugins', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + */ + protected function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['name']['filter']['id'] = 'test_filter'; + + return $data; + } + + /** + * Test query of the row plugin. + */ + public function testFilterQuery() { + // Check that we can find the test filter plugin. + $plugin = views_get_plugin('filter', 'test_filter'); + $this->assertTrue($plugin instanceof FilterPlugin, 'Test filter plugin found.'); + + $view = views_get_view('test_filter'); + $view->initDisplay(); + + // Change the filtering. + $view->displayHandlers['default']->overrideOption('filters', array( + 'test_filter' => array( + 'id' => 'test_filter', + 'table' => 'views_test_data', + 'field' => 'name', + 'operator' => '=', + 'value' => 'John', + 'group' => 0, + ), + )); + + $this->executeView($view); + + // Make sure the query have where data. + $this->assertTrue(!empty($view->query->where)); + + // Check the data added. + $where = $view->query->where; + $this->assertIdentical($where[0]['conditions'][0]['field'], 'views_test_data.name', 'Where condition field matches'); + $this->assertIdentical($where[0]['conditions'][0]['value'], 'John', 'Where condition value matches'); + $this->assertIdentical($where[0]['conditions'][0]['operator'], '=', 'Where condition operator matches'); + + $this->executeView($view); + + // Check that our operator and value match on the filter. + $this->assertIdentical($view->filter['test_filter']->operator, '='); + $this->assertIdentical($view->filter['test_filter']->value, 'John'); + + // Check that we have some results. + $this->assertEqual(count($view->result), 1, format_string('Results were returned. @count results.', array('@count' => count($view->result)))); + + $view->destroy(); + + $view->initDisplay(); + + // Change the filtering. + $view->displayHandlers['default']->overrideOption('filters', array( + 'test_filter' => array( + 'id' => 'test_filter', + 'table' => 'views_test_data', + 'field' => 'name', + 'operator' => '<>', + 'value' => 'John', + 'group' => 0, + ), + )); + + $this->executeView($view); + + // Check that our operator and value match on the filter. + $this->assertIdentical($view->filter['test_filter']->operator, '<>'); + $this->assertIdentical($view->filter['test_filter']->value, 'John'); + + // Test that no nodes have been returned (Only 'page' type nodes should + // exist). + $this->assertEqual(count($view->result), 4, format_string('No results were returned. @count results.', array('@count' => count($view->result)))); + + $view->destroy(); + $view->initDisplay(); + + // Set the test_enable option to FALSE. The 'where' clause should not be + // added to the query. + $view->displayHandlers['default']->overrideOption('filters', array( + 'test_filter' => array( + 'id' => 'test_filter', + 'table' => 'views_test_data', + 'field' => 'name', + 'operator' => '<>', + 'value' => 'John', + 'group' => 0, + // Disable this option, so nothing should be added to the query. + 'test_enable' => FALSE, + ), + )); + + // Execute the view again. + $this->executeView($view); + + // Check if we have all 5 results. + $this->assertEqual(count($view->result), 5, format_string('All @count results returned', array('@count' => count($view->displayHandlers)))); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/JoinTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/JoinTest.php new file mode 100644 index 0000000..3a3c89b --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/JoinTest.php @@ -0,0 +1,152 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\JoinTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views_test_data\Plugin\views\join\JoinTest as JoinTestPlugin; +use Drupal\views\Plugin\views\join\JoinPluginBase; + + +/** + * Tests a generic join plugin and the join plugin base. + */ +class JoinTest extends PluginTestBase { + + /** + * A plugin manager which handlers the instances of joins. + * + * @var Drupal\views\Plugin\Type\ViewsPluginManager + */ + protected $manager; + + public static function getInfo() { + return array( + 'name' => 'Join', + 'description' => 'Tests the join plugin.', + 'group' => 'Views Plugins', + ); + } + + protected function setUp() { + parent::setUp(); + + // Add a join plugin manager which can be used in all of the tests. + $this->manager = drupal_container()->get('plugin.manager.views.join'); + } + + + /** + * Tests an example join plugin. + */ + public function testExamplePlugin() { + + // Setup a simple join and test the result sql. + $view = views_get_view('frontpage'); + $view->initDisplay(); + $view->initQuery(); + + $configuration = array( + 'left_table' => 'node', + 'left_field' => 'uid', + 'table' => 'users', + 'field' => 'uid', + ); + $join = $this->manager->createInstance('join_test', $configuration); + $this->assertTrue($join instanceof JoinTestPlugin, 'The correct join class got loaded.'); + + $rand_int = rand(0, 1000); + $join->setJoinValue($rand_int); + + $query = db_select('node'); + $table = array('alias' => 'users'); + $join->buildJoin($query, $table, $view->query); + + $tables = $query->getTables(); + $join_info = $tables['users']; + debug($join_info); + $this->assertTrue(strpos($join_info['condition'], "node.uid = $rand_int") !== FALSE, 'Make sure that the custom join plugin can extend the join base and alter the result.'); + } + + /** + * Tests the join plugin base. + */ + public function testBasePlugin() { + + // Setup a simple join and test the result sql. + $view = views_get_view('frontpage'); + $view->initDisplay(); + $view->initQuery(); + + // First define a simple join without an extra condition. + // Set the various options on the join object. + $configuration = array( + 'left_table' => 'node', + 'left_field' => 'uid', + 'table' => 'users', + 'field' => 'uid', + ); + $join = $this->manager->createInstance('standard', $configuration); + $this->assertTrue($join instanceof JoinPluginBase, 'The correct join class got loaded.'); + + // Build the actual join values and read them back from the dbtng query + // object. + $query = db_select('node'); + $table = array('alias' => 'users'); + $join->buildJoin($query, $table, $view->query); + + $tables = $query->getTables(); + $join_info = $tables['users']; + $this->assertEqual($join_info['join type'], 'LEFT', 'Make sure the default join type is LEFT'); + $this->assertEqual($join_info['table'], $configuration['table']); + $this->assertEqual($join_info['alias'], 'users'); + $this->assertEqual($join_info['condition'], 'node.uid = users.uid'); + + // Set a different alias and make sure table info is as expected. + $join = $this->manager->createInstance('standard', $configuration); + $table = array('alias' => 'users1'); + $join->buildJoin($query, $table, $view->query); + + $tables = $query->getTables(); + $join_info = $tables['users1']; + $this->assertEqual($join_info['alias'], 'users1'); + + // Set a different join type (INNER) and make sure it is used. + $configuration['type'] = 'INNER'; + $join = $this->manager->createInstance('standard', $configuration); + $table = array('alias' => 'users2'); + $join->buildJoin($query, $table, $view->query); + + $tables = $query->getTables(); + $join_info = $tables['users2']; + $this->assertEqual($join_info['join type'], 'INNER'); + + // Setup addition conditions and make sure it is used. + $random_name_1 = $this->randomName(); + $random_name_2 = $this->randomName(); + $configuration['extra'] = array( + array( + 'field' => 'name', + 'value' => $random_name_1 + ), + array( + 'field' => 'name', + 'value' => $random_name_2, + 'operator' => '<>' + ) + ); + $join = $this->manager->createInstance('standard', $configuration); + $table = array('alias' => 'users3'); + $join->buildJoin($query, $table, $view->query); + + $tables = $query->getTables(); + $join_info = $tables['users3']; + $this->assertTrue(strpos($join_info['condition'], "users3.name = :views_join_condition_0") !== FALSE, 'Make sure the first extra join condition appears in the query and uses the first placeholder.'); + $this->assertTrue(strpos($join_info['condition'], "users3.name <> :views_join_condition_1") !== FALSE, 'Make sure the second extra join condition appears in the query and uses the second placeholder.'); + $this->assertEqual(array_values($join_info['arguments']), array($random_name_1, $random_name_2), 'Make sure the arguments are in the right order'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/PagerTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/PagerTest.php new file mode 100644 index 0000000..6aa64c5 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/PagerTest.php @@ -0,0 +1,321 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\PagerTest. + */ + +namespace Drupal\views\Tests\Plugin; + +/** + * Tests the pluggable pager system. + */ +class PagerTest extends PluginTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui'); + + public static function getInfo() { + return array( + 'name' => 'Pager', + 'description' => 'Test the pluggable pager system.', + 'group' => 'Views Plugins', + ); + } + + /** + * Pagers was sometimes not stored. + * + * @see http://drupal.org/node/652712 + */ + public function testStorePagerSettings() { + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + // Test behaviour described in http://drupal.org/node/652712#comment-2354918. + + $this->drupalGet('admin/structure/views/view/frontpage/edit'); + + + $edit = array( + 'pager_options[items_per_page]' => 20, + ); + $this->drupalPost('admin/structure/views/nojs/display/frontpage/default/pager_options', $edit, t('Apply')); + $this->assertText('20 items'); + + // Change type and check whether the type is new type is stored. + $edit = array(); + $edit = array( + 'pager[type]' => 'mini', + ); + $this->drupalPost('admin/structure/views/nojs/display/frontpage/default/pager', $edit, t('Apply')); + $this->drupalGet('admin/structure/views/view/frontpage/edit'); + $this->assertText('Mini', 'Changed pager plugin, should change some text'); + + // Test behaviour described in http://drupal.org/node/652712#comment-2354400 + $view = $this->createViewFromConfig('test_store_pager_settings'); + // Make it editable in the admin interface. + $view->save(); + + $this->drupalGet('admin/structure/views/view/test_store_pager_settings/edit'); + + $edit = array(); + $edit = array( + 'pager[type]' => 'full', + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager', $edit, t('Apply')); + $this->drupalGet('admin/structure/views/view/test_store_pager_settings/edit'); + $this->assertText('Full'); + + $edit = array( + 'pager_options[items_per_page]' => 20, + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager_options', $edit, t('Apply')); + $this->assertText('20 items'); + + // add new display and test the settings again, by override it. + $edit = array( ); + // Add a display and override the pager settings. + $this->drupalPost('admin/structure/views/view/test_store_pager_settings/edit', $edit, t('Add Page')); + $edit = array( + 'override[dropdown]' => 'page_1', + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/page_1/pager', $edit, t('Apply')); + + $edit = array( + 'pager[type]' => 'mini', + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/page_1/pager', $edit, t('Apply')); + $this->drupalGet('admin/structure/views/view/test_store_pager_settings/edit'); + $this->assertText('Mini', 'Changed pager plugin, should change some text'); + + $edit = array( + 'pager_options[items_per_page]' => 10, + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager_options', $edit, t('Apply')); + $this->assertText('20 items'); + + } + + /** + * Tests the none-pager-query. + */ + public function testNoLimit() { + // Create 11 nodes and make sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->getView(); + $this->executeView($view); + $this->assertEqual(count($view->result), 11, 'Make sure that every item is returned in the result'); + + // Setup and test a offset. + $view = $this->getView(); + + $pager = array( + 'type' => 'none', + 'options' => array( + 'offset' => 3, + ), + ); + $view->display_handler->setOption('pager', $pager); + $this->executeView($view); + + $this->assertEqual(count($view->result), 8, 'Make sure that every item beside the first three is returned in the result'); + + // Check some public functions. + $this->assertFalse($view->pager->use_pager()); + $this->assertFalse($view->pager->use_count_query()); + $this->assertEqual($view->pager->get_items_per_page(), 0); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_pager_none'); + } + + public function testViewTotalRowsWithoutPager() { + $this->createNodes(23); + + $this->view->get_total_rows = TRUE; + $this->executeView($this->view); + + $this->assertEqual($this->view->total_rows, 23, "'total_rows' is calculated when pager type is 'none' and 'get_total_rows' is TRUE."); + } + + public function createNodes($count) { + if ($count >= 0) { + for ($i = 0; $i < $count; $i++) { + $this->drupalCreateNode(); + } + } + } + + /** + * Tests the some pager plugin. + */ + public function testLimit() { + $saved_view = $this->createViewFromConfig('test_pager_some'); + + // Create 11 nodes and make sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $saved_view->cloneView(); + $this->executeView($view); + $this->assertEqual(count($view->result), 5, 'Make sure that only a certain count of items is returned'); + + // Setup and test a offset. + $view = $this->getView($saved_view); + + $pager = array( + 'type' => 'none', + 'options' => array( + 'offset' => 8, + 'items_per_page' => 5, + ), + ); + $view->display_handler->setOption('pager', $pager); + $this->executeView($view); + $this->assertEqual(count($view->result), 3, 'Make sure that only a certain count of items is returned'); + + // Check some public functions. + $this->assertFalse($view->pager->use_pager()); + $this->assertFalse($view->pager->use_count_query()); + } + + /** + * Tests the normal pager. + */ + public function testNormalPager() { + $saved_view = $this->createViewFromConfig('test_pager_full'); + + // Create 11 nodes and make sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $saved_view->cloneView(); + $this->executeView($view); + $this->assertEqual(count($view->result), 5, 'Make sure that only a certain count of items is returned'); + + // Setup and test a offset. + $view = $this->getView($saved_view); + + $pager = array( + 'type' => 'full', + 'options' => array( + 'offset' => 8, + 'items_per_page' => 5, + ), + ); + $view->display_handler->setOption('pager', $pager); + $this->executeView($view); + $this->assertEqual(count($view->result), 3, 'Make sure that only a certain count of items is returned'); + + // Test items per page = 0 + $view = $this->createViewFromConfig('test_view_pager_full_zero_items_per_page'); + $this->executeView($view); + + $this->assertEqual(count($view->result), 11, 'All items are return'); + + // TODO test number of pages. + + // Test items per page = 0. + // Setup and test a offset. + $view = $this->getView($saved_view); + + $pager = array( + 'type' => 'full', + 'options' => array( + 'offset' => 0, + 'items_per_page' => 0, + ), + ); + + $view->display_handler->setOption('pager', $pager); + $this->executeView($view); + $this->assertEqual($view->pager->get_items_per_page(), 0); + $this->assertEqual(count($view->result), 11); + } + + /** + * Tests rendering with NULL pager. + */ + public function testRenderNullPager() { + // Create 11 nodes and make sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->createViewFromConfig('test_pager_full'); + $this->executeView($view); + $view->use_ajax = TRUE; // force the value again here + $view->pager = NULL; + $output = $view->render(); + $this->assertEqual(preg_match('/<ul class="pager">/', $output), 0, t('The pager is not rendered.')); + } + + /** + * Test the api functions on the view object. + */ + function testPagerApi() { + $view = $this->createViewFromConfig('test_pager_full'); + // On the first round don't initialize the pager. + + $this->assertEqual($view->getItemsPerPage(), NULL, 'If the pager is not initialized and no manual override there is no items per page.'); + $rand_number = rand(1, 5); + $view->setItemsPerPage($rand_number); + $this->assertEqual($view->getItemsPerPage(), $rand_number, 'Make sure get_items_per_page uses the settings of set_items_per_page.'); + + $this->assertEqual($view->getOffset(), NULL, 'If the pager is not initialized and no manual override there is no offset.'); + $rand_number = rand(1, 5); + $view->setOffset($rand_number); + $this->assertEqual($view->getOffset(), $rand_number, 'Make sure get_offset uses the settings of set_offset.'); + + $this->assertEqual($view->getCurrentPage(), NULL, 'If the pager is not initialized and no manual override there is no current page.'); + $rand_number = rand(1, 5); + $view->setCurrentPage($rand_number); + $this->assertEqual($view->getCurrentPage(), $rand_number, 'Make sure get_current_page uses the settings of set_current_page.'); + + $view->destroy(); + + // On this round enable the pager. + $view->initDisplay(); + $view->initQuery(); + $view->initPager(); + + $this->assertEqual($view->getItemsPerPage(), 5, 'Per default the view has 5 items per page.'); + $rand_number = rand(1, 5); + $view->setItemsPerPage($rand_number); + $rand_number = rand(6, 11); + $view->pager->set_items_per_page($rand_number); + $this->assertEqual($view->getItemsPerPage(), $rand_number, 'Make sure get_items_per_page uses the settings of set_items_per_page.'); + + $this->assertEqual($view->getOffset(), 0, 'Per default a view has a 0 offset.'); + $rand_number = rand(1, 5); + $view->setOffset($rand_number); + $rand_number = rand(6, 11); + $view->pager->set_offset($rand_number); + $this->assertEqual($view->getOffset(), $rand_number, 'Make sure get_offset uses the settings of set_offset.'); + + $this->assertEqual($view->getCurrentPage(), 0, 'Per default the current page is 0.'); + $rand_number = rand(1, 5); + $view->setCurrentPage($rand_number); + $rand_number = rand(6, 11); + $view->pager->set_current_page($rand_number); + $this->assertEqual($view->getCurrentPage(), $rand_number, 'Make sure get_current_page uses the settings of set_current_page.'); + + // Set an invalid page and make sure the method takes care about it. + $view->setCurrentPage(-1); + $this->assertEqual($view->getCurrentPage(), 0, 'Make sure setCurrentPage always sets a valid page number.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/PluginTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/PluginTestBase.php new file mode 100644 index 0000000..048690e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/PluginTestBase.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\PluginTestBase. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views\Tests\ViewTestBase; + +/** + * @todo. + */ +abstract class PluginTestBase extends ViewTestBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/QueryTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/QueryTest.php new file mode 100644 index 0000000..6ba1124 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/QueryTest.php @@ -0,0 +1,65 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\QueryTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views\Tests\ViewTestBase; +use Drupal\views_test_data\Plugin\views\query\QueryTest as QueryTestPlugin; + +/** + * Tests query plugins. + */ +class QueryTest extends ViewTestBase { + + public static function getInfo() { + return array( + 'name' => 'Query', + 'description' => 'Tests query plugins.', + 'group' => 'Views Plugins' + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + protected function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['table']['base']['query_id'] = 'query_test'; + + return $data; + } + + /** + * Tests query plugins. + */ + public function testQuery() { + $this->_testInitQuery(); + $this->_testQueryExecute(); + } + + /** + * Tests the ViewExecutable::initQuery method. + */ + public function _testInitQuery() { + $view = $this->getBasicView(); + $view->initQuery(); + $this->assertTrue($view->query instanceof QueryTestPlugin, 'Make sure the right query plugin got instantiated.'); + } + + public function _testQueryExecute() { + $view = $this->getBasicView(); + $view->initQuery(); + $view->query->setAllItems($this->dataSet()); + + $this->executeView($view); + $this->assertTrue($view->result, 'Make sure the view result got filled'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleMappingTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleMappingTest.php new file mode 100644 index 0000000..d68857a --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleMappingTest.php @@ -0,0 +1,85 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\StyleMappingTest. + */ + +namespace Drupal\views\Tests\Plugin; + +/** + * Tests the default/mapping row style. + */ +class StyleMappingTest extends StyleTestBase { + + public static function getInfo() { + return array( + 'name' => 'Style: Mapping', + 'description' => 'Test mapping style functionality.', + 'group' => 'Views Plugins', + ); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_style_mapping'); + } + + /** + * Verifies that the fields were mapped correctly. + */ + public function testMappedOutput() { + $view = $this->getView(); + $output = $this->mappedOutputHelper($view); + $this->assertTrue(strpos($output, 'job') === FALSE, 'The job field is added to the view but not in the mapping.'); + + $view = $this->getView(); + $view->displayHandlers['default']->options['style']['options']['mapping']['name_field'] = 'job'; + $output = $this->mappedOutputHelper($view); + $this->assertTrue(strpos($output, 'job') !== FALSE, 'The job field is added to the view and is in the mapping.'); + } + + /** + * Tests the mapping of fields. + * + * @param Drupal\views\ViewExecutable $view + * The view to test. + * + * @return string + * The view rendered as HTML. + */ + protected function mappedOutputHelper($view) { + $rendered_output = $view->preview(); + $this->storeViewPreview($rendered_output); + $rows = $this->elements->body->div->div->div; + $data_set = $this->dataSet(); + + $count = 0; + foreach ($rows as $row) { + $attributes = $row->attributes(); + $class = (string) $attributes['class'][0]; + $this->assertTrue(strpos($class, 'views-row-mapping-test') !== FALSE, 'Make sure that each row has the correct CSS class.'); + + foreach ($row->div as $field) { + // Split up the field-level class, the first part is the mapping name + // and the second is the field ID. + $field_attributes = $field->attributes(); + $name = strtok((string) $field_attributes['class'][0], '-'); + $field_id = strtok('-'); + + // The expected result is the mapping name and the field value, + // separated by ':'. + $expected_result = $name . ':' . $data_set[$count][$field_id]; + $actual_result = (string) $field; + $this->assertIdentical($expected_result, $actual_result, format_string('The fields were mapped successfully: %name => %field_id', array('%name' => $name, '%field_id' => $field_id))); + } + + $count++; + } + + return $rendered_output; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTest.php new file mode 100644 index 0000000..108e683 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTest.php @@ -0,0 +1,311 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\StyleTest. + */ + +namespace Drupal\views\Tests\Plugin; + +/** + * Tests some general style plugin related functionality. + * + * @see Drupal\views_test_data\Plugin\views\style\StyleTest. + */ +class StyleTest extends StyleTestBase { + + public static function getInfo() { + return array( + 'name' => 'Style: General', + 'description' => 'Test general style functionality.', + 'group' => 'Views Plugins', + ); + } + + /** + * Tests the general renderering of styles. + */ + public function testStyle() { + $view = $this->getView(); + $style = $view->display_handler->getOption('style'); + $style['type'] = 'test_style'; + $view->display_handler->setOption('style', $style); + $view->initDisplay(); + $view->initStyle(); + $this->assertTrue($view->style_plugin instanceof \Drupal\views_test_data\Plugin\views\style\StyleTest, 'Make sure the right style plugin class is loaded.'); + + $random_text = $this->randomName(); + // Set some custom text to the output and make sure that this value is + // rendered. + $view->style_plugin->setOutput($random_text); + $output = $view->preview(); + $this->assertTrue(strpos($output, $random_text) !== FALSE, 'Take sure that the rendering of the style plugin appears in the output of the view.'); + + // This run use the test row plugin and render with it. + $view = $this->getView(); + $style = $view->display_handler->getOption('style'); + $style['type'] = 'test_style'; + $view->display_handler->setOption('style', $style); + $row = $view->display_handler->getOption('row'); + $row['type'] = 'test_row'; + $view->display_handler->setOption('row', $row); + $view->initDisplay(); + $view->initStyle(); + $view->style_plugin->setUsesRowPlugin(TRUE); + // Reinitialize the style as it supports row plugins now. + $view->style_plugin->init($view, $view->display_handler, array()); + $this->assertTrue($view->style_plugin->row_plugin instanceof \Drupal\views_test_data\Plugin\views\row\RowTest, 'Make sure the right row plugin class is loaded.'); + + $random_text = $this->randomName(); + $view->style_plugin->row_plugin->setOutput($random_text); + + $output = $view->preview(); + $this->assertTrue(strpos($output, $random_text) !== FALSE, 'Take sure that the rendering of the row plugin appears in the output of the view.'); + } + + /** + * Tests the grouping legacy features of styles. + */ + function testGroupingLegacy() { + $view = $this->view->cloneView(); + // Setup grouping by the job. + $view->initDisplay(); + $view->initStyle(); + $view->style_plugin->options['grouping'] = 'job'; + + // Reduce the amount of items to make the test a bit easier. + // Set up the pager. + $view->displayHandlers['default']->overrideOption('pager', array( + 'type' => 'some', + 'options' => array('items_per_page' => 3), + )); + + // Add the job field . + $view->displayHandlers['default']->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + 'job' => array( + 'id' => 'job', + 'table' => 'views_test_data', + 'field' => 'job', + 'relationship' => 'none', + ), + )); + + // Now run the query and groupby the result. + $this->executeView($view); + + // This is the old way to call it. + $sets = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping']); + + $expected = array(); + // Use Job: as label, so be sure that the label is used for groupby as well. + $expected['Job: Singer'] = array(); + $expected['Job: Singer'][0] = new \stdClass(); + $expected['Job: Singer'][0]->views_test_data_name = 'John'; + $expected['Job: Singer'][0]->views_test_data_job = 'Singer'; + $expected['Job: Singer'][0]->views_test_data_id = '1'; + $expected['Job: Singer'][1] = new \stdClass(); + $expected['Job: Singer'][1]->views_test_data_name = 'George'; + $expected['Job: Singer'][1]->views_test_data_job = 'Singer'; + $expected['Job: Singer'][1]->views_test_data_id = '2'; + $expected['Job: Drummer'] = array(); + $expected['Job: Drummer'][2] = new \stdClass(); + $expected['Job: Drummer'][2]->views_test_data_name = 'Ringo'; + $expected['Job: Drummer'][2]->views_test_data_job = 'Drummer'; + $expected['Job: Drummer'][2]->views_test_data_id = '3'; + + $this->assertEqual($sets, $expected, t('The style plugin should proper group the results with grouping by the rendered output.')); + + $expected = array(); + $expected['Job: Singer'] = array(); + $expected['Job: Singer']['group'] = 'Job: Singer'; + $expected['Job: Singer']['rows'][0] = new \stdClass(); + $expected['Job: Singer']['rows'][0]->views_test_data_name = 'John'; + $expected['Job: Singer']['rows'][0]->views_test_data_job = 'Singer'; + $expected['Job: Singer']['rows'][0]->views_test_data_id = '1'; + $expected['Job: Singer']['rows'][1] = new \stdClass(); + $expected['Job: Singer']['rows'][1]->views_test_data_name = 'George'; + $expected['Job: Singer']['rows'][1]->views_test_data_job = 'Singer'; + $expected['Job: Singer']['rows'][1]->views_test_data_id = '2'; + $expected['Job: Drummer'] = array(); + $expected['Job: Drummer']['group'] = 'Job: Drummer'; + $expected['Job: Drummer']['rows'][2] = new \stdClass(); + $expected['Job: Drummer']['rows'][2]->views_test_data_name = 'Ringo'; + $expected['Job: Drummer']['rows'][2]->views_test_data_job = 'Drummer'; + $expected['Job: Drummer']['rows'][2]->views_test_data_id = '3'; + + // The newer api passes the value of the grouping as well. + $sets_new_rendered = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], TRUE); + $sets_new_value = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], FALSE); + + $this->assertEqual($sets_new_rendered, $expected, t('The style plugins should proper group the results with grouping by the rendered output.')); + + // Reorder the group structure to group by value. + $expected['Singer'] = $expected['Job: Singer']; + $expected['Drummer'] = $expected['Job: Drummer']; + unset($expected['Job: Singer']); + unset($expected['Job: Drummer']); + + $this->assertEqual($sets_new_value, $expected, t('The style plugins should proper group the results with grouping by the value.')); + } + + function testGrouping() { + $this->_testGrouping(FALSE); + $this->_testGrouping(TRUE); + } + + /** + * Tests the grouping features of styles. + */ + function _testGrouping($stripped = FALSE) { + $view = $this->getView(); + // Setup grouping by the job and the age field. + $view->initDisplay(); + $view->initStyle(); + $view->style_plugin->options['grouping'] = array( + array('field' => 'job'), + array('field' => 'age'), + ); + + // Reduce the amount of items to make the test a bit easier. + // Set up the pager. + $view->displayHandlers['default']->overrideOption('pager', array( + 'type' => 'some', + 'options' => array('items_per_page' => 3), + )); + + // Add the job and age field. + $view->displayHandlers['default']->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ), + 'job' => array( + 'id' => 'job', + 'table' => 'views_test_data', + 'field' => 'job', + 'relationship' => 'none', + ), + 'age' => array( + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Now run the query and groupby the result. + $this->executeView($view); + + $expected = array(); + $expected['Job: Singer'] = array(); + $expected['Job: Singer']['group'] = 'Job: Singer'; + $expected['Job: Singer']['rows']['Age: 25'] = array(); + $expected['Job: Singer']['rows']['Age: 25']['group'] = 'Age: 25'; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0] = new \stdClass(); + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_data_name = 'John'; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_data_job = 'Singer'; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_data_age = '25'; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_data_id = '1'; + $expected['Job: Singer']['rows']['Age: 27'] = array(); + $expected['Job: Singer']['rows']['Age: 27']['group'] = 'Age: 27'; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1] = new \stdClass(); + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_data_name = 'George'; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_data_job = 'Singer'; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_data_age = '27'; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_data_id = '2'; + $expected['Job: Drummer'] = array(); + $expected['Job: Drummer']['group'] = 'Job: Drummer'; + $expected['Job: Drummer']['rows']['Age: 28'] = array(); + $expected['Job: Drummer']['rows']['Age: 28']['group'] = 'Age: 28'; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2] = new \stdClass(); + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_data_name = 'Ringo'; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_data_job = 'Drummer'; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_data_age = '28'; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_data_id = '3'; + + + // Alter the results to support the stripped case. + if ($stripped) { + + // Add some html to the result and expected value. + $rand = '<a data="' . $this->randomName() . '" />'; + $view->result[0]->views_test_data_job .= $rand; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_data_job = 'Singer' . $rand; + $expected['Job: Singer']['group'] = 'Job: Singer'; + $rand = '<a data="' . $this->randomName() . '" />'; + $view->result[1]->views_test_data_job .= $rand; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_data_job = 'Singer' . $rand; + $rand = '<a data="' . $this->randomName() . '" />'; + $view->result[2]->views_test_data_job .= $rand; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_data_job = 'Drummer' . $rand; + $expected['Job: Drummer']['group'] = 'Job: Drummer'; + + $view->style_plugin->options['grouping'][0] = array('field' => 'job', 'rendered' => TRUE, 'rendered_strip' => TRUE); + $view->style_plugin->options['grouping'][1] = array('field' => 'age', 'rendered' => TRUE, 'rendered_strip' => TRUE); + } + + + // The newer api passes the value of the grouping as well. + $sets_new_rendered = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], TRUE); + + $this->assertEqual($sets_new_rendered, $expected, t('The style plugins should proper group the results with grouping by the rendered output.')); + + // Don't test stripped case, because the actual value is not stripped. + if (!$stripped) { + $sets_new_value = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], FALSE); + + // Reorder the group structure to grouping by value. + $expected['Singer'] = $expected['Job: Singer']; + $expected['Singer']['rows']['25'] = $expected['Job: Singer']['rows']['Age: 25']; + $expected['Singer']['rows']['27'] = $expected['Job: Singer']['rows']['Age: 27']; + $expected['Drummer'] = $expected['Job: Drummer']; + $expected['Drummer']['rows']['28'] = $expected['Job: Drummer']['rows']['Age: 28']; + unset($expected['Job: Singer']); + unset($expected['Singer']['rows']['Age: 25']); + unset($expected['Singer']['rows']['Age: 27']); + unset($expected['Job: Drummer']); + unset($expected['Drummer']['rows']['Age: 28']); + + $this->assertEqual($sets_new_value, $expected, t('The style plugins should proper group the results with grouping by the value.')); + } + } + + /** + * Tests custom css classes. + */ + function testCustomRowClasses() { + $view = $this->view->cloneView(); + + // Setup some random css class. + $view->initDisplay(); + $view->initStyle(); + $random_name = $this->randomName(); + $view->style_plugin->options['row_class'] = $random_name . " test-token-[name]"; + + $rendered_output = $view->preview(); + $this->storeViewPreview($rendered_output); + + $rows = $this->elements->body->div->div->div; + $count = 0; + foreach ($rows as $row) { + $attributes = $row->attributes(); + $class = (string) $attributes['class'][0]; + $this->assertTrue(strpos($class, $random_name) !== FALSE, 'Take sure that a custom css class is added to the output.'); + + // Check token replacement. + $name = $view->field['name']->get_value($view->result[$count]); + $this->assertTrue(strpos($class, "test-token-$name") !== FALSE, 'Take sure that a token in custom css class is replaced.'); + + $count++; + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTestBase.php new file mode 100644 index 0000000..20fb2d6 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleTestBase.php @@ -0,0 +1,43 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\StyleTestBase. + */ + +namespace Drupal\views\Tests\Plugin; + +use DOMDocument; + +/** + * Tests some general style plugin related functionality. + */ +abstract class StyleTestBase extends PluginTestBase { + + /** + * Stores the SimpleXML representation of the output. + * + * @var SimpleXMLElement + */ + protected $elements; + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Stores a view output in the elements. + */ + function storeViewPreview($output) { + $htmlDom = new DOMDocument(); + @$htmlDom->loadHTML($output); + if ($htmlDom) { + // It's much easier to work with simplexml than DOM, luckily enough + // we can just simply import our DOM tree. + $this->elements = simplexml_import_dom($htmlDom); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleUnformattedTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleUnformattedTest.php new file mode 100644 index 0000000..bcc2e8c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleUnformattedTest.php @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Plugin\StyleUnformattedTest. + */ + +namespace Drupal\views\Tests\Plugin; + +/** + * Tests the default/unformatted row style. + */ +class StyleUnformattedTest extends StyleTestBase { + + public static function getInfo() { + return array( + 'name' => 'Style: Unformatted', + 'description' => 'Test unformatted style functionality.', + 'group' => 'Views Plugins', + ); + } + + /** + * Take sure that the default css classes works as expected. + */ + function testDefaultRowClasses() { + $view = $this->getView(); + $rendered_output = $view->preview(); + $this->storeViewPreview($rendered_output); + + $rows = $this->elements->body->div->div->div; + $count = 0; + $count_result = count($view->result); + foreach ($rows as $row) { + $count++; + $attributes = $row->attributes(); + $class = (string) $attributes['class'][0]; + // Take sure that each row has a row css class. + $this->assertTrue(strpos($class, "views-row-$count") !== FALSE, 'Take sure that each row has a row css class.'); + // Take sure that the odd/even classes are set right. + $odd_even = $count % 2 == 0 ? 'even' : 'odd'; + $this->assertTrue(strpos($class, "views-row-$odd_even") !== FALSE, 'Take sure that the odd/even classes are set right.'); + + if ($count == 1) { + $this->assertTrue(strpos($class, "views-row-first") !== FALSE, 'Take sure that the first class is set right.'); + } + elseif ($count == $count_result) { + $this->assertTrue(strpos($class, "views-row-last") !== FALSE, 'Take sure that the last class is set right.'); + + } + $this->assertTrue(strpos($class, 'views-row') !== FALSE, 'Take sure that the views row class is set right.'); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/PluginInstanceTest.php b/core/modules/views/lib/Drupal/views/Tests/PluginInstanceTest.php new file mode 100644 index 0000000..b918772 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/PluginInstanceTest.php @@ -0,0 +1,108 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\PluginInstanceTest. + */ + +namespace Drupal\views\Tests; + +use ReflectionClass; + +/** + * Checks general plugin data and instances for all plugin types. + */ +class PluginInstanceTest extends ViewTestBase { + + /** + * All views plugin types. + * + * @var array + */ + protected $pluginTypes = array( + 'access', + 'area', + 'argument', + 'argument_default', + 'argument_validator', + 'cache', + 'display_extender', + 'display', + 'exposed_form', + 'field', + 'filter', + 'join', + 'pager', + 'query', + 'relationship', + 'row', + 'sort', + 'style', + 'wizard', + ); + + /** + * An array of plugin definitions, keyed by plugin type. + * + * @var array + */ + protected $definitions; + + public static function getInfo() { + return array( + 'name' => 'Plugin instantiation', + 'description' => 'Tests that an instance of all views plugins can be created.', + 'group' => 'Views', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->definitions = views_get_plugin_definitions(); + } + + /** + * Confirms that there is plugin data for all views plugin types. + */ + public function testPluginData() { + // Check that we have an array of data. + $this->assertTrue(is_array($this->definitions), 'Plugin data is an array.'); + + // Check all plugin types. + foreach ($this->pluginTypes as $type) { + $this->assertTrue(array_key_exists($type, $this->definitions), format_string('Key for plugin type @type found.', array('@type' => $type))); + $this->assertTrue(is_array($this->definitions[$type]) && !empty($this->definitions[$type]), format_string('Plugin type @type has an array of plugins.', array('@type' => $type))); + } + + // Tests that the plugin list has not missed any types. + $diff = array_diff(array_keys($this->definitions), $this->pluginTypes); + $this->assertTrue(empty($diff), 'All plugins were found and matched.'); + } + + /** + * Tests creating instances of every views plugin. + * + * This will iterate through all plugins from _views_fetch_plugin_data(). + */ + public function testPluginInstances() { + $container = drupal_container(); + foreach ($this->definitions as $type => $plugins) { + // Get a plugin manager for this type. + $manager = $container->get("plugin.manager.views.$type"); + foreach ($plugins as $definition) { + // Get a reflection class for this plugin. + // We only want to test true plugins, i.e. They extend PluginBase. + $reflection = new ReflectionClass($definition['class']); + if ($reflection->isSubclassOf('Drupal\views\Plugin\views\PluginBase')) { + // Create a plugin instance and check what it is. This is not just + // good to check they can be created but for throwing any notices for + // method signatures etc... too. + $instance = $manager->createInstance($definition['id']); + $this->assertTrue($instance instanceof $definition['class'], format_string('Instance of @type:@id created', array('@type' => $type, '@id' => $definition['id']))); + } + } + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/QueryGroupByTest.php b/core/modules/views/lib/Drupal/views/Tests/QueryGroupByTest.php new file mode 100644 index 0000000..cb5416b --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/QueryGroupByTest.php @@ -0,0 +1,149 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\QueryGroupByTest. + */ + +namespace Drupal\views\Tests; + +/** + * Tests aggregate functionality of views, for example count. + */ +class QueryGroupByTest extends ViewTestBase { + + public static function getInfo() { + return array( + 'name' => 'Groupby', + 'description' => 'Tests aggregate functionality of views, for example count.', + 'group' => 'Views', + ); + } + + /** + * Tests aggregate count feature. + */ + public function testAggregateCount() { + // Create 2 nodes of type1 and 3 nodes of type2 + $type1 = $this->drupalCreateContentType(); + $type2 = $this->drupalCreateContentType(); + + $node_1 = array( + 'type' => $type1->type, + ); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + + $node_2 = array( + 'type' => $type2->type, + ); + $this->drupalCreateNode($node_2); + $this->drupalCreateNode($node_2); + $this->drupalCreateNode($node_2); + + $view = $this->createViewFromConfig('test_aggregate_count'); + $this->executeView($view); + + $this->assertEqual(count($view->result), 2, 'Make sure the count of items is right.'); + + $types = array(); + foreach ($view->result as $item) { + // num_records is a alias for nid. + $types[$item->node_type] = $item->num_records; + } + + $this->assertEqual($types[$type1->type], 4); + $this->assertEqual($types[$type2->type], 3); + } + + //public function testAggregateSum() { + //} + + /** + * @param $group_by + * Which group_by function should be used, for example sum or count. + */ + function GroupByTestHelper($group_by, $values) { + // Create 2 nodes of type1 and 3 nodes of type2 + $type1 = $this->drupalCreateContentType(); + $type2 = $this->drupalCreateContentType(); + + $node_1 = array( + 'type' => $type1->type, + ); + // Nids from 1 to 4. + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $node_2 = array( + 'type' => $type2->type, + ); + // Nids from 5 to 7. + $this->drupalCreateNode($node_2); + $this->drupalCreateNode($node_2); + $this->drupalCreateNode($node_2); + + $view = $this->createViewFromConfig('test_group_by_count'); + $view->displayHandlers['default']->options['fields']['nid']['group_type'] = $group_by; + $this->executeView($view); + + $this->assertEqual(count($view->result), 2, 'Make sure the count of items is right.'); + // Group by nodetype to identify the right count. + foreach ($view->result as $item) { + $results[$item->node_type] = $item->nid; + } + $this->assertEqual($results[$type1->type], $values[0]); + $this->assertEqual($results[$type2->type], $values[1]); + } + + public function testGroupByCount() { + $this->GroupByTestHelper('count', array(4, 3)); + } + + function testGroupBySum() { + $this->GroupByTestHelper('sum', array(10, 18)); + } + + + function testGroupByAverage() { + $this->GroupByTestHelper('avg', array(2.5, 6)); + } + + function testGroupByMin() { + $this->GroupByTestHelper('min', array(1, 5)); + } + + function testGroupByMax() { + $this->GroupByTestHelper('max', array(4, 7)); + } + + public function testGroupByCountOnlyFilters() { + // Check if GROUP BY and HAVING are included when a view + // Doesn't display SUM, COUNT, MAX... functions in SELECT statment + + $type1 = $this->drupalCreateContentType(); + + $node_1 = array( + 'type' => $type1->type, + ); + for ($x = 0; $x < 10; $x++) { + $this->drupalCreateNode($node_1); + } + + $this->executeView($this->view); + + $this->assertTrue(strpos($this->view->build_info['query'], 'GROUP BY'), t('Make sure that GROUP BY is in the query')); + $this->assertTrue(strpos($this->view->build_info['query'], 'HAVING'), t('Make sure that HAVING is in the query')); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_group_by_in_filters'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Taxonomy/RelationshipNodeTermDataTest.php b/core/modules/views/lib/Drupal/views/Tests/Taxonomy/RelationshipNodeTermDataTest.php new file mode 100644 index 0000000..dbfd8fa --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Taxonomy/RelationshipNodeTermDataTest.php @@ -0,0 +1,145 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Taxonomy\RelationshipNodeTermDataTest. + */ + +namespace Drupal\views\Tests\Taxonomy; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Tests the node_term_data relationship handler. + */ +class RelationshipNodeTermDataTest extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('taxonomy'); + + /** + * The vocabulary for the test. + * + * @var Drupal\taxonomy\Vocabulary + */ + protected $vocabulary; + + public static function getInfo() { + return array( + 'name' => 'Taxonomy: Node term data Relationship', + 'description' => 'Tests the taxonomy term on node relationship handler.', + 'group' => 'Views Modules', + ); + } + + /** + * Returns a new term with random properties in vocabulary $vid. + */ + function createTerm() { + $term = entity_create('taxonomy_term', array( + 'name' => $this->randomName(), + 'description' => $this->randomName(), + // Use the first available text format. + 'format' => db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField(), + 'vid' => $this->vocabulary->vid, + 'langcode' => LANGUAGE_NOT_SPECIFIED, + )); + taxonomy_term_save($term); + return $term; + } + + function setUp() { + parent::setUp(); + $this->mockStandardInstall(); + + $this->term_1 = $this->createTerm(); + $this->term_2 = $this->createTerm(); + + $node = array(); + $node['type'] = 'article'; + $node['field_views_testing_tags'][LANGUAGE_NOT_SPECIFIED][]['tid'] = $this->term_1->tid; + $node['field_views_testing_tags'][LANGUAGE_NOT_SPECIFIED][]['tid'] = $this->term_2->tid; + $this->node = $this->drupalCreateNode($node); + } + + /** + * Provides a workaround for the inability to use the standard profile. + * + * @see http://drupal.org/node/1708692 + */ + protected function mockStandardInstall() { + $type = array( + 'type' => 'article', + ); + + $type = node_type_set_defaults($type); + node_type_save($type); + node_add_body_field($type); + + // Create the vocabulary for the tag field. + $this->vocabulary = entity_create('taxonomy_vocabulary', array( + 'name' => 'Views testing tags', + 'machine_name' => 'views_testing_tags', + )); + $this->vocabulary->save(); + $field = array( + 'field_name' => 'field_' . $this->vocabulary->machine_name, + 'type' => 'taxonomy_term_reference', + // Set cardinality to unlimited for tagging. + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary->machine_name, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($field); + $instance = array( + 'field_name' => 'field_' . $this->vocabulary->machine_name, + 'entity_type' => 'node', + 'label' => 'Tags', + 'bundle' => 'article', + 'widget' => array( + 'type' => 'taxonomy_autocomplete', + 'weight' => -4, + ), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + 'teaser' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + ), + ); + field_create_instance($instance); + } + + function testViewsHandlerRelationshipNodeTermData() { + $this->executeView($this->view, array($this->term_1->tid, $this->term_2->tid)); + $resultset = array( + array( + 'nid' => $this->node->nid, + ), + ); + $this->column_map = array('nid' => 'nid'); + $this->assertIdenticalResultset($this->view, $resultset, $this->column_map); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_taxonomy_node_term_data'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/DefaultViewsTest.php b/core/modules/views/lib/Drupal/views/Tests/UI/DefaultViewsTest.php new file mode 100644 index 0000000..9142a32 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/DefaultViewsTest.php @@ -0,0 +1,129 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UI\DefaultViewsTest. + */ + +namespace Drupal\views\Tests\UI; + +/** + * Tests enabling, disabling, and reverting default views via the listing page. + */ +class DefaultViewsTest extends UITestBase { + + public static function getInfo() { + return array( + 'name' => 'Default views functionality', + 'description' => 'Test enabling, disabling, and reverting default views via the listing page.', + 'group' => 'Views UI', + ); + } + + /** + * Tests default views. + */ + function testDefaultViews() { + // Make sure the front page view starts off as disabled (does not appear on + // the listing page). + $edit_href = 'admin/structure/views/view/frontpage/edit'; + $this->drupalGet('admin/structure/views'); + // @todo Disabled default views do now appear on the front page. Test this + // behavior with templates instead. + // $this->assertNoLinkByHref($edit_href); + + // Enable the front page view, and make sure it is now visible on the main + // listing page. + $this->drupalGet('admin/structure/views/templates'); + $this->clickViewsOperationLink(t('Enable'), '/frontpage/'); + $this->assertUrl('admin/structure/views'); + $this->assertLinkByHref($edit_href); + + // It should not be possible to revert the view yet. + // @todo Figure out how to handle this with the new configuration system. + // $this->assertNoLink(t('Revert')); + // $revert_href = 'admin/structure/views/view/frontpage/revert'; + // $this->assertNoLinkByHref($revert_href); + + // Edit the view and change the title. Make sure that the new title is + // displayed. + $new_title = $this->randomName(16); + $edit = array('title' => $new_title); + $this->drupalPost('admin/structure/views/nojs/display/frontpage/page/title', $edit, t('Apply')); + $this->drupalPost('admin/structure/views/view/frontpage/edit/page', array(), t('Save')); + $this->drupalGet('frontpage'); + $this->assertResponse(200); + $this->assertText($new_title); + + // Save another view in the UI. + $this->drupalPost('admin/structure/views/nojs/display/archive/page/title', array(), t('Apply')); + $this->drupalPost('admin/structure/views/view/archive/page', array(), t('Save')); + + // Check there is an enable link. i.e. The view has not been enabled after + // editing. + $this->drupalGet('admin/structure/views'); + $this->assertLinkByHref('admin/structure/views/view/archive/enable'); + + // It should now be possible to revert the view. Do that, and make sure the + // view title we added above no longer is displayed. + // $this->drupalGet('admin/structure/views'); + // $this->assertLink(t('Revert')); + // $this->assertLinkByHref($revert_href); + // $this->drupalPost($revert_href, array(), t('Revert')); + // $this->drupalGet('frontpage'); + // $this->assertNoText($new_title); + + // Now disable the view, and make sure it stops appearing on the main view + // listing page but instead goes back to displaying on the disabled views + // listing page. + // @todo Test this behavior with templates instead. + $this->drupalGet('admin/structure/views'); + $this->clickViewsOperationLink(t('Disable'), '/frontpage/'); + // $this->assertUrl('admin/structure/views'); + // $this->assertNoLinkByHref($edit_href); + // The easiest way to verify it appears on the disabled views listing page + // is to try to click the "enable" link from there again. + $this->drupalGet('admin/structure/views/templates'); + $this->clickViewsOperationLink(t('Enable'), '/frontpage/'); + $this->assertUrl('admin/structure/views'); + $this->assertLinkByHref($edit_href); + } + + /** + * Click a link to perform an operation on a view. + * + * In general, we expect lots of links titled "enable" or "disable" on the + * various views listing pages, and they might have tokens in them. So we + * need special code to find the correct one to click. + * + * @param $label + * Text between the anchor tags of the desired link. + * @param $unique_href_part + * A unique string that is expected to occur within the href of the desired + * link. For example, if the link URL is expected to look like + * "admin/structure/views/view/frontpage/...", then "/frontpage/" could be + * passed as the expected unique string. + * + * @return + * The page content that results from clicking on the link, or FALSE on + * failure. Failure also results in a failed assertion. + */ + function clickViewsOperationLink($label, $unique_href_part) { + $links = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label)); + foreach ($links as $link_index => $link) { + $position = strpos($link['href'], $unique_href_part); + if ($position !== FALSE) { + $index = $link_index; + break; + } + } + $this->assertTrue(isset($index), t('Link to "@label" containing @part found.', array('@label' => $label, '@part' => $unique_href_part))); + if (isset($index)) { + return $this->clickLink($label, $index); + } + else { + return FALSE; + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/DisplayExtenderUITest.php b/core/modules/views/lib/Drupal/views/Tests/UI/DisplayExtenderUITest.php new file mode 100644 index 0000000..e46aecb --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/DisplayExtenderUITest.php @@ -0,0 +1,45 @@ +<?php + + /** + * @file + * Definition of Drupal\views\Tests\UI\DisplayExtenderUITest. + */ + +namespace Drupal\views\Tests\UI; + +/** + * Tests the display extender UI. + */ +class DisplayExtenderUITest extends UITestBase { + + public static function getInfo() { + return array( + 'name' => 'Display extender: UI', + 'description' => 'Tests the display extender UI.', + 'group' => 'Views UI', + ); + } + + /** + * Tests the display extender UI. + */ + public function testDisplayExtenderUI() { + config('views.settings')->set('display_extenders', array('display_extender_test'))->save(); + + $view = $this->getView(); + $view_edit_url = "admin/structure/views/view/{$view->storage->name}/edit"; + $display_option_url = 'admin/structure/views/nojs/display/test_view/default/test_extender_test_option'; + + $this->drupalGet($view_edit_url); + $this->assertLinkByHref($display_option_url, 0, 'Make sure the option defined by the test display extender appears in the UI.'); + + $random_text = $this->randomName(); + $this->drupalPost($display_option_url, array('test_extender_test_option' => $random_text), t('Apply')); + $this->assertLink($random_text); + $this->drupalPost(NULL, array(), t('Save')); + $view = views_get_view($view->storage->name); + $view->initDisplay(); + $this->assertEqual($view->display_handler->getOption('test_extender_test_option'), $random_text, 'Make sure that the display extender option got saved.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/DisplayTest.php b/core/modules/views/lib/Drupal/views/Tests/UI/DisplayTest.php new file mode 100644 index 0000000..cf24c74 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/DisplayTest.php @@ -0,0 +1,186 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UI\DisplayTest. + */ + +namespace Drupal\views\Tests\UI; + +/** + * Tests the handling of displays in the UI, adding removing etc. + */ +class DisplayTest extends UITestBase { + + public static function getInfo() { + return array( + 'name' => 'Display tests', + 'description' => 'Tests the handling of displays in the UI, adding removing etc.', + 'group' => 'Views UI', + ); + } + + /** + * A helper method which creates a random view. + */ + public function randomView(array $view = array()) { + // Create a new view in the UI. + $default = array(); + $default['human_name'] = $this->randomName(16); + $default['name'] = strtolower($this->randomName(16)); + $default['description'] = $this->randomName(16); + $default['page[create]'] = TRUE; + $default['page[path]'] = $default['name']; + + $view += $default; + + $this->drupalPost('admin/structure/views/add', $view, t('Continue & edit')); + + return $default; + } + + /** + * Tests removing a display. + */ + public function testRemoveDisplay() { + $view = $this->randomView(); + $path_prefix = 'admin/structure/views/view/' . $view['name'] .'/edit'; + + $this->drupalGet($path_prefix . '/default'); + $this->assertNoFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-delete', 'delete Page', 'Make sure there is no delete button on the default display.'); + + $this->drupalGet($path_prefix . '/page'); + $this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-delete', 'delete Page', 'Make sure there is a delete button on the page display.'); + + // Delete the page, so we can test the undo process. + $this->drupalPost($path_prefix . '/page', array(), 'delete Page'); + $this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-undo-delete', 'undo delete of Page', 'Make sure there a undo button on the page display after deleting.'); + $this->assertTrue($this->xpath('//div[contains(@class, views-display-deleted-link)]'). 'Make sure the display link is marked as to be deleted.'); + + // Undo the deleting of the display. + $this->drupalPost($path_prefix . '/page', array(), 'undo delete of Page'); + $this->assertNoFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-undo-delete', 'undo delete of Page', 'Make sure there is no undo button on the page display after reverting.'); + $this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-delete', 'delete Page', 'Make sure there is a delete button on the page display after the reverting.'); + + // Now delete again and save the view. + $this->drupalPost($path_prefix . '/page', array(), 'delete Page'); + $this->drupalPost(NULL, array(), t('Save')); + + $this->assertNoLinkByHref($path_prefix . '/page', 'Make sure there is no display tab for the deleted display.'); + } + + /** + * Tests adding a display. + */ + public function testAddDisplay() { + // Show the master display. + config('views.settings')->set('ui.show.master_display', TRUE)->save(); + + $settings['page[create]'] = FALSE; + $view = $this->randomView($settings); + + $path_prefix = 'admin/structure/views/view/' . $view['name'] .'/edit'; + $this->drupalGet($path_prefix); + $this->drupalPost(NULL, array(), t('Save')); + + // Add a new display. + $this->drupalPost(NULL, array(), 'Add Page'); + // @todo Revising this after http://drupal.org/node/1793700 got in. + $this->assertLinkByHref($path_prefix . '/page_1', 0, 'Make sure after adding a display the new display appears in the UI'); + + $this->assertNoLink('Master*', 0, 'Make sure the master display is not marked as changed.'); + $this->assertLink('Page*', 0, 'Make sure the added display is marked as changed.'); + } + + /** + * Tests reordering of displays. + */ + public function testReorderDisplay() { + $view = array( + 'block[create]' => TRUE + ); + $view = $this->randomView($view); + $path_prefix = 'admin/structure/views/view/' . $view['name'] .'/edit'; + + $edit = array(); + $this->drupalPost($path_prefix, $edit, t('Save')); + $this->clickLink(t('reorder displays')); + $this->assertTrue($this->xpath('//tr[@id="display-row-default"]'), 'Make sure the default display appears on the reorder listing'); + $this->assertTrue($this->xpath('//tr[@id="display-row-page"]'), 'Make sure the page display appears on the reorder listing'); + $this->assertTrue($this->xpath('//tr[@id="display-row-block"]'), 'Make sure the block display appears on the reorder listing'); + + // Put the block display in front of the page display. + $edit = array( + 'page[weight]' => 2, + 'block[weight]' => 1 + ); + $this->drupalPost(NULL, $edit, t('Apply')); + $this->drupalPost(NULL, array(), t('Save')); + + $view = views_get_view($view['name']); + $this->assertEqual($view->storage->display['default']['position'], 0, 'Make sure the master display comes first.'); + $this->assertEqual($view->storage->display['block']['position'], 1, 'Make sure the block display comes before the page display.'); + $this->assertEqual($view->storage->display['page']['position'], 2, 'Make sure the page display comes after the block display.'); + } + + /** + * Tests that the correct display is loaded by default. + */ + public function testDefaultDisplay() { + $this->drupalGet('admin/structure/views/view/test_display'); + $elements = $this->xpath('//*[@id="views-page-display-title"]'); + $this->assertEqual(count($elements), 1, 'The page display is loaded as the default display.'); + } + + /** + * Tests the cloning of a display. + */ + public function testCloneDisplay() { + $view = $this->randomView(); + $path_prefix = 'admin/structure/views/view/' . $view['name'] .'/edit'; + + $this->drupalGet($path_prefix); + $this->drupalPost(NULL, array(), 'clone Page'); + // @todo Revising this after http://drupal.org/node/1793700 got in. + $this->assertLinkByHref($path_prefix . '/page_1', 0, 'Make sure after cloning the new display appears in the UI'); + } + + /** + * Tests disabling of a display. + */ + public function testDisableDisplay() { + $view = $this->randomView(); + $path_prefix = 'admin/structure/views/view/' . $view['name'] .'/edit'; + + $this->drupalGet($path_prefix); + $this->assertFalse($this->xpath('//div[contains(@class, :class)]', array(':class' => 'views-display-disabled')), 'Make sure the disabled display css class does not appear after initial adding of a view.'); + + $this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-disable', '', 'Make sure the disable button is visible.'); + $this->assertNoFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-enable', '', 'Make sure the enable button is not visible.'); + $this->drupalPost(NULL, array(), 'disable Page'); + $this->assertTrue($this->xpath('//div[contains(@class, :class)]', array(':class' => 'views-display-disabled')), 'Make sure the disabled display css class appears once the display is marked as such.'); + + $this->assertNoFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-disable', '', 'Make sure the disable button is not visible.'); + $this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-enable', '', 'Make sure the enable button is visible.'); + $this->drupalPost(NULL, array(), 'enable Page'); + $this->assertFalse($this->xpath('//div[contains(@class, :class)]', array(':class' => 'views-display-disabled')), 'Make sure the disabled display css class does not appears once the display is enabled again.'); + } + + /** + * Tests views_ui_views_plugins_display_alter is altering plugin definitions. + */ + public function testDisplayPluginsAlter() { + $definitions = drupal_container()->get('plugin.manager.views.display')->getDefinitions(); + + $expected = array( + 'parent path' => 'admin/structure/views/view', + 'argument properties' => array('name'), + ); + + // Test the expected views_ui array exists on each definition. + foreach ($definitions as $definition) { + $this->assertIdentical($definition['contextual links']['views_ui'], $expected, 'Expected views_ui array found in plugin definition.'); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/GroupByTest.php b/core/modules/views/lib/Drupal/views/Tests/UI/GroupByTest.php new file mode 100644 index 0000000..f0abbf7 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/GroupByTest.php @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UI\GroupByTest. + */ + +namespace Drupal\views\Tests\UI; + +/** + * Tests UI of aggregate functionality.. + */ +class GroupByTest extends UITestBase { + + public static function getInfo() { + return array( + 'name' => 'Group By functionality', + 'description' => 'Tests UI of aggregate functionality.', + 'group' => 'Views UI', + ); + } + + /** + * Tests whether basic saving works. + * + * @todo This should check the change of the settings as well. + */ + function testGroupBySave() { + $this->drupalGet('admin/structure/views/view/test_views_groupby_save/edit'); + + $edit = array( + 'group_by' => TRUE, + ); + $this->drupalPost('admin/structure/views/nojs/display/test_views_groupby_save/default/group_by', $edit, t('Apply')); + + $this->drupalGet('admin/structure/views/view/test_views_groupby_save/edit'); + $this->drupalPost('admin/structure/views/view/test_views_groupby_save/edit', array(), t('Save')); + + $this->drupalGet('admin/structure/views/nojs/display/test_views_groupby_save/default/group_by'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/OverrideDisplaysTest.php b/core/modules/views/lib/Drupal/views/Tests/UI/OverrideDisplaysTest.php new file mode 100644 index 0000000..3c7e6cc --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/OverrideDisplaysTest.php @@ -0,0 +1,202 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UI\OverrideDisplaysTest. + */ + +namespace Drupal\views\Tests\UI; + +/** + * Tests that displays can be correctly overridden via the user interface. + */ +class OverrideDisplaysTest extends UITestBase { + + public static function getInfo() { + return array( + 'name' => 'Overridden displays functionality', + 'description' => 'Test that displays can be correctly overridden via the user interface.', + 'group' => 'Views UI', + ); + } + + /** + * Tests that displays can be overridden via the UI. + */ + function testOverrideDisplays() { + // Create a basic view that shows all content, with a page and a block + // display. + $view['human_name'] = $this->randomName(16); + $view['name'] = strtolower($this->randomName(16)); + $view['page[create]'] = 1; + $view['page[path]'] = $this->randomName(16); + $view['block[create]'] = 1; + $view_path = $view['page[path]']; + $this->drupalPost('admin/structure/views/add', $view, t('Save & exit')); + + // Configure its title. Since the page and block both started off with the + // same (empty) title in the views wizard, we expect the wizard to have set + // things up so that they both inherit from the default display, and we + // therefore only need to change that to have it take effect for both. + $edit = array(); + $edit['title'] = $original_title = $this->randomName(16); + $edit['override[dropdown]'] = 'default'; + $this->drupalPost("admin/structure/views/nojs/display/{$view['name']}/page/title", $edit, t('Apply')); + $this->drupalPost("admin/structure/views/view/{$view['name']}/edit/page", array(), t('Save')); + + // Put the block into the first sidebar region, and make sure it will not + // display on the view's page display (since we will be searching for the + // presence/absence of the view's title in both the page and the block). + $this->drupalGet('admin/structure/block'); + $edit = array(); + $edit["blocks[views_{$view['name']}-block][region]"] = 'sidebar_first'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + $edit = array(); + $edit['visibility'] = BLOCK_VISIBILITY_NOTLISTED; + $edit['pages'] = $view_path; + $this->drupalPost("admin/structure/block/manage/views/{$view['name']}-block/configure", $edit, t('Save block')); + + // Add a node that will appear in the view, so that the block will actually + // be displayed. + $this->drupalCreateNode(); + + // Make sure the title appears in both the page and the block. + $this->drupalGet($view_path); + $this->assertResponse(200); + $this->assertText($original_title); + $this->drupalGet(''); + $this->assertText($original_title); + + // Change the title for the page display only, and make sure that is the + // only one that is changed. + $edit = array(); + $edit['title'] = $new_title = $this->randomName(16); + $edit['override[dropdown]'] = 'page'; + $this->drupalPost("admin/structure/views/nojs/display/{$view['name']}/page/title", $edit, t('Apply')); + $this->drupalPost("admin/structure/views/view/{$view['name']}/edit/page", array(), t('Save')); + $this->drupalGet($view_path); + $this->assertResponse(200); + $this->assertText($new_title); + $this->assertNoText($original_title); + $this->drupalGet(''); + $this->assertText($original_title); + $this->assertNoText($new_title); + } + + /** + * Tests that the wizard correctly sets up default and overridden displays. + */ + function testWizardMixedDefaultOverriddenDisplays() { + // Create a basic view with a page, block, and feed. Give the page and feed + // identical titles, but give the block a different one, so we expect the + // page and feed to inherit their titles from the default display, but the + // block to override it. + $view['human_name'] = $this->randomName(16); + $view['name'] = strtolower($this->randomName(16)); + $view['page[create]'] = 1; + $view['page[title]'] = $this->randomName(16); + $view['page[path]'] = $this->randomName(16); + $view['page[feed]'] = 1; + $view['page[feed_properties][path]'] = $this->randomName(16); + $view['block[create]'] = 1; + $view['block[title]'] = $this->randomName(16); + $this->drupalPost('admin/structure/views/add', $view, t('Save & exit')); + + // Put the block into the first sidebar region, and make sure it will not + // display on the view's page display (since we will be searching for the + // presence/absence of the view's title in both the page and the block). + $this->drupalGet('admin/structure/block'); + $edit = array(); + $edit["blocks[views_{$view['name']}-block][region]"] = 'sidebar_first'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + $edit = array(); + $edit['visibility'] = BLOCK_VISIBILITY_NOTLISTED; + $edit['pages'] = $view['page[path]']; + $this->drupalPost("admin/structure/block/manage/views/{$view['name']}-block/configure", $edit, t('Save block')); + + // Add a node that will appear in the view, so that the block will actually + // be displayed. + $this->drupalCreateNode(); + + // Make sure that the feed, page and block all start off with the correct + // titles. + $this->drupalGet($view['page[path]']); + $this->assertResponse(200); + $this->assertText($view['page[title]']); + $this->assertNoText($view['block[title]']); + $this->drupalGet($view['page[feed_properties][path]']); + $this->assertResponse(200); + $this->assertText($view['page[title]']); + $this->assertNoText($view['block[title]']); + $this->drupalGet(''); + $this->assertText($view['block[title]']); + $this->assertNoText($view['page[title]']); + + // Edit the page and change the title. This should automatically change + // the feed's title also, but not the block. + $edit = array(); + $edit['title'] = $new_default_title = $this->randomName(16); + $this->drupalPost("admin/structure/views/nojs/display/{$view['name']}/page/title", $edit, t('Apply')); + $this->drupalPost("admin/structure/views/view/{$view['name']}/edit/page", array(), t('Save')); + $this->drupalGet($view['page[path]']); + $this->assertResponse(200); + $this->assertText($new_default_title); + $this->assertNoText($view['page[title]']); + $this->assertNoText($view['block[title]']); + $this->drupalGet($view['page[feed_properties][path]']); + $this->assertResponse(200); + $this->assertText($new_default_title); + $this->assertNoText($view['page[title]']); + $this->assertNoText($view['block[title]']); + $this->drupalGet(''); + $this->assertText($view['block[title]']); + $this->assertNoText($new_default_title); + $this->assertNoText($view['page[title]']); + + // Edit the block and change the title. This should automatically change + // the block title only, and leave the defaults alone. + $edit = array(); + $edit['title'] = $new_block_title = $this->randomName(16); + $this->drupalPost("admin/structure/views/nojs/display/{$view['name']}/block/title", $edit, t('Apply')); + $this->drupalPost("admin/structure/views/view/{$view['name']}/edit/block", array(), t('Save')); + $this->drupalGet($view['page[path]']); + $this->assertResponse(200); + $this->assertText($new_default_title); + $this->assertNoText($new_block_title); + $this->drupalGet($view['page[feed_properties][path]']); + $this->assertResponse(200); + $this->assertText($new_default_title); + $this->assertNoText($new_block_title); + $this->drupalGet(''); + $this->assertText($new_block_title); + $this->assertNoText($view['block[title]']); + } + + /** + * Tests that the revert to all displays select-option works as expected. + */ + function testRevertAllDisplays() { + // Create a basic view with a page, block. + // Because there is both a title on page and block we expect the title on + // the block be overriden. + $view['human_name'] = $this->randomName(16); + $view['name'] = strtolower($this->randomName(16)); + $view['page[create]'] = 1; + $view['page[title]'] = $this->randomName(16); + $view['page[path]'] = $this->randomName(16); + $view['block[create]'] = 1; + $view['block[title]'] = $this->randomName(16); + $this->drupalPost('admin/structure/views/add', $view, t('Continue & edit')); + + // Revert the title of the block back to the default ones, but submit some + // new values to be sure that the new value is not stored. + $edit = array(); + $edit['title'] = $new_block_title = $this->randomName(); + $edit['override[dropdown]'] = 'default_revert'; + + $this->drupalPost("admin/structure/views/nojs/display/{$view['name']}/block/title", $edit, t('Apply')); + $this->drupalPost("admin/structure/views/view/{$view['name']}/edit/block", array(), t('Save')); + $this->assertText($view['page[title]']); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/RowUITest.php b/core/modules/views/lib/Drupal/views/Tests/UI/RowUITest.php new file mode 100644 index 0000000..7d7b245 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/RowUITest.php @@ -0,0 +1,61 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UI\RowUITest. + */ + +namespace Drupal\views\Tests\UI; + +/** + * Tests the UI of row plugins. + * + * @see Drupal\views_test_data\Plugin\views\row\RowTest. + */ +class RowUITest extends UITestBase { + + public static function getInfo() { + return array( + 'name' => 'Row: UI', + 'description' => 'Tests the UI of row plugins.', + 'group' => 'Views UI', + ); + } + + /** + * Tests changing the row plugin and changing some options of a row. + */ + public function testRowUI() { + $view = $this->getView(); + $view_edit_url = "admin/structure/views/view/{$view->storage->name}/edit"; + + $row_plugin_url = "admin/structure/views/nojs/display/{$view->storage->name}/default/row"; + $row_options_url = "admin/structure/views/nojs/display/{$view->storage->name}/default/row_options"; + + $this->drupalGet($row_plugin_url); + $this->assertFieldByName('row', 'fields', 'The default row plugin selected in the UI should be fields.'); + + $edit = array( + 'row' => 'test_row' + ); + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertFieldByName('row_options[test_option]', NULL, 'Make sure the custom settings form from the test plugin appears.'); + $random_name = $this->randomName(); + $edit = array( + 'row_options[test_option]' => $random_name + ); + $this->drupalPost(NULL, $edit, t('Apply')); + $this->drupalGet($row_options_url); + $this->assertFieldByName('row_options[test_option]', $random_name, 'Make sure the custom settings form field has the expected value stored.'); + + $this->drupalPost($view_edit_url, array(), t('Save')); + $this->assertLink(t('Test row plugin'), 0, 'Make sure the test row plugin is shown in the UI'); + + $view = views_get_view($view->storage->name); + $view->initDisplay(); + $row = $view->display_handler->getOption('row'); + $this->assertEqual($row['type'], 'test_row', 'Make sure that the test_row got saved as used row plugin.'); + $this->assertEqual($row['options']['test_option'], $random_name, 'Make sure that the custom settings field got saved as expected.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/SettingsTest.php b/core/modules/views/lib/Drupal/views/Tests/UI/SettingsTest.php new file mode 100644 index 0000000..faa92e1 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/SettingsTest.php @@ -0,0 +1,96 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UI\SettingsTest. + */ + +namespace Drupal\views\Tests\UI; + +/** + * Tests the various settings in the views UI. + */ +class SettingsTest extends UITestBase { + + /** + * Stores an admin user used by the different tests. + * + * @var Drupal\user\User + */ + protected $adminUser; + + public static function getInfo() { + return array( + 'name' => 'Settings functionality', + 'description' => 'Tests all ui related settings under admin/structure/views/settings.', + 'group' => 'Views UI', + ); + } + + /** + * Tests the settings for the edit ui. + */ + function testEditUI() { + $this->drupalLogin($this->adminUser); + + // Test the settings tab exists. + $this->drupalGet('admin/structure/views'); + $this->assertLinkByHref('admin/structure/views/settings'); + + // Configure to always show the master display. + $edit = array( + 'ui_show_master_display' => TRUE, + ); + $this->drupalPost('admin/structure/views/settings', $edit, t('Save configuration')); + + $view = array(); + $view['human_name'] = $this->randomName(16); + $view['name'] = strtolower($this->randomName(16)); + $view['description'] = $this->randomName(16); + $view['page[create]'] = TRUE; + $view['page[title]'] = $this->randomName(16); + $view['page[path]'] = $this->randomName(16); + $this->drupalPost('admin/structure/views/add', $view, t('Continue & edit')); + + $this->assertLink(t('Master') . '*'); + + // Configure to not always show the master display. + // If you have a view without a page or block the master display should be + // still shown. + $edit = array( + 'ui_show_master_display' => FALSE, + ); + $this->drupalPost('admin/structure/views/settings', $edit, t('Save configuration')); + + $view['page[create]'] = FALSE; + $this->drupalPost('admin/structure/views/add', $view, t('Continue & edit')); + + $this->assertLink(t('Master') . '*'); + + // Create a view with an additional display, so master should be hidden. + $view['page[create]'] = TRUE; + $this->drupalPost('admin/structure/views/add', $view, t('Continue & edit')); + + $this->assertNoLink(t('Master')); + + // Configure to always show the advanced settings. + // @todo It doesn't seem to be a way to test this as this works just on js. + + // Configure to show the embedable display. + $edit = array( + 'ui_show_display_embed' => TRUE, + ); + $this->drupalPost('admin/structure/views/settings', $edit, t('Save configuration')); + $this->drupalPost('admin/structure/views/add', $view, t('Continue & edit')); + $this->assertFieldById('edit-displays-top-add-display-embed'); + + $edit = array( + 'ui_show_display_embed' => FALSE, + ); + $this->drupalPost('admin/structure/views/settings', $edit, t('Save configuration')); + views_invalidate_cache(); + $this->drupalPost('admin/structure/views/add', $view, t('Continue & edit')); + $this->assertNoFieldById('edit-displays-top-add-display-embed'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/StyleUITest.php b/core/modules/views/lib/Drupal/views/Tests/UI/StyleUITest.php new file mode 100644 index 0000000..5255af9 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/StyleUITest.php @@ -0,0 +1,61 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UI\StyleUITest. + */ + +namespace Drupal\views\Tests\UI; + +/** + * Tests the UI of style plugins. + * + * @see Drupal\views_test_data\Plugin\views\style\StyleTest. + */ +class StyleUITest extends UITestBase { + + public static function getInfo() { + return array( + 'name' => 'Style: UI', + 'description' => 'Tests the UI of style plugins.', + 'group' => 'Views UI', + ); + } + + /** + * Tests changing the style plugin and changing some options of a style. + */ + public function testStyleUI() { + $view = $this->getView(); + $view_edit_url = "admin/structure/views/view/{$view->storage->name}/edit"; + + $style_plugin_url = "admin/structure/views/nojs/display/{$view->storage->name}/default/style"; + $style_options_url = "admin/structure/views/nojs/display/{$view->storage->name}/default/style_options"; + + $this->drupalGet($style_plugin_url); + $this->assertFieldByName('style', 'default', 'The default style plugin selected in the UI should be unformatted list.'); + + $edit = array( + 'style' => 'test_style' + ); + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertFieldByName('style_options[test_option]', NULL, 'Make sure the custom settings form from the test plugin appears.'); + $random_name = $this->randomName(); + $edit = array( + 'style_options[test_option]' => $random_name + ); + $this->drupalPost(NULL, $edit, t('Apply')); + $this->drupalGet($style_options_url); + $this->assertFieldByName('style_options[test_option]', $random_name, 'Make sure the custom settings form field has the expected value stored.'); + + $this->drupalPost($view_edit_url, array(), t('Save')); + $this->assertLink(t('Test style plugin'), 0, 'Make sure the test style plugin is shown in the UI'); + + $view = views_get_view($view->storage->name); + $view->initDisplay(); + $style = $view->display_handler->getOption('style'); + $this->assertEqual($style['type'], 'test_style', 'Make sure that the test_style got saved as used style plugin.'); + $this->assertEqual($style['options']['test_option'], $random_name, 'Make sure that the custom settings field got saved as expected.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UI/UITestBase.php b/core/modules/views/lib/Drupal/views/Tests/UI/UITestBase.php new file mode 100644 index 0000000..eaf1d7d --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UI/UITestBase.php @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UI\UITestBase. + */ + +namespace Drupal\views\Tests\UI; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Provides a base class for testing the Views UI. + */ +abstract class UITestBase extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui', 'block'); + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + $this->adminUser = $this->drupalCreateUser(array('administer views')); + + $views_admin = $this->drupalCreateUser(array('administer views', 'administer blocks', 'bypass node access', 'access user profiles', 'view revisions')); + $this->drupalLogin($views_admin); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/UpgradeTestCase.php b/core/modules/views/lib/Drupal/views/Tests/UpgradeTestCase.php new file mode 100644 index 0000000..2ee7fd2 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/UpgradeTestCase.php @@ -0,0 +1,103 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\UpgradeTestCase. + */ + +namespace Drupal\views\Tests; + +/** + * Tests the upgrade path of all conversions. + * + * You can find all conversions by searching for "moved to". + */ +class UpgradeTestCase extends ViewTestBase { + + /** + * Modules to enable. + * + * To import a view the user needs use PHP for settings rights, so enable php + * module. + * + * @var array + */ + public static $modules = array('views_ui', 'block', 'php'); + + public static function getInfo() { + return array( + 'name' => 'Upgrade path', + 'description' => 'Tests the upgrade path of modules which were changed.', + 'group' => 'Views', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test_data']['old_field_1']['moved to'] = array('views_test_data', 'id'); + $data['views_test_data']['old_field_2']['field']['moved to'] = array('views_test_data', 'name'); + $data['views_test_data']['old_field_3']['filter']['moved to'] = array('views_test_data', 'age'); + + // @todo Test this scenario, too. + $data['views_old_table_2']['old_field']['moved to'] = array('views_test_data', 'job'); + + $data['views_old_table']['moved to'] = 'views_test_data'; + + return $data; + } + + function debugField($field) { + $keys = array('id', 'table', 'field', 'actualField', 'original_field', 'realField'); + $info = array(); + foreach ($keys as $key) { + $info[$key] = $field->{$key}; + } + debug($info, NULL, TRUE); + } + + /** + * Tests the moved to parameter in general. + */ + public function testMovedTo() { + // Test moving on field lavel. + $view = $this->createViewFromConfig('test_views_move_to_field'); + $view->storage->update(); + $view->build(); + +// $this->assertEqual('old_field_1', $view->field['old_field_1']->options['id'], "Id shouldn't change during conversion"); +// $this->assertEqual('id', $view->field['old_field_1']->field, 'The field should change during conversion'); + $this->assertEqual('id', $view->field['old_field_1']->realField); + $this->assertEqual('views_test_data', $view->field['old_field_1']->table); + $this->assertEqual('old_field_1', $view->field['old_field_1']->original_field, 'The field should have stored the original_field'); + + // Test moving on handler lavel. + $view = $this->createViewFromConfig('test_views_move_to_handler'); + $view->storage->update(); + $view->build(); + +// $this->assertEqual('old_field_2', $view->field['old_field_2']->options['id']); + $this->assertEqual('name', $view->field['old_field_2']->realField); + $this->assertEqual('views_test_data', $view->field['old_field_2']->table); + +// $this->assertEqual('old_field_3', $view->filter['old_field_3']->options['id']); + $this->assertEqual('age', $view->filter['old_field_3']->realField); + $this->assertEqual('views_test_data', $view->filter['old_field_3']->table); + + // Test moving on table level. + $view = $this->createViewFromConfig('test_views_move_to_table'); + $view->storage->update(); + $view->build(); + + $this->assertEqual('views_test_data', $view->storage->base_table, 'Make sure that view->base_table gets automatically converted.'); +// $this->assertEqual('id', $view->field['id']->field, 'If we move a whole table fields of this table should work, too.'); + $this->assertEqual('id', $view->field['id']->realField, 'To run the query right the realField has to be set right.'); + $this->assertEqual('views_test_data', $view->field['id']->table); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/User/ArgumentDefaultTest.php b/core/modules/views/lib/Drupal/views/Tests/User/ArgumentDefaultTest.php new file mode 100644 index 0000000..bdaf382 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/User/ArgumentDefaultTest.php @@ -0,0 +1,50 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\User\ArgumentDefaultTest. + */ + +namespace Drupal\views\Tests\User; + +/** + * Tests views user argument default plugin. + */ +class ArgumentDefaultTest extends UserTestBase { + + public static function getInfo() { + return array( + 'name' => 'User: Argument default', + 'description' => 'Tests user argument default plugin.', + 'group' => 'Views Modules', + ); + } + + public function test_plugin_argument_default_current_user() { + // Create a user to test. + $account = $this->drupalCreateUser(); + + // Switch the user, we have to check the global user too, because drupalLogin is only for the simpletest browser. + $this->drupalLogin($account); + global $user; + $admin = $user; + drupal_save_session(FALSE); + $user = $account; + + $this->view->preExecute(); + $this->view->initHandlers(); + + $this->assertEqual($this->view->argument['null']->get_default_argument(), $account->uid, 'Uid of the current user is used.'); + // Switch back. + $user = $admin; + drupal_save_session(TRUE); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_plugin_argument_default_current_user'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/User/ArgumentValidateTest.php b/core/modules/views/lib/Drupal/views/Tests/User/ArgumentValidateTest.php new file mode 100644 index 0000000..c699261 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/User/ArgumentValidateTest.php @@ -0,0 +1,87 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\User\ArgumentValidateTest. + */ + +namespace Drupal\views\Tests\User; + +/** + * Tests views user argument argument handler. + */ +class ArgumentValidateTest extends UserTestBase { + + public static function getInfo() { + return array( + 'name' => 'User: Argument validator', + 'description' => 'Tests user argument validator.', + 'group' => 'Views Modules', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->account = $this->drupalCreateUser(); + } + + function testArgumentValidateUserUid() { + $account = $this->account; + // test 'uid' case + $view = $this->view_argument_validate_user('uid'); + $this->assertTrue($view->argument['null']->validateArgument($account->uid)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a string variable since type is 'uid' + $this->assertFalse($view->argument['null']->validateArgument($account->name)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a valid numeric, but for a user that doesn't exist + $this->assertFalse($view->argument['null']->validateArgument(32)); + } + + function testArgumentValidateUserName() { + $account = $this->account; + // test 'name' case + $view = $this->view_argument_validate_user('name'); + $this->assertTrue($view->argument['null']->validateArgument($account->name)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a uid variable since type is 'name' + $this->assertFalse($view->argument['null']->validateArgument($account->uid)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a valid string, but for a user that doesn't exist + $this->assertFalse($view->argument['null']->validateArgument($this->randomName())); + } + + function testArgumentValidateUserEither() { + $account = $this->account; + // test 'either' case + $view = $this->view_argument_validate_user('either'); + $this->assertTrue($view->argument['null']->validateArgument($account->name)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a uid variable since type is 'name' + $this->assertTrue($view->argument['null']->validateArgument($account->uid)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a valid string, but for a user that doesn't exist + $this->assertFalse($view->argument['null']->validateArgument($this->randomName())); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a valid uid, but for a user that doesn't exist + $this->assertFalse($view->argument['null']->validateArgument(32)); + } + + function view_argument_validate_user($argtype) { + $view = $this->createViewFromConfig('test_view_argument_validate_user'); + $view->displayHandlers['default']->options['arguments']['null']['validate_options']['type'] = $argtype; + $view->preExecute(); + $view->initHandlers(); + + return $view; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/User/HandlerFieldUserNameTest.php b/core/modules/views/lib/Drupal/views/Tests/User/HandlerFieldUserNameTest.php new file mode 100644 index 0000000..7a28fef --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/User/HandlerFieldUserNameTest.php @@ -0,0 +1,63 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\User\HandlerFieldUserNameTest. + */ + +namespace Drupal\views\Tests\User; + +/** + * Tests the field username handler. + * + * @see views_handler_field_user_name + */ +class HandlerFieldUserNameTest extends UserTestBase { + + public static function getInfo() { + return array( + 'name' => 'User: Name Field', + 'description' => 'Tests the handler of the user: name field.', + 'group' => 'Views Modules', + ); + } + + function testUserName() { + $view = $this->getView(); + $this->executeView($view); + + $view->row_index = 0; + + $view->field['name']->options['link_to_user'] = TRUE; + $username = $view->result[0]->users_name = $this->randomName(); + $view->result[0]->uid = 1; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertTrue(strpos($render, $username) !== FALSE, 'If link to user is checked the username should be part of the output.'); + $this->assertTrue(strpos($render, 'user/1') !== FALSE, 'If link to user is checked the link to the user should appear as well.'); + + $view->field['name']->options['link_to_user'] = FALSE; + $username = $view->result[0]->users_name = $this->randomName(); + $view->result[0]->uid = 1; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $username, 'If the user is not linked the username should be printed out for a normal user.'); + + $view->result[0]->uid = 0; + $anon_name = config('user.settings')->get('anonymous'); + $view->result[0]->users_name = ''; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $anon_name , 'For user0 it should use the default anonymous name by default.'); + + $view->field['name']->options['overwrite_anonymous'] = TRUE; + $anon_name = $view->field['name']->options['anonymous_text'] = $this->randomName(); + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $anon_name , 'For user0 it should use the configured anonymous text if overwrite_anonymous is checked.'); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_views_handler_field_user_name'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/User/UserTestBase.php b/core/modules/views/lib/Drupal/views/Tests/User/UserTestBase.php new file mode 100644 index 0000000..28eeb01 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/User/UserTestBase.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\User\UserTestBase. + */ + +namespace Drupal\views\Tests\User; + +use Drupal\views\Tests\ViewTestBase; + +/** + * @todo. + */ +abstract class UserTestBase extends ViewTestBase { + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewElementTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewElementTest.php new file mode 100644 index 0000000..3ea578e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/ViewElementTest.php @@ -0,0 +1,117 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\ViewElementTest. + */ + +namespace Drupal\views\Tests; + +/** + * Tests the 'view' element type. + */ +class ViewElementTest extends ViewTestBase { + + /** + * The raw render data array to use in tests. + * + * @var array + */ + protected $render; + + public static function getInfo() { + return array( + 'name' => 'View element', + 'description' => 'Tests the view render element.', + 'group' => 'Views' + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + + // Set up a render array to use. We need to copy this as drupal_render + // passes by reference. + $this->render = array( + 'view' => array( + '#type' => 'view', + '#name' => 'test_view', + '#display_id' => 'default', + '#arguments' => array(25), + ), + ); + } + + /** + * Tests the rendered output and form output of a view element. + */ + public function testViewElement() { + $view = $this->getBasicView(); + + // Set the content as our rendered array. + $render = $this->render; + $this->drupalSetContent(drupal_render($render)); + + $xpath = $this->xpath('//div[@class="views-element-container"]'); + $this->assertTrue($xpath, 'The view container has been found in the rendered output.'); + + $xpath = $this->xpath('//div[@class="view-content"]'); + $this->assertTrue($xpath, 'The view content has been found in the rendered output.'); + // There should be 5 rows in the results. + $xpath = $this->xpath('//div[@class="view-content"]/div'); + $this->assertEqual(count($xpath), 5); + + // Test a form. + $this->drupalGet('views_test_data_element_form'); + + $xpath = $this->xpath('//div[@class="views-element-container form-wrapper"]'); + $this->assertTrue($xpath, 'The view container has been found on the form.'); + + $xpath = $this->xpath('//div[@class="view-content"]'); + $this->assertTrue($xpath, 'The view content has been found on the form.'); + // There should be 5 rows in the results. + $xpath = $this->xpath('//div[@class="view-content"]/div'); + $this->assertEqual(count($xpath), 5); + + // Add an argument and save the view. + $view->displayHandlers['default']->overrideOption('arguments', array( + 'age' => array( + 'default_action' => 'ignore', + 'style_plugin' => 'default_summary', + 'style_options' => array(), + 'wildcard' => 'all', + 'wildcard_substitution' => 'All', + 'title' => '', + 'breadcrumb' => '', + 'default_argument_type' => 'fixed', + 'default_argument' => '', + 'validate' => array( + 'type' => 'none', + 'fail' => 'not found', + ), + 'break_phrase' => 0, + 'not' => 0, + 'id' => 'age', + 'table' => 'views_test_data', + 'field' => 'age', + 'validate_user_argument_type' => 'uid', + ) + )); + $view->save(); + + // Test the render array again. + $render = $this->render; + $this->drupalSetContent(drupal_render($render)); + // There should be 1 row in the results, 'John' arg 25. + $xpath = $this->xpath('//div[@class="view-content"]/div'); + $this->assertEqual(count($xpath), 1); + + // Test that the form has the same expected result. + $this->drupalGet('views_test_data_element_form'); + $xpath = $this->xpath('//div[@class="view-content"]/div'); + $this->assertEqual(count($xpath), 1); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewExecutableTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewExecutableTest.php new file mode 100644 index 0000000..9fb3fbc --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/ViewExecutableTest.php @@ -0,0 +1,231 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\ViewExecutableTest. + */ + +namespace Drupal\views\Tests; + +use Drupal\views\ViewExecutable; +use Drupal\views\Plugin\views\display\DefaultDisplay; +use Drupal\views\Plugin\views\display\Page; + +/** + * Tests the ViewExecutable class. + * + * @see Drupal\views\ViewExecutable + */ +class ViewExecutableTest extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('comment'); + + /** + * Properties that should be stored in the configuration. + * + * @var array + */ + protected $configProperties = array( + 'disabled', + 'api_version', + 'name', + 'description', + 'tag', + 'base_table', + 'human_name', + 'core', + 'display', + ); + + /** + * Properties that should be stored in the executable. + * + * @var array + */ + protected $executableProperties = array( + 'build_info' + ); + + public static function getInfo() { + return array( + 'name' => 'View executable tests', + 'description' => 'Tests the ViewExecutable class.', + 'group' => 'Views' + ); + } + + /** + * Tests the initDisplay() and initHandlers() methods. + */ + public function testInitMethods() { + $view = $this->getBasicView(); + $view->initDisplay(); + + $this->assertTrue($view->display_handler instanceof DefaultDisplay, 'Make sure a reference to the current display handler is set.'); + $this->assertTrue($view->displayHandlers['default'] instanceof DefaultDisplay, 'Make sure a display handler is created for each display.'); + + $view = $this->getBasicView(); + $view->destroy(); + $view->initHandlers(); + + // Check for all handler types. + $handler_types = array_keys(ViewExecutable::viewsHandlerTypes()); + foreach ($handler_types as $type) { + // The views_test integration doesn't have relationships. + if ($type == 'relationship') { + continue; + } + $this->assertTrue(count($view->$type), format_string('Make sure a %type instance got instantiated.', array('%type' => $type))); + } + + // initHandlers() should create display handlers automatically as well. + $this->assertTrue($view->display_handler instanceof DefaultDisplay, 'Make sure a reference to the current display handler is set.'); + $this->assertTrue($view->displayHandlers['default'] instanceof DefaultDisplay, 'Make sure a display handler is created for each display.'); + } + + /** + * Overrides Drupal\views\Tests\ViewTestBase::getBasicView(). + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_destroy'); + } + + /** + * Tests the generation of the executable object. + */ + public function testConstructing() { + $view = $this->getView(); + } + + /** + * Tests the accessing of values on the object. + */ + public function testProperties() { + $view = $this->getView(); + foreach ($this->executableProperties as $property) { + $this->assertTrue(isset($view->{$property})); + } + } + + /** + * Tests the display related methods and properties. + */ + public function testDisplays() { + $view = views_get_view('test_executable_displays'); + + // Tests Drupal\views\ViewExecutable::initDisplay(). + $view->initDisplay(); + $count = count($view->displayHandlers); + $this->assertEqual($count, 3, format_string('Make sure all display handlers got instantiated (@count of @count_expected)', array('@count' => $count, '@count_expected' => 3))); + // Tests the classes of the instances. + $this->assertTrue($view->displayHandlers['default'] instanceof DefaultDisplay); + $this->assertTrue($view->displayHandlers['page'] instanceof Page); + $this->assertTrue($view->displayHandlers['page_2'] instanceof Page); + + // After initializing the default display is the current used display. + $this->assertEqual($view->current_display, 'default'); + $this->assertEqual(spl_object_hash($view->display_handler), spl_object_hash($view->displayHandlers['default'])); + + // All handlers should have a reference to the default display. + $this->assertEqual(spl_object_hash($view->displayHandlers['page']->default_display), spl_object_hash($view->displayHandlers['default'])); + $this->assertEqual(spl_object_hash($view->displayHandlers['page_2']->default_display), spl_object_hash($view->displayHandlers['default'])); + + // Tests Drupal\views\ViewExecutable::setDisplay(). + $view->setDisplay(); + $this->assertEqual($view->current_display, 'default', 'If setDisplay is called with no parameter the default display should be used.'); + $this->assertEqual(spl_object_hash($view->display_handler), spl_object_hash($view->displayHandlers['default'])); + + // Set two different valid displays. + $view->setDisplay('page'); + $this->assertEqual($view->current_display, 'page', 'If setDisplay is called with a valid display id the appropriate display should be used.'); + $this->assertEqual(spl_object_hash($view->display_handler), spl_object_hash($view->displayHandlers['page'])); + + $view->setDisplay('page_2'); + $this->assertEqual($view->current_display, 'page_2', 'If setDisplay is called with a valid display id the appropriate display should be used.'); + $this->assertEqual(spl_object_hash($view->display_handler), spl_object_hash($view->displayHandlers['page_2'])); + + + } + + /** + * Tests the deconstructor to be sure that every kind of heavy objects are removed. + */ + function testDestroy() { + $view = $this->getView(); + + $view->preview(); + $view->destroy(); + + $this->assertViewDestroy($view); + + // Manipulate the display variable to test a previous bug. + $view = $this->getView(); + $view->preview(); + + $view->destroy(); + $this->assertViewDestroy($view); + } + + function assertViewDestroy($view) { + $this->assertFalse(isset($view->displayHandlers['default']), 'Make sure all displays are destroyed.'); + $this->assertFalse(isset($view->displayHandlers['attachment_1']), 'Make sure all displays are destroyed.'); + + $this->assertFalse(isset($view->filter), 'Make sure all filter handlers are destroyed'); + $this->assertFalse(isset($view->field), 'Make sure all field handlers are destroyed'); + $this->assertFalse(isset($view->argument), 'Make sure all argument handlers are destroyed'); + $this->assertFalse(isset($view->relationship), 'Make sure all relationship handlers are destroyed'); + $this->assertFalse(isset($view->sort), 'Make sure all sort handlers are destroyed'); + $this->assertFalse(isset($view->area), 'Make sure all area handlers are destroyed'); + + $keys = array('current_display', 'display_handler', 'field', 'argument', 'filter', 'sort', 'relationship', 'header', 'footer', 'empty', 'query', 'result', 'inited', 'style_plugin', 'plugin_name', 'exposed_data', 'exposed_input', 'many_to_one_tables'); + foreach ($keys as $key) { + $this->assertFalse(isset($view->{$key}), $key); + } + $this->assertEqual($view->built, FALSE); + $this->assertEqual($view->executed, FALSE); + $this->assertEqual($view->build_info, array()); + $this->assertEqual($view->attachment_before, ''); + $this->assertEqual($view->attachment_after, ''); + } + + /** + * Tests ViewExecutable::viewsHandlerTypes(). + */ + public function testViewsHandlerTypes() { + $types = ViewExecutable::viewsHandlerTypes(); + foreach (array('field', 'filter', 'argument', 'sort', 'header', 'footer', 'empty') as $type) { + $this->assertTrue(isset($types[$type])); + // @todo The key on the display should be footers, headers and empties + // or something similar instead of the singular, but so long check for + // this special case. + if (isset($types[$type]['type']) && $types[$type]['type'] == 'area') { + $this->assertEqual($types[$type]['plural'], $type); + } + else { + $this->assertEqual($types[$type]['plural'], $type . 's'); + } + } + } + + function testValidate() { + // Test a view with multiple displays. + // Validating a view shouldn't change the active display. + // @todo Create an extra validation view. + $this->view->setDisplay('page_1'); + + $this->view->validate(); + + $this->assertEqual('page_1', $this->view->current_display, "The display should be constant while validating"); + + // @todo Write real tests for the validation. + // In general the following things could be tested: + // - Deleted displays shouldn't be validated + // - Multiple displays are validating and the errors are merged together. + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewStorageTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewStorageTest.php new file mode 100644 index 0000000..30cdd4c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/ViewStorageTest.php @@ -0,0 +1,473 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\ViewStorageTest. + */ + +namespace Drupal\views\Tests; + +use Drupal\views\ViewStorageController; +use Drupal\views\ViewStorage; +use Drupal\views\Plugin\views\display\Page; +use Drupal\views\Plugin\views\display\DefaultDisplay; +use Drupal\views\Plugin\views\display\Feed; + +/** + * Tests the functionality of ViewStorage and ViewStorageController. + * + * @see Drupal\views\ViewStorage + * @see Drupal\views\ViewStorageController + */ +class ViewStorageTest extends ViewTestBase { + + /** + * Properties that should be stored in the configuration. + * + * @var array + */ + protected $config_properties = array( + 'disabled', + 'api_version', + 'module', + 'name', + 'description', + 'tag', + 'base_table', + 'human_name', + 'core', + 'display', + ); + + /** + * The configuration entity information from entity_get_info(). + * + * @var array + */ + protected $info; + + /** + * The configuration entity storage controller. + * + * @var Drupal\views\ViewStorageController + */ + protected $controller; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('node', 'search', 'comment', 'taxonomy'); + + public static function getInfo() { + return array( + 'name' => 'Configuration entity CRUD tests', + 'description' => 'Tests the CRUD functionality for ViewStorage.', + 'group' => 'Views', + ); + } + + /** + * Tests CRUD operations. + */ + function testConfigurationEntityCRUD() { + // Get the configuration entity information and controller. + $this->info = entity_get_info('view'); + $this->controller = entity_get_controller('view'); + + // Confirm that an info array has been returned. + $this->assertTrue(!empty($this->info) && is_array($this->info), 'The View info array is loaded.'); + + // Confirm we have the correct controller class. + $this->assertTrue($this->controller instanceof ViewStorageController, 'The correct controller is loaded.'); + + // CRUD tests. + $this->loadTests(); + $this->createTests(); + $this->saveTests(); + $this->deleteTests(); + $this->displayTests(); + $this->statusTests(); + + // Helper method tests + $this->displayMethodTests(); + } + + /** + * Tests loading configuration entities. + */ + protected function loadTests() { + $view = $this->loadView('archive'); + $data = config('views.view.archive')->get(); + + // Confirm that an actual view object is loaded and that it returns all of + // expected properties. + $this->assertTrue($view instanceof ViewStorage, 'Single View instance loaded.'); + foreach ($this->config_properties as $property) { + $this->assertTrue(isset($view->{$property}), format_string('Property: @property loaded onto View.', array('@property' => $property))); + } + + // Check the displays have been loaded correctly from config display data. + $expected_displays = array('default', 'page', 'block'); + $this->assertEqual(array_keys($view->display), $expected_displays, 'The correct display names are present.'); + + // Check each ViewDisplay object and confirm that it has the correct key and + // property values. + foreach ($view->display as $key => $display) { + $this->assertEqual($key, $display['id'], 'The display has the correct ID assigned.'); + + // Get original display data and confirm that the display options array + // exists. + $original_options = $data['display'][$key]; + foreach ($original_options as $orig_key => $value) { + $this->assertIdentical($display[$orig_key], $value, format_string('@key is identical to saved data', array('@key' => $key))); + } + } + + // Fetch data for all configuration entities and default view configurations. + $all_configuration_entities = $this->controller->load(); + $all_config = config_get_storage_names_with_prefix('views.view'); + + // Remove the 'views.view.' prefix from config names for comparision with + // loaded configuration entities. + $prefix_map = function ($value) { + $parts = explode('.', $value); + return end($parts); + }; + + // Check that the correct number of configuration entities have been loaded. + $count = count($all_configuration_entities); + $this->assertEqual($count, count($all_config), format_string('The array of all @count configuration entities is loaded.', array('@count' => $count))); + + // Check that all of these machine names match. + $this->assertIdentical(array_keys($all_configuration_entities), array_map($prefix_map, $all_config), 'All loaded elements match.'); + + // Make sure that loaded default views get a UUID. + $view = views_get_view('frontpage'); + $this->assertTrue($view->storage->uuid()); + } + + /** + * Tests creating configuration entities. + */ + protected function createTests() { + // Create a new View instance with empty values. + $created = $this->controller->create(array()); + + $this->assertTrue($created instanceof ViewStorage, 'Created object is a View.'); + // Check that the View contains all of the properties. + foreach ($this->config_properties as $property) { + $this->assertTrue(property_exists($created, $property), format_string('Property: @property created on View.', array('@property' => $property))); + } + + // Create a new View instance with config values. + $values = config('views.view.glossary')->get(); + $created = $this->controller->create($values); + + $this->assertTrue($created instanceof ViewStorage, 'Created object is a View.'); + // Check that the View contains all of the properties. + $properties = $this->config_properties; + // Remove display from list. + array_pop($properties); + + // Test all properties except displays. + foreach ($properties as $property) { + $this->assertTrue(isset($created->{$property}), format_string('Property: @property created on View.', array('@property' => $property))); + $this->assertIdentical($values[$property], $created->{$property}, format_string('Property value: @property matches configuration value.', array('@property' => $property))); + } + + // Check the UUID of the loaded View. + $created->set('name', 'glossary_new'); + $created->save(); + $created_loaded = $this->loadView('glossary_new'); + $this->assertIdentical($created->uuid(), $created_loaded->uuid(), 'The created UUID has been saved correctly.'); + } + + /** + * Tests saving configuration entities. + */ + protected function saveTests() { + $view = $this->loadView('archive'); + + // Save the newly created view, but modify the name. + $view->set('name', 'archive_copy'); + $view->set('tag', 'changed'); + $view->save(); + + // Load the newly saved config. + $config = config('views.view.archive_copy'); + $this->assertFalse($config->isNew(), 'New config has been loaded.'); + + $this->assertEqual($view->tag, $config->get('tag'), 'A changed value has been saved.'); + + // Change a value and save. + $view->tag = 'changed'; + $view->save(); + + // Check values have been written to config. + $config = config('views.view.archive_copy')->get(); + $this->assertEqual($view->tag, $config['tag'], 'View property saved to config.'); + + // Check whether load, save and load produce the same kind of view. + $values = config('views.view.archive')->get(); + $created = $this->controller->create($values); + + $created->save(); + $created_loaded = $this->loadView($created->id()); + $values_loaded = config('views.view.archive')->get(); + + $this->assertTrue(isset($created_loaded->display['default']['display_options']), 'Make sure that the display options exist.'); + $this->assertEqual($created_loaded->display['default']['display_plugin'], 'default', 'Make sure the right display plugin is set.'); + + $this->assertEqual($values, $values_loaded, 'The loaded config is the same as the original loaded one.'); + + } + + /** + * Tests deleting configuration entities. + */ + protected function deleteTests() { + $view = $this->loadView('tracker'); + + // Delete the config. + $view->delete(); + $config = config('views.view.tracker'); + + $this->assertTrue($config->isNew(), 'Deleted config is now new.'); + } + + /** + * Tests adding, saving, and loading displays on configuration entities. + */ + protected function displayTests() { + // Check whether a display can be added and saved to a View. + $view = $this->loadView('frontpage'); + + $view->newDisplay('page', 'Test', 'test'); + + $new_display = $view->display['test']; + + // Ensure the right display_plugin is created/instantiated. + $this->assertEqual($new_display['display_plugin'], 'page', 'New page display "test" uses the right display plugin.'); + $this->assertTrue($view->getExecutable()->displayHandlers[$new_display['id']] instanceof Page, 'New page display "test" uses the right display plugin.'); + + + $view->set('name', 'frontpage_new'); + $view->save(); + $values = config('views.view.frontpage_new')->get(); + + $this->assertTrue(isset($values['display']['test']) && is_array($values['display']['test']), 'New display was saved.'); + } + + /** + * Tests statuses of configuration entities. + */ + protected function statusTests() { + // Test a View can be enabled and disabled again (with a new view). + $view = $this->loadView('backlinks'); + + // The view should already be disabled. + $view->enable(); + $this->assertTrue($view->isEnabled(), 'A view has been enabled.'); + + // Check the saved values. + $view->save(); + $config = config('views.view.backlinks')->get(); + $this->assertFalse($config['disabled'], 'The changed disabled property was saved.'); + + // Disable the view. + $view->disable(); + $this->assertFalse($view->isEnabled(), 'A view has been disabled.'); + + // Check the saved values. + $view->save(); + $config = config('views.view.backlinks')->get(); + $this->assertTrue($config['disabled'], 'The changed disabled property was saved.'); + } + + /** + * Loads a single configuration entity from the controller. + * + * @param string $view_name + * The machine name of the view. + * + * @return object Drupal\views\ViewExecutable. + * The loaded view object. + */ + protected function loadView($view_name) { + $load = $this->controller->load(array($view_name)); + return reset($load); + } + + /** + * Tests the display related functions like getDisplaysList(). + */ + protected function displayMethodTests() { + $config['display'] = array( + 'page' => array( + 'display_options' => array('path' => 'test'), + 'display_plugin' => 'page', + 'id' => 'page_2', + 'display_title' => 'Page 2', + 'position' => 1 + ), + 'feed' => array( + 'display_options' => array('path' => 'test.xml'), + 'display_plugin' => 'feed', + 'id' => 'feed', + 'display_title' => 'Feed', + 'position' => 2 + ), + 'page_2' => array( + 'display_options' => array('path' => 'test/%/extra'), + 'display_plugin' => 'page', + 'id' => 'page_2', + 'display_title' => 'Page 2', + 'position' => 3 + ) + ); + $view = $this->controller->create($config); + + $this->assertEqual($view->getDisplaysList(), array('Feed', 'Page'), 'Make sure the display admin names are returns in alphabetic order.'); + + // Paths with a "%" shouldn't not be linked + $expected_paths = array(); + $expected_paths[] = l('/test', 'test'); + $expected_paths[] = l('/test.xml', 'test.xml'); + $expected_paths[] = '/test/%/extra'; + + $this->assertEqual($view->getPaths(), $expected_paths, 'Make sure the paths in the ui are generated as expected.'); + + // Tests Drupal\views\ViewStorage::addDisplay() + $view = $this->controller->create(array()); + $random_title = $this->randomName(); + + $id = $view->addDisplay('page', $random_title); + $this->assertEqual($id, 'page_1', format_string('Make sure the first display (%id_new) has the expected ID (%id)', array('%id_new' => $id, '%id' => 'page_1'))); + $this->assertEqual($view->display[$id]['display_title'], $random_title); + + $random_title = $this->randomName(); + $id = $view->addDisplay('page', $random_title); + $this->assertEqual($id, 'page_2', format_string('Make sure the second display (%id_new) has the expected ID (%id)', array('%id_new' => $id, '%id' => 'page_2'))); + $this->assertEqual($view->display[$id]['display_title'], $random_title); + + $id = $view->addDisplay('page'); + $this->assertEqual($view->display[$id]['display_title'], 'Page 3'); + + // Tests Drupal\views\ViewStorage::generateDisplayId(). + // @todo Sadly this method is not public so it cannot be tested. + // $view = $this->controller->create(array()); + // $this->assertEqual($view->generateDisplayId('default'), 'default', 'The plugin ID for default is always default.'); + // $this->assertEqual($view->generateDisplayId('feed'), 'feed_1', 'The generated ID for the first instance of a plugin type should have an suffix of _1.'); + // $view->addDisplay('feed', 'feed title'); + // $this->assertEqual($view->generateDisplayId('feed'), 'feed_2', 'The generated ID for the first instance of a plugin type should have an suffix of _2.'); + + // Tests Drupal\views\ViewStorage::newDisplay(). + $view = $this->controller->create(array()); + $view->newDisplay('default'); + + $display = $view->newDisplay('page'); + $this->assertTrue($display instanceof Page); + $this->assertTrue($view->getExecutable()->displayHandlers['page_1'] instanceof Page); + $this->assertTrue($view->getExecutable()->displayHandlers['page_1']->default_display instanceof DefaultDisplay); + + $display = $view->newDisplay('page'); + $this->assertTrue($display instanceof Page); + $this->assertTrue($view->getExecutable()->displayHandlers['page_2'] instanceof Page); + $this->assertTrue($view->getExecutable()->displayHandlers['page_2']->default_display instanceof DefaultDisplay); + + $display = $view->newDisplay('feed'); + $this->assertTrue($display instanceof Feed); + $this->assertTrue($view->getExecutable()->displayHandlers['feed_1'] instanceof Feed); + $this->assertTrue($view->getExecutable()->displayHandlers['feed_1']->default_display instanceof DefaultDisplay); + + // Tests item related methods(). + $view = $this->controller->create(array('base_table' => 'views_test_data')); + $view->addDisplay('default'); + $view = $view->getExecutable(); + + $display_id = 'default'; + $expected_items = array(); + // Tests addItem with getItem. + // Therefore add one item without any optioins and one item with some + // options. + $id1 = $view->addItem($display_id, 'field', 'views_test_data', 'id'); + $item1 = $view->getItem($display_id, 'field', 'id'); + $expected_items[$id1] = $expected_item = array( + 'id' => 'id', + 'table' => 'views_test_data', + 'field' => 'id' + ); + $this->assertEqual($item1, $expected_item); + + $options = array( + 'alter' => array( + 'text' => $this->randomName() + ) + ); + $id2 = $view->addItem($display_id, 'field', 'views_test_data', 'name', $options); + $item2 = $view->getItem($display_id, 'field', 'name'); + $expected_items[$id2] = $expected_item = array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name' + ) + $options; + $this->assertEqual($item2, $expected_item); + + // Tests the expected fields from the previous additions. + $this->assertEqual($view->getItems('field', $display_id), $expected_items); + + // Alter an existing item via setItem and check the result via getItem + // and getItems. + $item = array( + 'alter' => array( + 'text' => $this->randomName(), + ) + ) + $item1; + $expected_items[$id1] = $item; + $view->setItem($display_id, 'field', $id1, $item); + $this->assertEqual($view->getItem($display_id, 'field', 'id'), $item); + $this->assertEqual($view->getItems('field', $display_id), $expected_items); + } + + /** + * Tests the createDuplicate() View method. + */ + public function testCreateDuplicate() { + $view = views_get_view('archive'); + $copy = $view->createDuplicate(); + + $this->assertTrue($copy instanceof ViewStorage, 'The copied object is a View.'); + + // Check that the original view and the copy have different UUIDs. + $this->assertNotIdentical($view->storage->uuid(), $copy->uuid(), 'The copied view has a new UUID.'); + + // Check the 'name' (ID) is using the View objects default value ('') as it + // gets unset. + $this->assertIdentical($copy->id(), '', 'The ID has been reset.'); + + // Check the other properties. + // @todo Create a reusable property on the base test class for these? + $config_properties = array( + 'disabled', + 'api_version', + 'description', + 'tag', + 'base_table', + 'human_name', + 'core', + ); + + foreach ($config_properties as $property) { + $this->assertIdentical($view->storage->{$property}, $copy->{$property}, format_string('@property property is identical.', array('@property' => $property))); + } + + // Check the displays are the same. + foreach ($view->storage->display as $id => $display) { + // assertIdentical will not work here. + $this->assertEqual($display, $copy->display[$id], format_string('The @display display has been copied correctly.', array('@display' => $id))); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewTestBase.php b/core/modules/views/lib/Drupal/views/Tests/ViewTestBase.php new file mode 100644 index 0000000..0da4c1f --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/ViewTestBase.php @@ -0,0 +1,439 @@ +<?php +/** + * @file + * Definition of Drupal\views\Tests\ViewTestBase. + */ + +namespace Drupal\views\Tests; + +use Drupal\views\ViewExecutable; +use Drupal\simpletest\WebTestBase; + +/** + * Abstract class for views testing. + */ +abstract class ViewTestBase extends WebTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views'); + + /** + * The view to use for the test. + * + * @var Drupal\views\ViewExecutable + */ + protected $view; + + protected function setUp() { + parent::setUp(); + + $this->view = $this->getBasicView(); + } + + /** + * Sets up the views_test_data.module. + * + * Because the schema of views_test_data.module is dependent on the test using it, + * it cannot be enabled normally. + */ + protected function enableViewsTestModule() { + // Define the schema and views data variable before enabling the test module. + variable_set('views_test_data_schema', $this->schemaDefinition()); + variable_set('views_test_data_views_data', $this->viewsData()); + + module_enable(array('views_test_data')); + $this->resetAll(); + + // Load the test dataset. + $data_set = $this->dataSet(); + $query = db_insert('views_test_data') + ->fields(array_keys($data_set[0])); + foreach ($data_set as $record) { + $query->values($record); + } + $query->execute(); + $this->checkPermissions(array(), TRUE); + + // Reset the test view, in case it was dependent on the test data module. + $this->view = $this->getBasicView(); + } + + /** + * Helper function: verify a result set returned by view. + * + * The comparison is done on the string representation of the columns of the + * column map, taking the order of the rows into account, but not the order + * of the columns. + * + * @param $view + * An executed View. + * @param $expected_result + * An expected result set. + * @param $column_map + * An associative array mapping the columns of the result set from the view + * (as keys) and the expected result set (as values). + */ + protected function assertIdenticalResultset($view, $expected_result, $column_map = array(), $message = 'Identical result set') { + return $this->assertIdenticalResultsetHelper($view, $expected_result, $column_map, $message, 'assertIdentical'); + } + + /** + * Helper function: verify a result set returned by view.. + * + * Inverse of ViewsTestCase::assertIdenticalResultset(). + * + * @param $view + * An executed View. + * @param $expected_result + * An expected result set. + * @param $column_map + * An associative array mapping the columns of the result set from the view + * (as keys) and the expected result set (as values). + */ + protected function assertNotIdenticalResultset($view, $expected_result, $column_map = array(), $message = 'Identical result set') { + return $this->assertIdenticalResultsetHelper($view, $expected_result, $column_map, $message, 'assertNotIdentical'); + } + + protected function assertIdenticalResultsetHelper($view, $expected_result, $column_map, $message, $assert_method) { + // Convert $view->result to an array of arrays. + $result = array(); + foreach ($view->result as $key => $value) { + $row = array(); + foreach ($column_map as $view_column => $expected_column) { + // The comparison will be done on the string representation of the value. + $row[$expected_column] = (string) $value->$view_column; + } + $result[$key] = $row; + } + + // Remove the columns we don't need from the expected result. + foreach ($expected_result as $key => $value) { + $row = array(); + foreach ($column_map as $expected_column) { + // The comparison will be done on the string representation of the value. + $row[$expected_column] = (string) (is_object($value) ? $value->$expected_column : $value[$expected_column]); + } + $expected_result[$key] = $row; + } + + // Reset the numbering of the arrays. + $result = array_values($result); + $expected_result = array_values($expected_result); + + $this->verbose('<pre>Returned data set: ' . print_r($result, TRUE) . "\n\nExpected: ". print_r($expected_result, TRUE)); + + // Do the actual comparison. + return $this->$assert_method($result, $expected_result, $message); + } + + /** + * Helper function: order an array of array based on a column. + */ + protected function orderResultSet($result_set, $column, $reverse = FALSE) { + $this->sort_column = $column; + $this->sort_order = $reverse ? -1 : 1; + usort($result_set, array($this, 'helperCompareFunction')); + return $result_set; + } + + protected $sort_column = NULL; + protected $sort_order = 1; + + /** + * Helper comparison function for orderResultSet(). + */ + protected function helperCompareFunction($a, $b) { + $value1 = $a[$this->sort_column]; + $value2 = $b[$this->sort_column]; + if ($value1 == $value2) { + return 0; + } + return $this->sort_order * (($value1 < $value2) ? -1 : 1); + } + + /** + * Helper function to check whether a button with a certain id exists and has a certain label. + */ + protected function helperButtonHasLabel($id, $expected_label, $message = 'Label has the expected value: %label.') { + return $this->assertFieldById($id, $expected_label, t($message, array('%label' => $expected_label))); + } + + /** + * Helper function to execute a view with debugging. + * + * @param view $view + * @param array $args + */ + protected function executeView($view, $args = array()) { + $view->setDisplay(); + $view->preExecute($args); + $view->execute(); + $this->verbose('<pre>Executed view: ' . ((string) $view->build_info['query']) . '</pre>'); + } + + /** + * Build and return a Page view of the views_test_data table. + * + * @return view + */ + protected function getBasicPageView() { + $view = $this->getView(); + + // In order to test exposed filters, we have to disable + // the exposed forms cache. + drupal_static_reset('views_exposed_form_cache'); + + $display = $view->storage->newDisplay('page', 'Page', 'page_1'); + return $view; + } + + /** + * The schema definition. + */ + protected function schemaDefinition() { + $schema['views_test_data'] = array( + 'description' => 'Basic test table for Views tests.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => "A person's name", + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'age' => array( + 'description' => "The person's age", + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0), + 'job' => array( + 'description' => "The person's job", + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => 'Undefined', + ), + 'created' => array( + 'description' => "The creation date of this record", + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'name' => array('name') + ), + 'indexes' => array( + 'ages' => array('age'), + ), + ); + return $schema; + } + + /** + * The views data definition. + */ + protected function viewsData() { + // Declaration of the base table. + $data['views_test_data']['table'] = array( + 'group' => t('Views test'), + 'base' => array( + 'field' => 'id', + 'title' => t('Views test data'), + 'help' => t('Users who have created accounts on your site.'), + ), + ); + + // Declaration of fields. + $data['views_test_data']['id'] = array( + 'title' => t('ID'), + 'help' => t('The test data ID'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + $data['views_test_data']['name'] = array( + 'title' => t('Name'), + 'help' => t('The name of the person'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'string', + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + $data['views_test_data']['age'] = array( + 'title' => t('Age'), + 'help' => t('The age of the person'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'numeric', + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + $data['views_test_data']['job'] = array( + 'title' => t('Job'), + 'help' => t('The job of the person'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'string', + ), + 'filter' => array( + 'id' => 'string', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + $data['views_test_data']['created'] = array( + 'title' => t('Created'), + 'help' => t('The creation date of this record'), + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'id' => 'date', + ), + 'filter' => array( + 'id' => 'date', + ), + 'sort' => array( + 'id' => 'date', + ), + ); + return $data; + } + + /** + * A very simple test dataset. + */ + protected function dataSet() { + return array( + array( + 'name' => 'John', + 'age' => 25, + 'job' => 'Singer', + 'created' => gmmktime(0, 0, 0, 1, 1, 2000), + ), + array( + 'name' => 'George', + 'age' => 27, + 'job' => 'Singer', + 'created' => gmmktime(0, 0, 0, 1, 2, 2000), + ), + array( + 'name' => 'Ringo', + 'age' => 28, + 'job' => 'Drummer', + 'created' => gmmktime(6, 30, 30, 1, 1, 2000), + ), + array( + 'name' => 'Paul', + 'age' => 26, + 'job' => 'Songwriter', + 'created' => gmmktime(6, 0, 0, 1, 1, 2000), + ), + array( + 'name' => 'Meredith', + 'age' => 30, + 'job' => 'Speaker', + 'created' => gmmktime(6, 30, 10, 1, 1, 2000), + ), + ); + } + + /** + * Build and return a basic view of the views_test_data table. + * + * @return Drupal\views\ViewExecutable + */ + protected function getBasicView() { + return $this->createViewFromConfig('test_view'); + } + + /** + * Creates a new View instance by creating directly from config data. + * + * @param string $view_name + * The name of the test view to create. + * + * @return Drupal\views\ViewExecutable + * A View instance. + */ + protected function createViewFromConfig($view_name) { + if (!module_exists('views_test_config')) { + module_enable(array('views_test_config')); + } + + $data = config("views.view.$view_name")->get(); + + $view = entity_create('view', $data); + $view = $view->getExecutable(); + $view->setDisplay(); + + return $view; + } + + /** + * Clones the view used in this test and sets the default display. + * + * @param Drupal\views\ViewStorage $original_view + * (optional) The view to clone. If not specified, the default view for the + * test will be used. + * + * @return Drupal\views\ViewExecutable + * A clone of the view. + */ + protected function getView($original_view = NULL) { + if (isset($original_view)) { + $view = $original_view->cloneView(); + } + else { + $view = $this->view->cloneView(); + } + $view->setDisplay(); + return $view; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewsDataTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewsDataTest.php new file mode 100644 index 0000000..66406ef --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/ViewsDataTest.php @@ -0,0 +1,52 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\ViewsDataTest. + */ + +namespace Drupal\views\Tests; + +/** + * Tests the fetching of views data. + * + * @see hook_views_data + */ +class ViewsDataTest extends ViewTestBase { + + public static function getInfo() { + return array( + 'name' => 'Table Data', + 'description' => 'Tests the fetching of views data.', + 'group' => 'Views', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->enableViewsTestModule(); + } + + /** + * Tests the views_fetch_data function. + * + * @see views_fetch_data + */ + public function testViewsFetchData() { + $table_name = 'views_test_data'; + $expected_data = $this->viewsData(); + + $data = views_fetch_data($table_name); + $this->assertEqual($data, $expected_data[$table_name], 'Make sure fetching views data by table works as expected.'); + + $data = views_fetch_data(); + $this->assertTrue(isset($data[$table_name]), 'Make sure the views_test_data info appears in the total views data.'); + $this->assertEqual($data[$table_name], $expected_data[$table_name], 'Make sure the views_test_data has the expected values.'); + + $data = views_fetch_data(NULL, TRUE, TRUE); + $this->assertTrue(isset($data[$table_name]), 'Make sure the views_fetch_data appears in the total views data with reset = TRUE.'); + $this->assertEqual($data[$table_name], $expected_data[$table_name], 'Make sure the views_test_data has the expected values.'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Wizard/BasicTest.php b/core/modules/views/lib/Drupal/views/Tests/Wizard/BasicTest.php new file mode 100644 index 0000000..965e8cb --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Wizard/BasicTest.php @@ -0,0 +1,140 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Wizard\BasicTest. + */ + +namespace Drupal\views\Tests\Wizard; + +/** + * Tests creating views with the wizard and viewing them on the listing page. + */ +class BasicTest extends WizardTestBase { + + public static function getInfo() { + return array( + 'name' => 'Basic functionality', + 'description' => 'Test creating basic views with the wizard and viewing them on the listing page.', + 'group' => 'Views Wizard', + ); + } + + function testViewsWizardAndListing() { + $this->drupalCreateContentType(array('type' => 'article')); + $this->drupalCreateContentType(array('type' => 'page')); + + // Check if we can access the main views admin page. + $this->drupalGet('admin/structure/views'); + $this->assertText(t('Add new view')); + + // Create a simple and not at all useful view. + $view1 = array(); + $view1['human_name'] = $this->randomName(16); + $view1['name'] = strtolower($this->randomName(16)); + $view1['description'] = $this->randomName(16); + $view1['page[create]'] = FALSE; + $this->drupalPost('admin/structure/views/add', $view1, t('Save & exit')); + $this->assertResponse(200); + $this->assertText(t('Your view was saved. You may edit it from the list below.')); + $this->assertText($view1['human_name']); + $this->assertText($view1['description']); + // @todo For now, clone is being left to config.module to solve. + foreach (array('delete', 'edit') as $operation) { + $this->assertLinkByHref(url('admin/structure/views/view/' . $view1['name'] . '/' . $operation)); + } + + // This view should not have a block. + $this->drupalGet('admin/structure/block'); + $this->assertNoText('View: ' . $view1['human_name']); + + // Create two nodes. + $node1 = $this->drupalCreateNode(array('type' => 'page')); + $node2 = $this->drupalCreateNode(array('type' => 'article')); + + // Now create a page with simple node listing and an attached feed. + $view2 = array(); + $view2['human_name'] = $this->randomName(16); + $view2['name'] = strtolower($this->randomName(16)); + $view2['description'] = $this->randomName(16); + $view2['page[create]'] = 1; + $view2['page[title]'] = $this->randomName(16); + $view2['page[path]'] = $this->randomName(16); + $view2['page[feed]'] = 1; + $view2['page[feed_properties][path]'] = $this->randomName(16); + $this->drupalPost('admin/structure/views/add', $view2, t('Save & exit')); + $this->assertResponse(200); + + // Since the view has a page, we expect to be automatically redirected to + // it. + $this->assertUrl($view2['page[path]']); + $this->assertText($view2['page[title]']); + $this->assertText($node1->label()); + $this->assertText($node2->label()); + + // Check if we have the feed. + $this->assertLinkByHref(url($view2['page[feed_properties][path]'])); + $this->drupalGet($view2['page[feed_properties][path]']); + $this->assertRaw('<rss version="2.0"'); + // The feed should have the same title and nodes as the page. + $this->assertText($view2['page[title]']); + $this->assertRaw(url('node/' . $node1->nid, array('absolute' => TRUE))); + $this->assertText($node1->label()); + $this->assertRaw(url('node/' . $node2->nid, array('absolute' => TRUE))); + $this->assertText($node2->label()); + + // Go back to the views page and check if this view is there. + $this->drupalGet('admin/structure/views'); + $this->assertText($view2['human_name']); + $this->assertText($view2['description']); + $this->assertLinkByHref(url($view2['page[path]'])); + + // This view should not have a block. + $this->drupalGet('admin/structure/block'); + $this->assertNoText('View: ' . $view2['human_name']); + + // Create a view with a page and a block, and filter the listing. + $view3 = array(); + $view3['human_name'] = $this->randomName(16); + $view3['name'] = strtolower($this->randomName(16)); + $view3['description'] = $this->randomName(16); + $view3['show[wizard_key]'] = 'node'; + $view3['show[type]'] = 'page'; + $view3['page[create]'] = 1; + $view3['page[title]'] = $this->randomName(16); + $view3['page[path]'] = $this->randomName(16); + $view3['block[create]'] = 1; + $view3['block[title]'] = $this->randomName(16); + $this->drupalPost('admin/structure/views/add', $view3, t('Save & exit')); + $this->assertResponse(200); + + // Make sure the view only displays the node we expect. + $this->assertUrl($view3['page[path]']); + $this->assertText($view3['page[title]']); + $this->assertText($node1->label()); + $this->assertNoText($node2->label()); + + // Go back to the views page and check if this view is there. + $this->drupalGet('admin/structure/views'); + $this->assertText($view3['human_name']); + $this->assertText($view3['description']); + $this->assertLinkByHref(url($view3['page[path]'])); + + // Put the block into the first sidebar region. + $this->drupalGet('admin/structure/block'); + $this->assertText('View: ' . $view3['human_name']); + $edit = array(); + $edit["blocks[views_{$view3['name']}-block][region]"] = 'sidebar_first'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + + // Visit a random page (not the one that displays the view itself) and look + // for the expected node title in the block. + $this->drupalGet('user'); + $this->assertText($node1->label()); + $this->assertNoText($node2->label()); + + // Make sure the listing page doesn't show disabled default views. + $this->assertNoText('tracker', t('Default tracker view does not show on the listing page.')); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Wizard/ItemsPerPageTest.php b/core/modules/views/lib/Drupal/views/Tests/Wizard/ItemsPerPageTest.php new file mode 100644 index 0000000..3759e5b --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Wizard/ItemsPerPageTest.php @@ -0,0 +1,98 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Wizard\ItemsPerPageTest. + */ + +namespace Drupal\views\Tests\Wizard; + +/** + * Tests the ability of the views wizard to specify the number of items per page. + */ +class ItemsPerPageTest extends WizardTestBase { + + public static function getInfo() { + return array( + 'name' => 'Items per page functionality', + 'description' => 'Test the ability of the views wizard to specify the number of items per page.', + 'group' => 'Views Wizard', + ); + } + + /** + * Tests the number of items per page. + */ + function testItemsPerPage() { + $this->drupalCreateContentType(array('type' => 'article')); + + // Create articles, each with a different creation time so that we can do a + // meaningful sort. + $node1 = $this->drupalCreateNode(array('type' => 'article', 'created' => REQUEST_TIME)); + $node2 = $this->drupalCreateNode(array('type' => 'article', 'created' => REQUEST_TIME + 1)); + $node3 = $this->drupalCreateNode(array('type' => 'article', 'created' => REQUEST_TIME + 2)); + $node4 = $this->drupalCreateNode(array('type' => 'article', 'created' => REQUEST_TIME + 3)); + $node5 = $this->drupalCreateNode(array('type' => 'article', 'created' => REQUEST_TIME + 4)); + + // Create a page. This should never appear in the view created below. + $page_node = $this->drupalCreateNode(array('type' => 'page', 'created' => REQUEST_TIME + 2)); + + // Create a view that sorts newest first, and shows 4 items in the page and + // 3 in the block. + $view = array(); + $view['human_name'] = $this->randomName(16); + $view['name'] = strtolower($this->randomName(16)); + $view['description'] = $this->randomName(16); + $view['show[wizard_key]'] = 'node'; + $view['show[type]'] = 'article'; + $view['show[sort]'] = 'created:DESC'; + $view['page[create]'] = 1; + $view['page[title]'] = $this->randomName(16); + $view['page[path]'] = $this->randomName(16); + $view['page[items_per_page]'] = 4; + $view['block[create]'] = 1; + $view['block[title]'] = $this->randomName(16); + $view['block[items_per_page]'] = 3; + $this->drupalPost('admin/structure/views/add', $view, t('Save & exit')); + $this->assertResponse(200); + + // Make sure the page display shows the nodes we expect, and that they + // appear in the expected order. + $this->assertUrl($view['page[path]']); + $this->assertText($view['page[title]']); + $content = $this->drupalGetContent(); + $this->assertText($node5->label()); + $this->assertText($node4->label()); + $this->assertText($node3->label()); + $this->assertText($node2->label()); + $this->assertNoText($node1->label()); + $this->assertNoText($page_node->label()); + $pos5 = strpos($content, $node5->label()); + $pos4 = strpos($content, $node4->label()); + $pos3 = strpos($content, $node3->label()); + $pos2 = strpos($content, $node2->label()); + $this->assertTrue($pos5 < $pos4 && $pos4 < $pos3 && $pos3 < $pos2, t('The nodes appear in the expected order in the page display.')); + + // Put the block into the first sidebar region, visit a page that displays + // the block, and check that the nodes we expect appear in the correct + // order. + $this->drupalGet('admin/structure/block'); + $this->assertText('View: ' . $view['human_name']); + $edit = array(); + $edit["blocks[views_{$view['name']}-block][region]"] = 'sidebar_first'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + $this->drupalGet('user'); + $content = $this->drupalGetContent(); + $this->assertText($node5->label()); + $this->assertText($node4->label()); + $this->assertText($node3->label()); + $this->assertNoText($node2->label()); + $this->assertNoText($node1->label()); + $this->assertNoText($page_node->label()); + $pos5 = strpos($content, $node5->label()); + $pos4 = strpos($content, $node4->label()); + $pos3 = strpos($content, $node3->label()); + $this->assertTrue($pos5 < $pos4 && $pos4 < $pos3, t('The nodes appear in the expected order in the block display.')); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Wizard/MenuTest.php b/core/modules/views/lib/Drupal/views/Tests/Wizard/MenuTest.php new file mode 100644 index 0000000..1340534 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Wizard/MenuTest.php @@ -0,0 +1,60 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Wizard\MenuTest. + */ + +namespace Drupal\views\Tests\Wizard; + +/** + * Tests the ability of the views wizard to put views in a menu. + */ +class MenuTest extends WizardTestBase { + + public static function getInfo() { + return array( + 'name' => 'Menu functionality', + 'description' => 'Test the ability of the views wizard to put views in a menu.', + 'group' => 'Views Wizard', + ); + } + + /** + * Tests the menu functionality. + */ + function testMenus() { + // Create a view with a page display and a menu link in the Main Menu. + $view = array(); + $view['human_name'] = $this->randomName(16); + $view['name'] = strtolower($this->randomName(16)); + $view['description'] = $this->randomName(16); + $view['page[create]'] = 1; + $view['page[title]'] = $this->randomName(16); + $view['page[path]'] = $this->randomName(16); + $view['page[link]'] = 1; + $view['page[link_properties][menu_name]'] = 'main-menu'; + $view['page[link_properties][title]'] = $this->randomName(16); + $this->drupalPost('admin/structure/views/add', $view, t('Save & exit')); + $this->assertResponse(200); + + // Make sure there is a link to the view from the front page (where we + // expect the main menu to display). + $this->drupalGet(''); + $this->assertResponse(200); + $this->assertLink($view['page[link_properties][title]']); + $this->assertLinkByHref(url($view['page[path]'])); + + // Make sure the link is associated with the main menu. + $links = menu_load_links('main-menu'); + $found = FALSE; + foreach ($links as $link) { + if ($link['link_path'] == $view['page[path]']) { + $found = TRUE; + break; + } + } + $this->assertTrue($found, t('Found a link to %path in the main menu', array('%path' => $view['page[path]']))); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Wizard/SortingTest.php b/core/modules/views/lib/Drupal/views/Tests/Wizard/SortingTest.php new file mode 100644 index 0000000..888d60c --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Wizard/SortingTest.php @@ -0,0 +1,82 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Wizard\SortingTest. + */ + +namespace Drupal\views\Tests\Wizard; + +/** + * Tests the ability of the views wizard to create views with sorts. + */ +class SortingTest extends WizardTestBase { + + public static function getInfo() { + return array( + 'name' => 'Sorting functionality', + 'description' => 'Test the ability of the views wizard to create views with sorts.', + 'group' => 'Views Wizard', + ); + } + + /** + * Tests the sorting functionality. + */ + function testSorting() { + // Create nodes, each with a different creation time so that we can do a + // meaningful sort. + $node1 = $this->drupalCreateNode(array('created' => REQUEST_TIME)); + $node2 = $this->drupalCreateNode(array('created' => REQUEST_TIME + 1)); + $node3 = $this->drupalCreateNode(array('created' => REQUEST_TIME + 2)); + + // Create a view that sorts oldest first. + $view1 = array(); + $view1['human_name'] = $this->randomName(16); + $view1['name'] = strtolower($this->randomName(16)); + $view1['description'] = $this->randomName(16); + $view1['show[sort]'] = 'created:ASC'; + $view1['page[create]'] = 1; + $view1['page[title]'] = $this->randomName(16); + $view1['page[path]'] = $this->randomName(16); + $this->drupalPost('admin/structure/views/add', $view1, t('Save & exit')); + $this->assertResponse(200); + + // Make sure the view shows the nodes in the expected order. + $this->assertUrl($view1['page[path]']); + $this->assertText($view1['page[title]']); + $content = $this->drupalGetContent(); + $this->assertText($node1->label()); + $this->assertText($node2->label()); + $this->assertText($node3->label()); + $pos1 = strpos($content, $node1->label()); + $pos2 = strpos($content, $node2->label()); + $pos3 = strpos($content, $node3->label()); + $this->assertTrue($pos1 < $pos2 && $pos2 < $pos3, t('The nodes appear in the expected order in a view that sorts by oldest first.')); + + // Create a view that sorts newest first. + $view2 = array(); + $view2['human_name'] = $this->randomName(16); + $view2['name'] = strtolower($this->randomName(16)); + $view2['description'] = $this->randomName(16); + $view2['show[sort]'] = 'created:DESC'; + $view2['page[create]'] = 1; + $view2['page[title]'] = $this->randomName(16); + $view2['page[path]'] = $this->randomName(16); + $this->drupalPost('admin/structure/views/add', $view2, t('Save & exit')); + $this->assertResponse(200); + + // Make sure the view shows the nodes in the expected order. + $this->assertUrl($view2['page[path]']); + $this->assertText($view2['page[title]']); + $content = $this->drupalGetContent(); + $this->assertText($node3->label()); + $this->assertText($node2->label()); + $this->assertText($node1->label()); + $pos3 = strpos($content, $node3->label()); + $pos2 = strpos($content, $node2->label()); + $pos1 = strpos($content, $node1->label()); + $this->assertTrue($pos3 < $pos2 && $pos2 < $pos1, t('The nodes appear in the expected order in a view that sorts by newest first.')); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Wizard/TaggedWithTest.php b/core/modules/views/lib/Drupal/views/Tests/Wizard/TaggedWithTest.php new file mode 100644 index 0000000..cf3d19e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Wizard/TaggedWithTest.php @@ -0,0 +1,193 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Wizard\TaggedWithTest. + */ + +namespace Drupal\views\Tests\Wizard; + +/** + * Tests the ability of the views wizard to create views filtered by taxonomy. + */ +class TaggedWithTest extends WizardTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('taxonomy'); + + protected $node_type_with_tags; + + protected $node_type_without_tags; + + protected $tag_vocabulary; + + protected $tag_field; + + protected $tag_instance; + + public static function getInfo() { + return array( + 'name' => 'Taxonomy functionality', + 'description' => 'Test the ability of the views wizard to create views filtered by taxonomy.', + 'group' => 'Views Wizard', + ); + } + + function setUp() { + parent::setUp(); + + // Create two content types. One will have an autocomplete tagging field, + // and one won't. + $this->node_type_with_tags = $this->drupalCreateContentType(); + $this->node_type_without_tags = $this->drupalCreateContentType(); + + // Create the vocabulary for the tag field. + $this->tag_vocabulary = entity_create('taxonomy_vocabulary', array( + 'name' => 'Views testing tags', + 'machine_name' => 'views_testing_tags', + )); + $this->tag_vocabulary->save(); + + // Create the tag field itself. + $this->tag_field = array( + 'field_name' => 'field_views_testing_tags', + 'type' => 'taxonomy_term_reference', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->tag_vocabulary->machine_name, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($this->tag_field); + + // Create an instance of the tag field on one of the content types, and + // configure it to display an autocomplete widget. + $this->tag_instance = array( + 'field_name' => 'field_views_testing_tags', + 'entity_type' => 'node', + 'bundle' => $this->node_type_with_tags->type, + 'widget' => array( + 'type' => 'taxonomy_autocomplete', + ), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + 'teaser' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + ), + ); + field_create_instance($this->tag_instance); + } + + /** + * Tests the "tagged with" functionality. + */ + function testTaggedWith() { + // In this test we will only create nodes that have an instance of the tag + // field. + $node_add_path = 'node/add/' . $this->node_type_with_tags->type; + + // Create three nodes, with different tags. + $tag_field = $this->tag_field['field_name'] . '[' . LANGUAGE_NOT_SPECIFIED . ']'; + $edit = array(); + $edit['title'] = $node_tag1_title = $this->randomName(); + $edit[$tag_field] = 'tag1'; + $this->drupalPost($node_add_path, $edit, t('Save')); + $edit = array(); + $edit['title'] = $node_tag1_tag2_title = $this->randomName(); + $edit[$tag_field] = 'tag1, tag2'; + $this->drupalPost($node_add_path, $edit, t('Save')); + $edit = array(); + $edit['title'] = $node_no_tags_title = $this->randomName(); + $this->drupalPost($node_add_path, $edit, t('Save')); + + // Create a view that filters by taxonomy term "tag1". It should show only + // the two nodes from above that are tagged with "tag1". + $view1 = array(); + // First select the node type and update the form so the correct tag field + // is used. + $view1['show[type]'] = $this->node_type_with_tags->type; + $this->drupalPost('admin/structure/views/add', $view1, t('Update "of type" choice')); + // Now resubmit the entire form to the same URL. + $view1['human_name'] = $this->randomName(16); + $view1['name'] = strtolower($this->randomName(16)); + $view1['description'] = $this->randomName(16); + $view1['show[tagged_with]'] = 'tag1'; + $view1['page[create]'] = 1; + $view1['page[title]'] = $this->randomName(16); + $view1['page[path]'] = $this->randomName(16); + $this->drupalPost(NULL, $view1, t('Save & exit')); + $this->assertResponse(200); + // Visit the page and check that the nodes we expect are present and the + // ones we don't expect are absent. + $this->drupalGet($view1['page[path]']); + $this->assertText($node_tag1_title); + $this->assertText($node_tag1_tag2_title); + $this->assertNoText($node_no_tags_title); + + // Create a view that filters by taxonomy term "tag2". It should show only + // the one node from above that is tagged with "tag2". + $view2 = array(); + $view2['show[type]'] = $this->node_type_with_tags->type; + $this->drupalPost('admin/structure/views/add', $view2, t('Update "of type" choice')); + $this->assertResponse(200); + $view2['human_name'] = $this->randomName(16); + $view2['name'] = strtolower($this->randomName(16)); + $view2['description'] = $this->randomName(16); + $view2['show[tagged_with]'] = 'tag2'; + $view2['page[create]'] = 1; + $view2['page[title]'] = $this->randomName(16); + $view2['page[path]'] = $this->randomName(16); + $this->drupalPost(NULL, $view2, t('Save & exit')); + $this->assertResponse(200); + $this->drupalGet($view2['page[path]']); + $this->assertNoText($node_tag1_title); + $this->assertText($node_tag1_tag2_title); + $this->assertNoText($node_no_tags_title); + } + + /** + * Tests that the "tagged with" form element only shows for node types that support it. + */ + function testTaggedWithByNodeType() { + // The tagging field is associated with one of our node types only. So the + // "tagged with" form element on the view wizard should appear on the form + // by default (when the wizard is configured to display all content) and + // also when the node type that has the tagging field is selected, but not + // when the node type that doesn't have the tagging field is selected. + $tags_xpath = '//input[@name="show[tagged_with]"]'; + $this->drupalGet('admin/structure/views/add'); + $this->assertFieldByXpath($tags_xpath); + $view['show[type]'] = $this->node_type_with_tags->type; + $this->drupalPost('admin/structure/views/add', $view, t('Update "of type" choice')); + $this->assertFieldByXpath($tags_xpath); + $view['show[type]'] = $this->node_type_without_tags->type; + $this->drupalPost(NULL, $view, t('Update "of type" choice')); + $this->assertNoFieldByXpath($tags_xpath); + + // If we add an instance of the tagging field to the second node type, the + // "tagged with" form element should not appear for it too. + $instance = $this->tag_instance; + $instance['bundle'] = $this->node_type_without_tags->type; + field_create_instance($instance); + $view['show[type]'] = $this->node_type_with_tags->type; + $this->drupalPost('admin/structure/views/add', $view, t('Update "of type" choice')); + $this->assertFieldByXpath($tags_xpath); + $view['show[type]'] = $this->node_type_without_tags->type; + $this->drupalPost(NULL, $view, t('Update "of type" choice')); + $this->assertFieldByXpath($tags_xpath); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Wizard/WizardTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Wizard/WizardTestBase.php new file mode 100644 index 0000000..acf02eb --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Wizard/WizardTestBase.php @@ -0,0 +1,32 @@ +<?php + +/** + * @file + * Definition of Drupal\views\Tests\Wizard\WizardTestBase. + */ + +namespace Drupal\views\Tests\Wizard; + +use Drupal\views\Tests\ViewTestBase; + +/** + * Views UI wizard tests. + */ +abstract class WizardTestBase extends ViewTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views_ui', 'block'); + + function setUp() { + parent::setUp(); + + // Create and log in a user with administer views permission. + $views_admin = $this->drupalCreateUser(array('administer views', 'administer blocks', 'bypass node access', 'access user profiles', 'view revisions')); + $this->drupalLogin($views_admin); + } + +} diff --git a/core/modules/views/lib/Drupal/views/ViewExecutable.php b/core/modules/views/lib/Drupal/views/ViewExecutable.php new file mode 100644 index 0000000..36aaee8 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php @@ -0,0 +1,2233 @@ +<?php + +/** + * @file + * Definition of Drupal\views\ViewExecutable. + */ + +namespace Drupal\views; + +use Symfony\Component\HttpFoundation\Response; + +/** + * @defgroup views_objects Objects that represent a View or part of a view + * @{ + * These objects are the core of Views do the bulk of the direction and + * storing of data. All database activity is in these objects. + */ + +/** + * An object to contain all of the data to generate a view, plus the member + * functions to build the view query, execute the query and render the output. + */ +class ViewExecutable { + + /** + * The config entity in which the view is stored. + * + * @var Drupal\views\ViewStorage + */ + public $storage; + + /** + * Whether or not the view has been built. + * + * @todo Group with other static properties. + * + * @var bool + */ + public $built = FALSE; + + /** + * Whether the view has been executed/query has been run. + * + * @todo Group with other static properties. + * + * @var bool + */ + public $executed = FALSE; + + /** + * Any arguments that have been passed into the view. + * + * @var array + */ + public $args = array(); + + /** + * An array of build info. + * + * @var array + */ + public $build_info = array(); + + /** + * Whether this view uses AJAX. + * + * @var bool + */ + public $use_ajax = FALSE; + + /** + * Where the results of a query will go. + * + * The array must use a numeric index starting at 0. + * + * @var array + */ + public $result = array(); + + // May be used to override the current pager info. + + /** + * The current page. If the view uses pagination. + * + * @var int + */ + public $current_page = NULL; + + /** + * The number of items per page. + * + * @var int + */ + public $items_per_page = NULL; + + /** + * The pager offset. + * + * @var int + */ + public $offset = NULL; + + /** + * The total number of rows returned from the query. + * + * @var array + */ + public $total_rows = NULL; + + /** + * Rendered attachments to place before the view. + * + * @var string + */ + public $attachment_before = ''; + + /** + * Rendered attachements to place after the view. + * + * @var string + */ + public $attachment_after = ''; + + // Exposed widget input + + /** + * All the form data from $form_state['values']. + * + * @var array + */ + public $exposed_data = array(); + + /** + * An array of input values from exposed forms. + * + * @var array + */ + public $exposed_input = array(); + + /** + * Exposed widget input directly from the $form_state['values']. + * + * @var array + */ + public $exposed_raw_input = array(); + + /** + * Used to store views that were previously running if we recurse. + * + * @var array + */ + public $old_view = array(); + + /** + * To avoid recursion in views embedded into areas. + * + * @var array + */ + public $parent_views = array(); + + /** + * Whether this view is an attachment to another view. + * + * @var bool + */ + public $is_attachment = NULL; + + /** + * Identifier of the current display. + * + * @var string + */ + public $current_display; + + /** + * Where the $query object will reside. + * + * @var Drupal\views\Plugin\query\QueryInterface + */ + public $query = NULL; + + /** + * The used pager plugin used by the current executed view. + * + * @var Drupal\views\Plugin\views\pager\PagerPluginBase + */ + public $pager = NULL; + + /** + * The current used display plugin. + * + * @var Drupal\views\Plugin\views\display\DisplayPluginBase + */ + public $display_handler; + + /** + * The list of used displays of the view. + * + * An array containing Drupal\views\Plugin\views\display\DisplayPluginBase + * objects. + * + * @var array + */ + public $displayHandlers; + + /** + * The current used style plugin. + * + * @var Drupal\views\Plugin\views\style\StylePluginBase + */ + public $style_plugin; + + /** + * Stores the current active row while rendering. + * + * @var int + */ + public $row_index; + + /** + * Allow to override the url of the current view. + * + * @var string + */ + public $override_url = NULL; + + /** + * Allow to override the path used for generated urls. + * + * @var string + */ + public $override_path = NULL; + + /** + * Allow to override the used database which is used for this query. + * + * @var bool + */ + public $base_database = NULL; + + // Handlers which are active on this view. + + /** + * Stores the field handlers which are initialized on this view. + * + * An array containing Drupal\views\Plugin\views\field\FieldPluginBase + * objects. + * + * @var array + */ + public $field; + + /** + * Stores the argument handlers which are initialized on this view. + * + * An array containing Drupal\views\Plugin\views\argument\ArgumentPluginBase + * objects. + * + * @var array + */ + public $argument; + + /** + * Stores the sort handlers which are initialized on this view. + * + * An array containing Drupal\views\Plugin\views\sort\SortPluginBase objects. + * + * @var array + */ + public $sort; + + /** + * Stores the filter handlers which are initialized on this view. + * + * An array containing Drupal\views\Plugin\views\filter\FilterPluginBase + * objects. + * + * @var array + */ + public $filter; + + /** + * Stores the relationship handlers which are initialized on this view. + * + * An array containing Drupal\views\Plugin\views\relationship\RelationshipPluginBase + * objects. + * + * @var array + */ + public $relationship; + + /** + * Stores the area handlers for the header which are initialized on this view. + * + * An array containing Drupal\views\Plugin\views\area\AreaPluginBase objects. + * + * @var array + */ + public $header; + + /** + * Stores the area handlers for the footer which are initialized on this view. + * + * An array containing Drupal\views\Plugin\views\area\AreaPluginBase objects. + * + * @var array + */ + public $footer; + + /** + * Stores the area handlers for the empty text which are initialized on this view. + * + * An array containing Drupal\views\Plugin\views\area\AreaPluginBase objects. + * + * @var array + */ + public $empty; + + /** + * Stores the current response object. + * + * @var Symfony\Component\HttpFoundation\Response + */ + protected $response = NULL; + + /** + * Does this view already have loaded it's handlers. + * + * @todo Group with other static properties. + * + * @var bool + */ + public $inited; + + /** + * The name of the active style plugin of the view. + * + * @todo remove this and just use $this->style_plugin + * + * @var string + */ + public $plugin_name; + + /** + * The options used by the style plugin of this running view. + * + * @todo To be able to remove it, Drupal\views\Plugin\views\argument\ArgumentPluginBase::default_summary() + * should instantiate the style plugin. + * @var array + */ + public $style_options; + + /** + * The rendered output of the exposed form. + * + * @var string + */ + public $exposed_widgets; + + /** + * If this view has been previewed. + * + * @var bool + */ + public $preview; + + /** + * Force the query to calculate the total number of results. + * + * @todo Move to the query. + * + * @var bool + */ + public $get_total_rows; + + /** + * Indicates if the sorts have been built. + * + * @todo Group with other static properties. + * + * @var bool + */ + public $build_sort; + + /** + * Stores the many-to-one tables for performance. + * + * @var array + */ + public $many_to_one_tables; + + /** + * A unique identifier which allows to update multiple views output via js. + * + * @var string + */ + public $dom_id; + + /** + * Constructs a new ViewExecutable object. + * + * @param Drupal\views\ViewStorage $storage + * The view config entity the actual information is stored on. + */ + public function __construct(ViewStorage $storage) { + // Reference the storage and the executable to each other. + $this->storage = $storage; + $this->storage->setExecutable($this); + } + + /** + * @todo. + */ + public function save() { + $this->storage->save(); + } + + /** + * Returns a list of the sub-object types used by this view. These types are + * stored on the display, and are used in the build process. + */ + public function displayObjects() { + return array('argument', 'field', 'sort', 'filter', 'relationship', 'header', 'footer', 'empty'); + } + + /** + * Set the arguments that come to this view. Usually from the URL + * but possibly from elsewhere. + */ + public function setArguments($args) { + $this->args = $args; + } + + /** + * Change/Set the current page for the pager. + */ + public function setCurrentPage($page) { + $this->current_page = $page; + + // If the pager is already initialized, pass it through to the pager. + if (!empty($this->pager)) { + return $this->pager->set_current_page($page); + } + } + + /** + * Get the current page from the pager. + */ + public function getCurrentPage() { + // If the pager is already initialized, pass it through to the pager. + if (!empty($this->pager)) { + return $this->pager->get_current_page(); + } + + if (isset($this->current_page)) { + return $this->current_page; + } + } + + /** + * Get the items per page from the pager. + */ + public function getItemsPerPage() { + // If the pager is already initialized, pass it through to the pager. + if (!empty($this->pager)) { + return $this->pager->get_items_per_page(); + } + + if (isset($this->items_per_page)) { + return $this->items_per_page; + } + } + + /** + * Set the items per page on the pager. + */ + public function setItemsPerPage($items_per_page) { + $this->items_per_page = $items_per_page; + + // If the pager is already initialized, pass it through to the pager. + if (!empty($this->pager)) { + $this->pager->set_items_per_page($items_per_page); + } + } + + /** + * Get the pager offset from the pager. + */ + public function getOffset() { + // If the pager is already initialized, pass it through to the pager. + if (!empty($this->pager)) { + return $this->pager->get_offset(); + } + + if (isset($this->offset)) { + return $this->offset; + } + } + + /** + * Set the offset on the pager. + */ + public function setOffset($offset) { + $this->offset = $offset; + + // If the pager is already initialized, pass it through to the pager. + if (!empty($this->pager)) { + $this->pager->set_offset($offset); + } + } + + /** + * Determine if the pager actually uses a pager. + */ + public function usePager() { + if (!empty($this->pager)) { + return $this->pager->use_pager(); + } + } + + /** + * Whether or not AJAX should be used. If AJAX is used, paging, + * tablesorting and exposed filters will be fetched via an AJAX call + * rather than a page refresh. + */ + public function setUseAJAX($use_ajax) { + $this->use_ajax = $use_ajax; + } + + /** + * Set the exposed filters input to an array. If unset they will be taken + * from $_GET when the time comes. + */ + public function setExposedInput($filters) { + $this->exposed_input = $filters; + } + + /** + * Figure out what the exposed input for this view is. + */ + public function getExposedInput() { + // Fill our input either from $_GET or from something previously set on the + // view. + if (empty($this->exposed_input)) { + $this->exposed_input = drupal_container()->get('request')->query->all(); + // unset items that are definitely not our input: + foreach (array('page', 'q') as $key) { + if (isset($this->exposed_input[$key])) { + unset($this->exposed_input[$key]); + } + } + + // If we have no input at all, check for remembered input via session. + + // If filters are not overridden, store the 'remember' settings on the + // default display. If they are, store them on this display. This way, + // multiple displays in the same view can share the same filters and + // remember settings. + $display_id = ($this->display_handler->isDefaulted('filters')) ? 'default' : $this->current_display; + + if (empty($this->exposed_input) && !empty($_SESSION['views'][$this->storage->name][$display_id])) { + $this->exposed_input = $_SESSION['views'][$this->storage->name][$display_id]; + } + } + + return $this->exposed_input; + } + + /** + * Set the display for this view and initialize the display handler. + */ + public function initDisplay() { + if (isset($this->current_display)) { + return TRUE; + } + + // Instantiate all displays + foreach (array_keys($this->storage->display) as $id) { + $this->displayHandlers[$id] = views_get_plugin('display', $this->storage->display[$id]['display_plugin']); + if (!empty($this->displayHandlers[$id])) { + // Initialize the new display handler with data. + $this->displayHandlers[$id]->init($this, $this->storage->display[$id]); + // If this is NOT the default display handler, let it know which is + // since it may well utilize some data from the default. + // This assumes that the 'default' handler is always first. It always + // is. Make sure of it. + if ($id != 'default') { + $this->displayHandlers[$id]->default_display =& $this->displayHandlers['default']; + } + } + } + + $this->current_display = 'default'; + $this->display_handler = $this->displayHandlers['default']; + + return TRUE; + } + + /** + * Get the first display that is accessible to the user. + * + * @param array|string $displays + * Either a single display id or an array of display ids. + * + * @return string + * The first accessible display id, at least default. + */ + public function chooseDisplay($displays) { + if (!is_array($displays)) { + return $displays; + } + + $this->initDisplay(); + + foreach ($displays as $display_id) { + if ($this->displayHandlers[$display_id]->access()) { + return $display_id; + } + } + + return 'default'; + } + + /** + * Set the display as current. + * + * @param $display_id + * The id of the display to mark as current. + */ + public function setDisplay($display_id = NULL) { + // If we have not already initialized the display, do so. But be careful. + if (empty($this->current_display)) { + $this->initDisplay(); + + // If handlers were not initialized, and no argument was sent, set up + // to the default display. + if (empty($display_id)) { + $display_id = 'default'; + } + } + + $display_id = $this->chooseDisplay($display_id); + + // If no display id sent in and one wasn't chosen above, we're finished. + if (empty($display_id)) { + return FALSE; + } + + // Ensure the requested display exists. + if (empty($this->displayHandlers[$display_id])) { + $display_id = 'default'; + if (empty($this->displayHandlers[$display_id])) { + debug('set_display() called with invalid display ID @display.', array('@display' => $display_id)); + return FALSE; + } + } + + // Set the current display. + $this->current_display = $display_id; + + // Ensure requested display has a working handler. + if (empty($this->displayHandlers[$display_id])) { + return FALSE; + } + + // Set a shortcut + $this->display_handler = $this->displayHandlers[$display_id]; + + return TRUE; + } + + /** + * Find and initialize the style plugin. + * + * Note that arguments may have changed which style plugin we use, so + * check the view object first, then ask the display handler. + */ + public function initStyle() { + if (isset($this->style_plugin)) { + return is_object($this->style_plugin); + } + + if (!isset($this->plugin_name)) { + $style = $this->display_handler->getOption('style'); + $this->plugin_name = $style['type']; + $this->style_options = $style['options']; + } + + $this->style_plugin = views_get_plugin('style', $this->plugin_name); + + if (empty($this->style_plugin)) { + return FALSE; + } + + // init the new style handler with data. + $this->style_plugin->init($this, $this->display_handler, $this->style_options); + return TRUE; + } + + /** + * Acquire and attach all of the handlers. + */ + public function initHandlers() { + $this->initDisplay(); + if (empty($this->inited)) { + foreach ($this::viewsHandlerTypes() as $key => $info) { + $this->_initHandler($key, $info); + } + $this->inited = TRUE; + } + } + + /** + * Initialize the pager + * + * Like style initialization, pager initialization is held until late + * to allow for overrides. + */ + public function initPager() { + if (!isset($this->pager)) { + $this->pager = $this->display_handler->getPlugin('pager'); + + if ($this->pager->use_pager()) { + $this->pager->set_current_page($this->current_page); + } + + // These overrides may have been set earlier via $view->set_* + // functions. + if (isset($this->items_per_page)) { + $this->pager->set_items_per_page($this->items_per_page); + } + + if (isset($this->offset)) { + $this->pager->set_offset($this->offset); + } + } + } + + /** + * Render the pager, if necessary. + */ + public function renderPager($exposed_input) { + if (!empty($this->pager) && $this->pager->use_pager()) { + return $this->pager->render($exposed_input); + } + + return ''; + } + + /** + * Create a list of base tables eligible for this view. Used primarily + * for the UI. Display must be already initialized. + */ + public function getBaseTables() { + $base_tables = array( + $this->storage->base_table => TRUE, + '#global' => TRUE, + ); + + foreach ($this->display_handler->getHandlers('relationship') as $handler) { + $base_tables[$handler->definition['base']] = TRUE; + } + return $base_tables; + } + + /** + * Run the preQuery() on all active handlers. + */ + protected function _preQuery() { + foreach ($this::viewsHandlerTypes() as $key => $info) { + $handlers = &$this->$key; + $position = 0; + foreach ($handlers as $id => $handler) { + $handlers[$id]->position = $position; + $handlers[$id]->preQuery(); + $position++; + } + } + } + + /** + * Run the postExecute() on all active handlers. + */ + protected function _postExecute() { + foreach ($this::viewsHandlerTypes() as $key => $info) { + $handlers = &$this->$key; + foreach ($handlers as $id => $handler) { + $handlers[$id]->postExecute($this->result); + } + } + } + + /** + * Attach all of the handlers for each type. + * + * @param $key + * One of 'argument', 'field', 'sort', 'filter', 'relationship' + * @param $info + * The $info from viewsHandlerTypes for this object. + */ + protected function _initHandler($key, $info) { + // Load the requested items from the display onto the object. + $this->$key = $this->display_handler->getHandlers($key); + + // This reference deals with difficult PHP indirection. + $handlers = &$this->$key; + + // Run through and test for accessibility. + foreach ($handlers as $id => $handler) { + if (!$handler->access()) { + unset($handlers[$id]); + } + } + } + + /** + * Build all the arguments. + */ + protected function _buildArguments() { + // Initially, we want to build sorts and fields. This can change, though, + // if we get a summary view. + if (empty($this->argument)) { + return TRUE; + } + + // build arguments. + $position = -1; + + // Create a title for use in the breadcrumb trail. + $title = $this->display_handler->getOption('title'); + + $this->build_info['breadcrumb'] = array(); + $breadcrumb_args = array(); + $substitutions = array(); + + $status = TRUE; + + // Iterate through each argument and process. + foreach ($this->argument as $id => $arg) { + $position++; + $argument = &$this->argument[$id]; + + if ($argument->broken()) { + continue; + } + + $argument->setRelationship(); + + $arg = isset($this->args[$position]) ? $this->args[$position] : NULL; + $argument->position = $position; + + if (isset($arg) || $argument->has_default_argument()) { + if (!isset($arg)) { + $arg = $argument->get_default_argument(); + // make sure default args get put back. + if (isset($arg)) { + $this->args[$position] = $arg; + } + // remember that this argument was computed, not passed on the URL. + $argument->is_default = TRUE; + } + + // Set the argument, which will also validate that the argument can be set. + if (!$argument->set_argument($arg)) { + $status = $argument->validateFail($arg); + break; + } + + if ($argument->is_exception()) { + $arg_title = $argument->exception_title(); + } + else { + $arg_title = $argument->get_title(); + $argument->query($this->display_handler->useGroupBy()); + } + + // Add this argument's substitution + $substitutions['%' . ($position + 1)] = $arg_title; + $substitutions['!' . ($position + 1)] = strip_tags(decode_entities($arg)); + + // Since we're really generating the breadcrumb for the item above us, + // check the default action of this argument. + if ($this->display_handler->usesBreadcrumb() && $argument->uses_breadcrumb()) { + $path = $this->getUrl($breadcrumb_args); + if (strpos($path, '%') === FALSE) { + if (!empty($argument->options['breadcrumb_enable']) && !empty($argument->options['breadcrumb'])) { + $breadcrumb = $argument->options['breadcrumb']; + } + else { + $breadcrumb = $title; + } + $this->build_info['breadcrumb'][$path] = str_replace(array_keys($substitutions), $substitutions, $breadcrumb); + } + } + + // Allow the argument to muck with this breadcrumb. + $argument->set_breadcrumb($this->build_info['breadcrumb']); + + // Test to see if we should use this argument's title + if (!empty($argument->options['title_enable']) && !empty($argument->options['title'])) { + $title = $argument->options['title']; + } + + $breadcrumb_args[] = $arg; + } + else { + // determine default condition and handle. + $status = $argument->default_action(); + break; + } + + // Be safe with references and loops: + unset($argument); + } + + // set the title in the build info. + if (!empty($title)) { + $this->build_info['title'] = $title; + } + + // Store the arguments for later use. + $this->build_info['substitutions'] = $substitutions; + + return $status; + } + + /** + * Do some common building initialization. + */ + public function initQuery() { + if (!empty($this->query)) { + $class = get_class($this->query); + if ($class && $class != 'stdClass') { + // return if query is already initialized. + return TRUE; + } + } + + // Create and initialize the query object. + $views_data = views_fetch_data($this->storage->base_table); + $this->storage->base_field = !empty($views_data['table']['base']['field']) ? $views_data['table']['base']['field'] : ''; + if (!empty($views_data['table']['base']['database'])) { + $this->base_database = $views_data['table']['base']['database']; + } + + // Load the options. + $query_options = $this->display_handler->getOption('query'); + + // Create and initialize the query object. + $plugin = !empty($views_data['table']['base']['query_id']) ? $views_data['table']['base']['query_id'] : 'views_query'; + $this->query = views_get_plugin('query', $plugin); + + if (empty($this->query)) { + return FALSE; + } + + $this->query->init($this->storage->base_table, $this->storage->base_field, $query_options['options']); + return TRUE; + } + + /** + * Build the query for the view. + */ + public function build($display_id = NULL) { + if (!empty($this->built)) { + return; + } + + if (empty($this->current_display) || $display_id) { + if (!$this->setDisplay($display_id)) { + return FALSE; + } + } + + // Let modules modify the view just prior to building it. + foreach (module_implements('views_pre_build') as $module) { + $function = $module . '_views_pre_build'; + $function($this); + } + + // Attempt to load from cache. + // @todo Load a build_info from cache. + + $start = microtime(TRUE); + // If that fails, let's build! + $this->build_info = array( + 'query' => '', + 'count_query' => '', + 'query_args' => array(), + ); + + $this->initQuery(); + + // Call a module hook and see if it wants to present us with a + // pre-built query or instruct us not to build the query for + // some reason. + // @todo: Implement this. Use the same mechanism Panels uses. + + // Run through our handlers and ensure they have necessary information. + $this->initHandlers(); + + // Let the handlers interact with each other if they really want. + $this->_preQuery(); + + if ($this->display_handler->usesExposed()) { + $exposed_form = $this->display_handler->getPlugin('exposed_form'); + $this->exposed_widgets = $exposed_form->render_exposed_form(); + if (form_set_error() || !empty($this->build_info['abort'])) { + $this->built = TRUE; + // Don't execute the query, but rendering will still be executed to display the empty text. + $this->executed = TRUE; + return empty($this->build_info['fail']); + } + } + + // Build all the relationships first thing. + $this->_build('relationship'); + + // Set the filtering groups. + if (!empty($this->filter)) { + $filter_groups = $this->display_handler->getOption('filter_groups'); + if ($filter_groups) { + $this->query->set_group_operator($filter_groups['operator']); + foreach ($filter_groups['groups'] as $id => $operator) { + $this->query->set_where_group($operator, $id); + } + } + } + + // Build all the filters. + $this->_build('filter'); + + $this->build_sort = TRUE; + + // Arguments can, in fact, cause this whole thing to abort. + if (!$this->_buildArguments()) { + $this->build_time = microtime(TRUE) - $start; + $this->attachDisplays(); + return $this->built; + } + + // Initialize the style; arguments may have changed which style we use, + // so waiting as long as possible is important. But we need to know + // about the style when we go to build fields. + if (!$this->initStyle()) { + $this->build_info['fail'] = TRUE; + return FALSE; + } + + if ($this->style_plugin->usesFields()) { + $this->_build('field'); + } + + // Build our sort criteria if we were instructed to do so. + if (!empty($this->build_sort)) { + // Allow the style handler to deal with sorting. + if ($this->style_plugin->build_sort()) { + $this->_build('sort'); + } + // allow the plugin to build second sorts as well. + $this->style_plugin->build_sort_post(); + } + + // Allow area handlers to affect the query. + $this->_build('header'); + $this->_build('footer'); + $this->_build('empty'); + + // Allow display handler to affect the query: + $this->display_handler->query($this->display_handler->useGroupBy()); + + // Allow style handler to affect the query: + $this->style_plugin->query($this->display_handler->useGroupBy()); + + // Allow exposed form to affect the query: + if (isset($exposed_form)) { + $exposed_form->query(); + } + + if (config('views.settings')->get('sql_signature')) { + $this->query->add_signature($this); + } + + // Let modules modify the query just prior to finalizing it. + $this->query->alter($this); + + // Only build the query if we weren't interrupted. + if (empty($this->built)) { + // Build the necessary info to execute the query. + $this->query->build($this); + } + + $this->built = TRUE; + $this->build_time = microtime(TRUE) - $start; + + // Attach displays + $this->attachDisplays(); + + // Let modules modify the view just after building it. + foreach (module_implements('views_post_build') as $module) { + $function = $module . '_views_post_build'; + $function($this); + } + + return TRUE; + } + + /** + * Internal method to build an individual set of handlers. + * + * @todo Some filter needs this function, even it is internal. + * + * @param string $key + * The type of handlers (filter etc.) which should be iterated over to + * build the relationship and query information. + */ + public function _build($key) { + $handlers = &$this->$key; + foreach ($handlers as $id => $data) { + + if (!empty($handlers[$id]) && is_object($handlers[$id])) { + $multiple_exposed_input = array(0 => NULL); + if ($handlers[$id]->multipleExposedInput()) { + $multiple_exposed_input = $handlers[$id]->group_multiple_exposed_input($this->exposed_data); + } + foreach ($multiple_exposed_input as $group_id) { + // Give this handler access to the exposed filter input. + if (!empty($this->exposed_data)) { + $converted = FALSE; + if ($handlers[$id]->isAGroup()) { + $converted = $handlers[$id]->convert_exposed_input($this->exposed_data, $group_id); + $handlers[$id]->store_group_input($this->exposed_data, $converted); + if (!$converted) { + continue; + } + } + $rc = $handlers[$id]->acceptExposedInput($this->exposed_data); + $handlers[$id]->storeExposedInput($this->exposed_data, $rc); + if (!$rc) { + continue; + } + } + $handlers[$id]->setRelationship(); + $handlers[$id]->query($this->display_handler->useGroupBy()); + } + } + } + } + + /** + * Execute the view's query. + * + * @param string $display_id + * The machine name of the display, which should be executed. + * + * @return bool + * Return whether the executing was successful, for example an argument + * could stop the process. + */ + public function execute($display_id = NULL) { + if (empty($this->built)) { + if (!$this->build($display_id)) { + return FALSE; + } + } + + if (!empty($this->executed)) { + return TRUE; + } + + // Don't allow to use deactivated displays, but display them on the live preview. + if (!$this->display_handler->isEnabled() && empty($this->live_preview)) { + $this->build_info['fail'] = TRUE; + return FALSE; + } + + // Let modules modify the view just prior to executing it. + foreach (module_implements('views_pre_execute') as $module) { + $function = $module . '_views_pre_execute'; + $function($this); + } + + // Check for already-cached results. + if (!empty($this->live_preview)) { + $cache = FALSE; + } + else { + $cache = $this->display_handler->getPlugin('cache'); + } + if ($cache && $cache->cache_get('results')) { + if ($this->pager->use_pager()) { + $this->pager->total_items = $this->total_rows; + $this->pager->update_page_info(); + } + } + else { + $this->query->execute($this); + // Enforce the array key rule as documented in + // views_plugin_query::execute(). + $this->result = array_values($this->result); + $this->_postExecute(); + if ($cache) { + $cache->cache_set('results'); + } + } + + // Let modules modify the view just after executing it. + foreach (module_implements('views_post_execute') as $module) { + $function = $module . '_views_post_execute'; + $function($this); + } + + $this->executed = TRUE; + } + + /** + * Render this view for a certain display. + * + * Note: You should better use just the preview function if you want to + * render a view. + * + * @param string $display_id + * The machine name of the display, which should be rendered. + * + * @return (string|NULL) + * Return the output of the rendered view or NULL if something failed in the process. + */ + public function render($display_id = NULL) { + $this->execute($display_id); + + // Check to see if the build failed. + if (!empty($this->build_info['fail'])) { + return; + } + if (!empty($this->build_info['denied'])) { + return; + } + + drupal_theme_initialize(); + $config = config('views.settings'); + + // Set the response so other parts can alter it. + $this->response = new Response('', 200); + + $start = microtime(TRUE); + if (!empty($this->live_preview) && $config->get('ui.show.additional_queries')) { + $this->startQueryCapture(); + } + + $exposed_form = $this->display_handler->getPlugin('exposed_form'); + $exposed_form->pre_render($this->result); + + // Check for already-cached output. + if (!empty($this->live_preview)) { + $cache = FALSE; + } + else { + $cache = $this->display_handler->getPlugin('cache'); + } + if ($cache && $cache->cache_get('output')) { + } + else { + if ($cache) { + $cache->cache_start(); + } + + // Run pre_render for the pager as it might change the result. + if (!empty($this->pager)) { + $this->pager->pre_render($this->result); + } + + // Initialize the style plugin. + $this->initStyle(); + + // Give field handlers the opportunity to perform additional queries + // using the entire resultset prior to rendering. + if ($this->style_plugin->usesFields()) { + foreach ($this->field as $id => $handler) { + if (!empty($this->field[$id])) { + $this->field[$id]->pre_render($this->result); + } + } + } + + $this->style_plugin->pre_render($this->result); + + // Let modules modify the view just prior to rendering it. + foreach (module_implements('views_pre_render') as $module) { + $function = $module . '_views_pre_render'; + $function($this); + } + + // Let the themes play too, because pre render is a very themey thing. + foreach ($GLOBALS['base_theme_info'] as $base) { + $function = $base->name . '_views_pre_render'; + if (function_exists($function)) { + $function($this); + } + } + $function = $GLOBALS['theme'] . '_views_pre_render'; + if (function_exists($function)) { + $function($this); + } + + $this->display_handler->output = $this->display_handler->render(); + if ($cache) { + $cache->cache_set('output'); + } + } + + $exposed_form->post_render($this->display_handler->output); + + if ($cache) { + $cache->post_render($this->display_handler->output); + } + + // Let modules modify the view output after it is rendered. + foreach (module_implements('views_post_render') as $module) { + $function = $module . '_views_post_render'; + $function($this, $this->display_handler->output, $cache); + } + + // Let the themes play too, because post render is a very themey thing. + foreach ($GLOBALS['base_theme_info'] as $base) { + $function = $base->name . '_views_post_render'; + if (function_exists($function)) { + $function($this); + } + } + $function = $GLOBALS['theme'] . '_views_post_render'; + if (function_exists($function)) { + $function($this, $this->display_handler->output, $cache); + } + + if (!empty($this->live_preview) && $config->get('ui.show.additional_queries')) { + $this->endQueryCapture(); + } + $this->render_time = microtime(TRUE) - $start; + + return $this->display_handler->output; + } + + /** + * Render a specific field via the field ID and the row # + * + * Note: You might want to use views_plugin_style::render_fields as it + * caches the output for you. + * + * @param string $field + * The id of the field to be rendered. + * + * @param int $row + * The row number in the $view->result which is used for the rendering. + * + * @return string + * The rendered output of the field. + */ + public function renderField($field, $row) { + if (isset($this->field[$field]) && isset($this->result[$row])) { + return $this->field[$field]->advanced_render($this->result[$row]); + } + } + + /** + * Execute the given display, with the given arguments. + * To be called externally by whatever mechanism invokes the view, + * such as a page callback, hook_block, etc. + * + * This function should NOT be used by anything external as this + * returns data in the format specified by the display. It can also + * have other side effects that are only intended for the 'proper' + * use of the display, such as setting page titles and breadcrumbs. + * + * If you simply want to view the display, use View::preview() instead. + */ + public function executeDisplay($display_id = NULL, $args = array()) { + if (empty($this->current_display) || $this->current_display != $this->chooseDisplay($display_id)) { + if (!$this->setDisplay($display_id)) { + return FALSE; + } + } + + $this->preExecute($args); + + // Execute the view + $output = $this->display_handler->execute(); + + $this->postExecute(); + return $output; + } + + /** + * Preview the given display, with the given arguments. + * + * To be called externally, probably by an AJAX handler of some flavor. + * Can also be called when views are embedded, as this guarantees + * normalized output. + */ + public function preview($display_id = NULL, $args = array()) { + if (empty($this->current_display) || ((!empty($display_id)) && $this->current_display != $display_id)) { + if (!$this->setDisplay($display_id)) { + return FALSE; + } + } + + $this->preview = TRUE; + $this->preExecute($args); + // Preview the view. + $output = $this->display_handler->preview(); + + $this->postExecute(); + return $output; + } + + /** + * Run attachments and let the display do what it needs to do prior + * to running. + */ + public function preExecute($args = array()) { + $this->old_view[] = views_get_current_view(); + views_set_current_view($this); + $display_id = $this->current_display; + + // Prepare the view with the information we have, but only if we were + // passed arguments, as they may have been set previously. + if ($args) { + $this->setArguments($args); + } + + // Let modules modify the view just prior to executing it. + foreach (module_implements('views_pre_view') as $module) { + $function = $module . '_views_pre_view'; + $function($this, $display_id, $this->args); + } + + // Allow hook_views_pre_view() to set the dom_id, then ensure it is set. + $this->dom_id = !empty($this->dom_id) ? $this->dom_id : md5($this->storage->name . REQUEST_TIME . rand()); + + // Allow the display handler to set up for execution + $this->display_handler->preExecute(); + } + + /** + * Unset the current view, mostly. + */ + public function postExecute() { + // unset current view so we can be properly destructed later on. + // Return the previous value in case we're an attachment. + + if ($this->old_view) { + $old_view = array_pop($this->old_view); + } + + views_set_current_view(isset($old_view) ? $old_view : FALSE); + } + + /** + * Run attachment displays for the view. + */ + public function attachDisplays() { + if (!empty($this->is_attachment)) { + return; + } + + if (!$this->display_handler->acceptAttachments()) { + return; + } + + $this->is_attachment = TRUE; + // Give other displays an opportunity to attach to the view. + foreach ($this->displayHandlers as $id => $display) { + if (!empty($this->displayHandlers[$id])) { + $this->displayHandlers[$id]->attachTo($this->current_display); + } + } + $this->is_attachment = FALSE; + } + + /** + * Called to get hook_menu() information from the view and the named display handler. + * + * @param $display_id + * A display id. + * @param $callbacks + * A menu callback array passed from views_menu_alter(). + */ + public function executeHookMenu($display_id = NULL, &$callbacks = array()) { + // Prepare the view with the information we have. + + // This was probably already called, but it's good to be safe. + if (!$this->setDisplay($display_id)) { + return FALSE; + } + + // Execute the view + if (isset($this->display_handler)) { + return $this->display_handler->executeHookMenu($callbacks); + } + } + + /** + * Called to get hook_block information from the view and the + * named display handler. + */ + public function executeHookBlockList($display_id = NULL) { + // Prepare the view with the information we have. + + // This was probably already called, but it's good to be safe. + if (!$this->setDisplay($display_id)) { + return FALSE; + } + + // Execute the view + if (isset($this->display_handler)) { + return $this->display_handler->executeHookBlockList(); + } + } + + /** + * Determine if the given user has access to the view. Note that + * this sets the display handler if it hasn't been. + */ + public function access($displays = NULL, $account = NULL) { + // Noone should have access to disabled views. + if (!$this->storage->isEnabled()) { + return FALSE; + } + + if (!isset($this->current_display)) { + $this->initDisplay(); + } + + if (!$account) { + $account = $GLOBALS['user']; + } + + // We can't use choose_display() here because that function + // calls this one. + $displays = (array)$displays; + foreach ($displays as $display_id) { + if (!empty($this->displayHandlers[$display_id])) { + if ($this->displayHandlers[$display_id]->access($account)) { + return TRUE; + } + } + } + + return FALSE; + } + + /** + * Sets the used response object of the view. + * + * @param Symfony\Component\HttpFoundation\Response $response + * The response object which should be set. + */ + public function setResponse(Response $response) { + $this->response = $response; + } + + /** + * Gets the response object used by the view. + * + * @return Symfony\Component\HttpFoundation\Response + * The response object of the view. + */ + public function getResponse() { + if (!isset($this->response)) { + $this->response = new Response(); + } + return $this->response; + } + + /** + * Get the view's current title. This can change depending upon how it + * was built. + */ + public function getTitle() { + if (empty($this->display_handler)) { + if (!$this->setDisplay('default')) { + return FALSE; + } + } + + // During building, we might find a title override. If so, use it. + if (!empty($this->build_info['title'])) { + $title = $this->build_info['title']; + } + else { + $title = $this->display_handler->getOption('title'); + } + + // Allow substitutions from the first row. + if ($this->initStyle()) { + $title = $this->style_plugin->tokenize_value($title, 0); + } + return $title; + } + + /** + * Override the view's current title. + * + * The tokens in the title get's replaced before rendering. + */ + public function setTitle($title) { + $this->build_info['title'] = $title; + return TRUE; + } + + /** + * Force the view to build a title. + */ + public function buildTitle() { + $this->initDisplay(); + + if (empty($this->built)) { + $this->initQuery(); + } + + $this->initHandlers(); + + $this->_buildArguments(); + } + + /** + * Get the URL for the current view. + * + * This URL will be adjusted for arguments. + */ + public function getUrl($args = NULL, $path = NULL) { + if (!empty($this->override_url)) { + return $this->override_url; + } + + if (!isset($path)) { + $path = $this->getPath(); + } + if (!isset($args)) { + $args = $this->args; + + // Exclude arguments that were computed, not passed on the URL. + $position = 0; + if (!empty($this->argument)) { + foreach ($this->argument as $argument_id => $argument) { + if (!empty($argument->is_default) && !empty($argument->options['default_argument_skip_url'])) { + unset($args[$position]); + } + $position++; + } + } + } + // Don't bother working if there's nothing to do: + if (empty($path) || (empty($args) && strpos($path, '%') === FALSE)) { + return $path; + } + + $pieces = array(); + $argument_keys = isset($this->argument) ? array_keys($this->argument) : array(); + $id = current($argument_keys); + foreach (explode('/', $path) as $piece) { + if ($piece != '%') { + $pieces[] = $piece; + } + else { + if (empty($args)) { + // Try to never put % in a url; use the wildcard instead. + if ($id && !empty($this->argument[$id]->options['exception']['value'])) { + $pieces[] = $this->argument[$id]->options['exception']['value']; + } + else { + $pieces[] = '*'; // gotta put something if there just isn't one. + } + + } + else { + $pieces[] = array_shift($args); + } + + if ($id) { + $id = next($argument_keys); + } + } + } + + if (!empty($args)) { + $pieces = array_merge($pieces, $args); + } + return implode('/', $pieces); + } + + /** + * Get the base path used for this view. + */ + public function getPath() { + if (!empty($this->override_path)) { + return $this->override_path; + } + + if (empty($this->display_handler)) { + if (!$this->setDisplay('default')) { + return FALSE; + } + } + return $this->display_handler->getPath(); + } + + /** + * Get the breadcrumb used for this view. + * + * @param $set + * If true, use drupal_set_breadcrumb() to install the breadcrumb. + */ + public function getBreadcrumb($set = FALSE) { + // Now that we've built the view, extract the breadcrumb. + $base = TRUE; + $breadcrumb = array(); + + if (!empty($this->build_info['breadcrumb'])) { + foreach ($this->build_info['breadcrumb'] as $path => $title) { + // Check to see if the frontpage is in the breadcrumb trail; if it + // is, we'll remove that from the actual breadcrumb later. + if ($path == config('system.site')->get('page.front')) { + $base = FALSE; + $title = t('Home'); + } + if ($title) { + $breadcrumb[] = l($title, $path, array('html' => TRUE)); + } + } + + if ($set) { + if ($base) { + $breadcrumb = array_merge(drupal_get_breadcrumb(), $breadcrumb); + } + drupal_set_breadcrumb($breadcrumb); + } + } + return $breadcrumb; + } + + /** + * Set up query capturing. + * + * db_query() stores the queries that it runs in global $queries, + * bit only if dev_query is set to true. In this case, we want + * to temporarily override that setting if it's not and we + * can do that without forcing a db rewrite by just manipulating + * $conf. This is kind of evil but it works. + */ + public function startQueryCapture() { + global $conf, $queries; + if (empty($conf['dev_query'])) { + $this->fix_dev_query = TRUE; + $conf['dev_query'] = TRUE; + } + + // Record the last query key used; anything already run isn't + // a query that we are interested in. + $this->last_query_key = NULL; + + if (!empty($queries)) { + $keys = array_keys($queries); + $this->last_query_key = array_pop($keys); + } + } + + /** + * Add the list of queries run during render to buildinfo. + * + * @see View::start_query_capture() + */ + public function endQueryCapture() { + global $conf, $queries; + if (!empty($this->fix_dev_query)) { + $conf['dev_query'] = FALSE; + } + + // make a copy of the array so we can manipulate it with array_splice. + $temp = $queries; + + // Scroll through the queries until we get to our last query key. + // Unset anything in our temp array. + if (isset($this->last_query_key)) { + while (list($id, $query) = each($queries)) { + if ($id == $this->last_query_key) { + break; + } + + unset($temp[$id]); + } + } + + $this->additional_queries = $temp; + } + + /** + * Overrides Drupal\entity\Entity::createDuplicate(). + * + * Makes a copy of this view that has been sanitized of handlers, any runtime + * data, ID, and UUID. + */ + public function createDuplicate() { + $data = config('views.view.' . $this->storage->id())->get(); + + // Reset the name and UUID. + unset($data['name']); + unset($data['uuid']); + + return entity_create('view', $data); + } + + /** + * Safely clone a view. + * + * This will completely wipe a view clean so it can be considered fresh. + * + * @return Drupal\views\ViewExecutable + * The cloned view. + */ + public function cloneView() { + $storage = clone $this->storage; + return $storage->getExecutable(TRUE); + } + + /** + * Unset references so that a $view object may be properly garbage + * collected. + */ + public function destroy() { + foreach (array_keys($this->displayHandlers) as $display_id) { + if (isset($this->displayHandlers[$display_id])) { + $this->displayHandlers[$display_id]->destroy(); + unset($this->displayHandlers[$display_id]); + } + } + + foreach ($this::viewsHandlerTypes() as $type => $info) { + if (isset($this->$type)) { + $handlers = &$this->$type; + foreach ($handlers as $id => $item) { + $handlers[$id]->destroy(); + } + unset($handlers); + } + } + + if (isset($this->style_plugin)) { + $this->style_plugin->destroy(); + } + + $keys = array('current_display', 'display_handler', 'displayHandlers', 'field', 'argument', 'filter', 'sort', 'relationship', 'header', 'footer', 'empty', 'query', 'result', 'inited', 'style_plugin', 'plugin_name', 'exposed_data', 'exposed_input', 'many_to_one_tables'); + foreach ($keys as $key) { + unset($this->$key); + } + + // These keys are checked by the next init, so instead of unsetting them, + // just set the default values. + $keys = array('items_per_page', 'offset', 'current_page'); + foreach ($keys as $key) { + if (isset($this->$key)) { + $this->$key = NULL; + } + } + + $this->built = $this->executed = FALSE; + $this->build_info = array(); + $this->attachment_before = ''; + $this->attachment_after = ''; + } + + /** + * Make sure the view is completely valid. + * + * @return + * TRUE if the view is valid; an array of error strings if it is not. + */ + public function validate() { + $this->initDisplay(); + + $errors = array(); + $this->display_errors = NULL; + + $current_display = $this->current_display; + foreach ($this->displayHandlers as $id => $display) { + if (!empty($display)) { + if (!empty($display->deleted)) { + continue; + } + + $result = $this->displayHandlers[$id]->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + // Mark this display as having validation errors. + $this->display_errors[$id] = TRUE; + } + } + } + + $this->setDisplay($current_display); + return $errors ? $errors : TRUE; + } + + /** + * Provide a list of views handler types used in a view, with some information + * about them. + * + * @return array + * An array of associative arrays containing: + * - title: The title of the handler type. + * - ltitle: The lowercase title of the handler type. + * - stitle: A singular title of the handler type. + * - lstitle: A singular lowercase title of the handler type. + * - plural: Plural version of the handler type. + * - (optional) type: The actual internal used handler type. This key is + * just used for header,footer,empty to link to the internal type: area. + */ + public static function viewsHandlerTypes() { + static $retval = NULL; + + // Statically cache this so t() doesn't run a bajillion times. + if (!isset($retval)) { + $retval = array( + 'field' => array( + 'title' => t('Fields'), // title + 'ltitle' => t('fields'), // lowercase title for mid-sentence + 'stitle' => t('Field'), // singular title + 'lstitle' => t('field'), // singular lowercase title for mid sentence + 'plural' => 'fields', + ), + 'argument' => array( + 'title' => t('Contextual filters'), + 'ltitle' => t('contextual filters'), + 'stitle' => t('Contextual filter'), + 'lstitle' => t('contextual filter'), + 'plural' => 'arguments', + ), + 'sort' => array( + 'title' => t('Sort criteria'), + 'ltitle' => t('sort criteria'), + 'stitle' => t('Sort criterion'), + 'lstitle' => t('sort criterion'), + 'plural' => 'sorts', + ), + 'filter' => array( + 'title' => t('Filter criteria'), + 'ltitle' => t('filter criteria'), + 'stitle' => t('Filter criterion'), + 'lstitle' => t('filter criterion'), + 'plural' => 'filters', + ), + 'relationship' => array( + 'title' => t('Relationships'), + 'ltitle' => t('relationships'), + 'stitle' => t('Relationship'), + 'lstitle' => t('Relationship'), + 'plural' => 'relationships', + ), + 'header' => array( + 'title' => t('Header'), + 'ltitle' => t('header'), + 'stitle' => t('Header'), + 'lstitle' => t('Header'), + 'plural' => 'header', + 'type' => 'area', + ), + 'footer' => array( + 'title' => t('Footer'), + 'ltitle' => t('footer'), + 'stitle' => t('Footer'), + 'lstitle' => t('Footer'), + 'plural' => 'footer', + 'type' => 'area', + ), + 'empty' => array( + 'title' => t('No results behavior'), + 'ltitle' => t('no results behavior'), + 'stitle' => t('No results behavior'), + 'lstitle' => t('No results behavior'), + 'plural' => 'empty', + 'type' => 'area', + ), + ); + } + + return $retval; + } + + /** + * Returns the valid types of plugins that can be used. + * + * @return array + * An array of plugin type strings. + */ + public static function getPluginTypes() { + return array( + 'access', + 'area', + 'argument', + 'argument_default', + 'argument_validator', + 'cache', + 'display_extender', + 'display', + 'exposed_form', + 'field', + 'filter', + 'join', + 'pager', + 'query', + 'relationship', + 'row', + 'sort', + 'style', + 'wizard', + ); + } + + /** + * Adds an instance of a handler to the view. + * + * Items may be fields, filters, sort criteria, or arguments. + * + * @param string $display_id + * The machine name of the display. + * @param string $type + * The type of handler being added. + * @param string $table + * The name of the table this handler is from. + * @param string $field + * The name of the field this handler is from. + * @param array $options + * (optional) Extra options for this instance. Defaults to an empty array. + * @param string $id + * (optional) A unique ID for this handler instance. Defaults to NULL, in + * which case one will be generated. + * + * @return string + * The unique ID for this handler instance. + */ + public function addItem($display_id, $type, $table, $field, $options = array(), $id = NULL) { + $types = $this::viewsHandlerTypes(); + $this->setDisplay($display_id); + + $fields = $this->displayHandlers[$display_id]->getOption($types[$type]['plural']); + + if (empty($id)) { + $id = $this->generateItemId($field, $fields); + } + + // If the desired type is not found, use the original value directly. + $handler_type = !empty($types[$type]['type']) ? $types[$type]['type'] : $type; + + // @todo This variable is never used. + $handler = views_get_handler($table, $field, $handler_type); + + $fields[$id] = array( + 'id' => $id, + 'table' => $table, + 'field' => $field, + ) + $options; + + $this->displayHandlers[$display_id]->setOption($types[$type]['plural'], $fields); + + return $id; + } + + /** + * Generates a unique ID for an handler instance. + * + * These handler instances are typically fields, filters, sort criteria, or + * arguments. + * + * @param string $requested_id + * The requested ID for the handler instance. + * @param array $existing_items + * An array of existing handler instancess, keyed by their IDs. + * + * @return string + * A unique ID. This will be equal to $requested_id if no handler instance + * with that ID already exists. Otherwise, it will be appended with an + * integer to make it unique, e.g., "{$requested_id}_1", + * "{$requested_id}_2", etc. + */ + public static function generateItemId($requested_id, $existing_items) { + $count = 0; + $id = $requested_id; + while (!empty($existing_items[$id])) { + $id = $requested_id . '_' . ++$count; + } + return $id; + } + + /** + * Gets an array of handler instances for the current display. + * + * @param string $type + * The type of handlers to retrieve. + * @param string $display_id + * (optional) A specific display machine name to use. If NULL, the current + * display will be used. + * + * @return array + * An array of handler instances of a given type for this display. + */ + public function getItems($type, $display_id = NULL) { + $this->setDisplay($display_id); + + if (!isset($display_id)) { + $display_id = $this->current_display; + } + + // Get info about the types so we can get the right data. + $types = $this::viewsHandlerTypes(); + return $this->displayHandlers[$display_id]->getOption($types[$type]['plural']); + } + + /** + * Gets the configuration of a handler instance on a given display. + * + * @param string $display_id + * The machine name of the display. + * @param string $type + * The type of handler to retrieve. + * @param string $id + * The ID of the handler to retrieve. + * + * @return array|null + * Either the handler instance's configuration, or NULL if the handler is + * not used on the display. + */ + public function getItem($display_id, $type, $id) { + // Get info about the types so we can get the right data. + $types = $this::viewsHandlerTypes(); + // Initialize the display + $this->setDisplay($display_id); + + // Get the existing configuration + $fields = $this->displayHandlers[$display_id]->getOption($types[$type]['plural']); + + return isset($fields[$id]) ? $fields[$id] : NULL; + } + + /** + * Sets the configuration of a handler instance on a given display. + * + * @param string $display_id + * The machine name of the display. + * @param string $type + * The type of handler being set. + * @param string $id + * The ID of the handler being set. + * @param array|null $item + * An array of configuration for a handler, or NULL to remove this instance. + * + * @see set_item_option() + */ + public function setItem($display_id, $type, $id, $item) { + // Get info about the types so we can get the right data. + $types = $this::viewsHandlerTypes(); + // Initialize the display. + $this->setDisplay($display_id); + + // Get the existing configuration. + $fields = $this->displayHandlers[$display_id]->getOption($types[$type]['plural']); + if (isset($item)) { + $fields[$id] = $item; + } + else { + unset($fields[$id]); + } + + // Store. + $this->displayHandlers[$display_id]->setOption($types[$type]['plural'], $fields); + } + + /** + * Sets an option on a handler instance. + * + * Use this only if you have just 1 or 2 options to set; if you have many, + * consider getting the handler instance, adding the options and using + * set_item() directly. + * + * @param string $display_id + * The machine name of the display. + * @param string $type + * The type of handler being set. + * @param string $id + * The ID of the handler being set. + * @param string $option + * The configuration key for the value being set. + * @param mixed $value + * The value being set. + * + * @see set_item() + */ + public function setItemOption($display_id, $type, $id, $option, $value) { + $item = $this->getItem($display_id, $type, $id); + $item[$option] = $value; + $this->setItem($display_id, $type, $id, $item); + } + + /** + * Creates and stores a new display. + * + * @param string $id + * The ID for the display being added. + * + * @return Drupal\views\Plugin\views\display\DisplayPluginBase + * A reference to the new handler object. + */ + public function &newDisplay($id) { + // Create a handler. + $this->displayHandlers[$id] = views_get_plugin('display', $this->storage->display[$id]['display_plugin']); + if (empty($this->displayHandlers[$id])) { + // provide a 'default' handler as an emergency. This won't work well but + // it will keep things from crashing. + $this->displayHandlers[$id] = views_get_plugin('display', 'default'); + } + + if (!empty($this->displayHandlers[$id])) { + // Initialize the new display handler with data. + $this->displayHandlers[$id]->init($this, $this->storage->display[$id]); + // If this is NOT the default display handler, let it know which is + if ($id != 'default') { + // @todo is the '&' still required in php5? + $this->displayHandlers[$id]->default_display = &$this->displayHandlers['default']; + } + } + + return $this->displayHandlers[$id]; + } + +} diff --git a/core/modules/views/lib/Drupal/views/ViewStorage.php b/core/modules/views/lib/Drupal/views/ViewStorage.php new file mode 100644 index 0000000..df6a8d7 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/ViewStorage.php @@ -0,0 +1,393 @@ +<?php + +/** + * @file + * Definition of Drupal\views\ViewStorage. + */ + +namespace Drupal\views; + +use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\views_ui\ViewUI; + +/** + * Defines a ViewStorage configuration entity class. + */ +class ViewStorage extends ConfigEntityBase implements ViewStorageInterface { + + /** + * The name of the base table this view will use. + * + * @var string + */ + public $base_table = 'node'; + + /** + * The name of the view. + * + * @var string + */ + public $name = ''; + + /** + * The description of the view, which is used only in the interface. + * + * @var string + */ + public $description = ''; + + /** + * The "tags" of a view. + * + * The tags are stored as a single string, though it is used as multiple tags + * for example in the views overview. + * + * @var string + */ + public $tag = ''; + + /** + * The human readable name of the view. + * + * @var string + */ + public $human_name = ''; + + /** + * The core version the view was created for. + * + * @var int + */ + public $core = DRUPAL_CORE_COMPATIBILITY; + + /** + * The views API version this view was created by. + * + * @var string + */ + public $api_version = VIEWS_API_VERSION; + + /** + * Stores all display handlers of this view. + * + * An array containing Drupal\views\Plugin\views\display\DisplayPluginBase + * objects. + * + * @var array + */ + public $display; + + /** + * The name of the base field to use. + * + * @var string + */ + public $base_field = 'nid'; + + /** + * Returns whether the view's status is disabled or not. + * + * This value is used for exported view, to provide some default views which + * aren't enabled. + * + * @var bool + */ + public $disabled = FALSE; + + /** + * The UUID for this entity. + * + * @var string + */ + public $uuid = NULL; + + /** + * Stores a reference to the executable version of this view. + * + * @var Drupal\views\ViewExecutable + */ + protected $executable; + + /** + * The module implementing this view. + * + * @var string + */ + public $module = 'views'; + + /** + * Stores the executable version of this view. + * + * @param Drupal\views\ViewExecutable $executable + * The executable version of this view. + */ + public function setExecutable(ViewExecutable $executable) { + $this->executable = $executable; + } + + /** + * Retrieves the executable version of this view. + * + * @param bool $reset + * Get a new Drupal\views\ViewExecutable instance. + * @param bool $ui + * If this should return Drupal\views_ui\ViewUI instead. + * + * @return Drupal\views\ViewExecutable + * The executable version of this view. + */ + public function getExecutable($reset = FALSE, $ui = FALSE) { + if (!isset($this->executable) || $reset) { + // @todo Remove this approach and use proper dependency injection. + if ($ui) { + $executable = new ViewUI($this); + } + else { + $executable = new ViewExecutable($this); + } + $this->setExecutable($executable); + } + return $this->executable; + } + + /** + * Initializes the display. + * + * @todo Inspect calls to this and attempt to clean up. + * @see Drupal\views\ViewExecutable::initDisplay() + */ + public function initDisplay() { + $this->getExecutable()->initDisplay(); + } + + /** + * Returns the name of the module implementing this view. + */ + public function getModule() { + return $this->module; + } + + /** + * Overrides Drupal\Core\Entity\EntityInterface::uri(). + */ + public function uri() { + $info = $this->entityInfo(); + return array( + 'path' => $info['list path'], + ); + } + + /** + * Overrides Drupal\Core\Entity\EntityInterface::id(). + */ + public function id() { + return $this->name; + } + + /** + * Implements Drupal\views\ViewStorageInterface::enable(). + */ + public function enable() { + $this->disabled = FALSE; + $this->save(); + } + + /** + * Implements Drupal\views\ViewStorageInterface::disable(). + */ + public function disable() { + $this->disabled = TRUE; + $this->save(); + } + + /** + * Implements Drupal\views\ViewStorageInterface::isEnabled(). + */ + public function isEnabled() { + return !$this->disabled; + } + + /** + * Return the human readable name for a view. + * + * When a certain view doesn't have a human readable name return the machine readable name. + */ + public function getHumanName() { + if (!empty($this->human_name)) { + $human_name = $this->human_name; + } + else { + $human_name = $this->name; + } + return $human_name; + } + + /** + * Perform automatic updates when loading or importing a view. + * + * Over time, some things about Views or Drupal data has changed. + * this attempts to do some automatic updates that must happen + * to ensure older views will at least try to work. + */ + public function update() { + // When views are converted automatically the base_table should be renamed + // to have a working query. + $this->base_table = views_move_table($this->base_table); + } + + /** + * Adds a new display handler to the view, automatically creating an ID. + * + * @param string $plugin_id + * (optional) The plugin type from the Views plugin annotation. Defaults to + * 'page'. + * @param string $title + * (optional) The title of the display. Defaults to NULL. + * @param string $id + * (optional) The ID to use, e.g., 'default', 'page_1', 'block_2'. Defaults + * to NULL. + * + * @return string|false + * The key to the display in $view->display, or FALSE if no plugin ID was + * provided. + */ + public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) { + if (empty($plugin_id)) { + return FALSE; + } + + $plugin = views_get_plugin_definition('display', $plugin_id); + if (empty($plugin)) { + $plugin['title'] = t('Broken'); + } + + if (empty($id)) { + $id = $this->generateDisplayId($plugin_id); + + // Generate a unique human-readable name by inspecting the counter at the + // end of the previous display ID, e.g., 'page_1'. + if ($id !== 'default') { + preg_match("/[0-9]+/", $id, $count); + $count = $count[0]; + } + else { + $count = ''; + } + + if (empty($title)) { + // If there is no title provided, use the plugin title, and if there are + // multiple displays, append the count. + $title = $plugin['title']; + if ($count > 1) { + $title .= ' ' . $count; + } + } + } + + $display_options = array( + 'display_plugin' => $plugin_id, + 'id' => $id, + 'display_title' => $title, + 'position' => NULL, + 'display_options' => array(), + ); + + // Add the display options to the view. + $this->display[$id] = $display_options; + return $id; + } + + /** + * Generates a display ID of a certain plugin type. + * + * @param string $plugin_id + * Which plugin should be used for the new display ID. + */ + protected function generateDisplayId($plugin_id) { + // 'default' is singular and is unique, so just go with 'default' + // for it. For all others, start counting. + if ($plugin_id == 'default') { + return 'default'; + } + // Initial ID. + $id = $plugin_id . '_1'; + $count = 1; + + // Loop through IDs based upon our style plugin name until + // we find one that is unused. + while (!empty($this->display[$id])) { + $id = $plugin_id . '_' . ++$count; + } + + return $id; + } + + /** + * Creates a new display and a display handler for it. + * + * @param string $plugin_id + * (optional) The plugin type from the Views plugin annotation. Defaults to + * 'page'. + * @param string $title + * (optional) The title of the display. Defaults to NULL. + * @param string $id + * (optional) The ID to use, e.g., 'default', 'page_1', 'block_2'. Defaults + * to NULL. + * + * @return Drupal\views\Plugin\views\display\DisplayPluginBase + * A reference to the new handler object. + */ + public function &newDisplay($plugin_id = 'page', $title = NULL, $id = NULL) { + $id = $this->addDisplay($plugin_id, $title, $id); + return $this->getExecutable()->newDisplay($id); + } + + /** + * Gets a list of displays included in the view. + * + * @return array + * An array of display types that this view includes. + */ + function getDisplaysList() { + $manager = drupal_container()->get('plugin.manager.views.display'); + $displays = array(); + foreach ($this->display as $display) { + $definition = $manager->getDefinition($display['display_plugin']); + if (!empty($definition['admin'])) { + $displays[$definition['admin']] = TRUE; + } + } + + ksort($displays); + return array_keys($displays); + } + + /** + * Gets a list of paths assigned to the view. + * + * @return array + * An array of paths for this view. + */ + public function getPaths() { + $all_paths = array(); + if (empty($this->display)) { + $all_paths[] = t('Edit this view to add a display.'); + } + else { + foreach ($this->display as $display) { + if (!empty($display['display_options']['path'])) { + $path = $display['display_options']['path']; + if ($this->isEnabled() && strpos($path, '%') === FALSE) { + $all_paths[] = l('/' . $path, $path); + } + else { + $all_paths[] = check_plain('/' . $path); + } + } + } + } + + return array_unique($all_paths); + } + +} diff --git a/core/modules/views/lib/Drupal/views/ViewStorageController.php b/core/modules/views/lib/Drupal/views/ViewStorageController.php new file mode 100644 index 0000000..007b9ff --- /dev/null +++ b/core/modules/views/lib/Drupal/views/ViewStorageController.php @@ -0,0 +1,116 @@ +<?php + +/** + * @file + * Definition of Drupal\views\ViewStorageController. + */ + +namespace Drupal\views; + +use Drupal\Core\Config\Entity\ConfigStorageController; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Component\Uuid\Uuid; + +/** + * Defines the storage controller class for ViewStorage entities. + */ +class ViewStorageController extends ConfigStorageController { + + /** + * Holds a UUID factory instance. + * + * @var Drupal\Component\Uuid\Uuid + */ + protected $uuidFactory = NULL; + + /** + * Overrides Drupal\config\ConfigStorageController::load(); + */ + public function load(array $ids = NULL) { + $entities = parent::load($ids); + + // Only return views for enabled modules. + return array_filter($entities, function ($entity) { + if (module_exists($entity->getModule())) { + return TRUE; + } + return FALSE; + }); + } + + /** + * Overrides Drupal\config\ConfigStorageController::attachLoad(); + */ + protected function attachLoad(&$queried_entities, $revision_id = FALSE) { + foreach ($queried_entities as $id => $entity) { + // Create a uuid if we don't have one. + if (empty($entity->{$this->uuidKey})) { + // Only get an instance of uuid once. + if (!isset($this->uuidFactory)) { + $this->uuidFactory = new Uuid(); + } + $entity->{$this->uuidKey} = $this->uuidFactory->generate(); + } + $this->attachDisplays($entity); + } + + parent::attachLoad($queried_entities, $revision_id); + } + + /** + * Overrides Drupal\config\ConfigStorageController::postSave(). + */ + public function postSave(EntityInterface $entity, $update) { + parent::postSave($entity, $update); + // Clear caches. + views_invalidate_cache(); + } + + /** + * Overrides Drupal\config\ConfigStorageController::create(). + */ + public function create(array $values) { + // If there is no information about displays available add at least the + // default display. + $values += array( + 'display' => array( + 'default' => array( + 'display_plugin' => 'default', + 'id' => 'default', + 'display_title' => 'Master', + ), + ) + ); + + $entity = parent::create($values); + + $this->attachDisplays($entity); + return $entity; + } + + /** + * Add defaults to the display options. + * + * @param Drupal\Core\Entity\EntityInterface $entity + */ + protected function attachDisplays(EntityInterface $entity) { + if (isset($entity->display) && is_array($entity->display)) { + $displays = array(); + + foreach ($entity->get('display') as $key => $options) { + $options += array( + 'display_options' => array(), + 'display_plugin' => NULL, + 'id' => NULL, + 'display_title' => '', + 'position' => NULL, + ); + // Add the defaults for the display. + $displays[$key] = $options; + } + + $entity->set('display', $displays); + } + } + +} diff --git a/core/modules/views/lib/Drupal/views/ViewStorageInterface.php b/core/modules/views/lib/Drupal/views/ViewStorageInterface.php new file mode 100644 index 0000000..b29fa03 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/ViewStorageInterface.php @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Definition of Drupal\views\ViewStorageInterface. + */ + +namespace Drupal\views; + +use Drupal\Core\Config\Entity\ConfigEntityInterface; + +/** + * Defines an interface for View storage classes. + */ +interface ViewStorageInterface extends ConfigEntityInterface { + + /** + * Sets the configuration entity status to enabled. + */ + public function enable(); + + /** + * Sets the configuration entity status to disabled. + */ + public function disable(); + + /** + * Returns whether the configuration entity is enabled. + * + * @return bool + */ + public function isEnabled(); + +} diff --git a/core/modules/views/lib/Drupal/views/ViewsBundle.php b/core/modules/views/lib/Drupal/views/ViewsBundle.php new file mode 100644 index 0000000..7a85ff0 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/ViewsBundle.php @@ -0,0 +1,37 @@ +<?php + +/** + * @file + * Definition of Drupal\views\ViewsBundle. + */ + +namespace Drupal\views; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; +use Drupal\views\ViewExecutable; + +/** + * Views dependency injection container. + */ +class ViewsBundle extends Bundle { + + /** + * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build(). + */ + public function build(ContainerBuilder $container) { + foreach (ViewExecutable::getPluginTypes() as $type) { + if ($type == 'join') { + $container->register('plugin.manager.views.join', 'Drupal\views\Plugin\Type\JoinManager'); + } + elseif ($type == 'wizard') { + $container->register('plugin.manager.views.wizard', 'Drupal\views\Plugin\Type\WizardManager'); + } + else { + $container->register("plugin.manager.views.$type", 'Drupal\views\Plugin\Type\PluginManager') + ->addArgument($type); + } + } + } + +} diff --git a/core/modules/views/lib/Views/search/ViewsSearchQuery.php b/core/modules/views/lib/Views/search/ViewsSearchQuery.php new file mode 100644 index 0000000..5b3508c --- /dev/null +++ b/core/modules/views/lib/Views/search/ViewsSearchQuery.php @@ -0,0 +1,84 @@ +<?php + +/** + * @file + * Definition of Views\search\ViewsSearchQuery. + */ + +namespace Views\search; + +/** + * Extends the core SearchQuery to be able to gets it's protected values. + */ +class ViewsSearchQuery extends SearchQuery { + + /** + * Returns the conditions property. + * + * @return array + */ + public function &conditions() { + return $this->conditions; + } + + /** + * Returns the words property. + * + * @return array + */ + public function words() { + return $this->words; + } + + /** + * Returns the simple property. + * + * @return bool + */ + public function simple() { + return $this->simple; + } + + /** + * Returns the matches property. + * + * @return int + */ + public function matches() { + return $this->matches; + } + + /** + * Executes and returns the protected parseSearchExpression method. + */ + public function publicParseSearchExpression() { + return $this->parseSearchExpression(); + } + + /** + * Replaces the original condition with a custom one from views recursively. + * + * @param string $search + * The searched value. + * @param string $replace + * The value which replaces the search value. + * @param Drupal\Core\Database\Query\Condition $condition + * The query condition in which the string is replaced. + */ + function condition_replace_string($search, $replace, &$condition) { + if ($condition['field'] instanceof DatabaseCondition) { + $conditions =& $condition['field']->conditions(); + foreach ($conditions as $key => &$subcondition) { + if (is_numeric($key)) { + // As conditions can have subconditions, for example db_or(), the + // function has to be called recursively. + $this->condition_replace_string($search, $replace, $subcondition); + } + } + } + else { + $condition['field'] = str_replace($search, $replace, $condition['field']); + } + } + +} diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_access_none.yml b/core/modules/views/tests/views_test_config/config/views.view.test_access_none.yml new file mode 100644 index 0000000..79019ef --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_access_none.yml @@ -0,0 +1,27 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_access_none +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_access_perm.yml b/core/modules/views/tests/views_test_config/config/views.view.test_access_perm.yml new file mode 100644 index 0000000..f509323 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_access_perm.yml @@ -0,0 +1,29 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + options: + perm: 'views_test_data test permission' + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_access_perm +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_access_role.yml b/core/modules/views/tests/views_test_config/config/views.view.test_access_role.yml new file mode 100644 index 0000000..3d2b4d9 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_access_role.yml @@ -0,0 +1,27 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: role + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_access_role +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_aggregate_count.yml b/core/modules/views/tests/views_test_config/config/views.view.test_aggregate_count.yml new file mode 100644 index 0000000..7a42247 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_aggregate_count.yml @@ -0,0 +1,57 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + arguments: + type: + default_action: summary + default_argument_type: fixed + field: type + id: type + summary: + format: default_summary + table: node + cache: + type: none + exposed_form: + type: basic + fields: + nid: + alter: + alter_text: '0' + ellipsis: '1' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '1' + empty_zero: '0' + field: title + hide_empty: '0' + id: nid + link_to_node: '0' + table: node + group_by: '1' + pager: + type: some + query: + options: + query_comment: '0' + type: views_query + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_aggregate_count +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_alias.yml b/core/modules/views/tests/views_test_config/config/views.view.test_alias.yml new file mode 100644 index 0000000..3755f24 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_alias.yml @@ -0,0 +1,88 @@ +api_version: '3.0' +base_table: users +core: 8.0-dev +description: '' +disabled: '0' +display: + default: + display_options: + access: + perm: 'access user profiles' + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + name: + alter: + absolute: '0' + alter_text: '0' + ellipsis: '0' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '0' + empty_zero: '0' + field: name + hide_empty: '0' + id: name + label: '' + link_to_user: '1' + overwrite_anonymous: '0' + table: users + filters: + uid_raw: + admin_label: '' + expose: + description: '' + identifier: '' + label: '' + multiple: '0' + operator: '' + operator_id: '0' + remember: '0' + remember_roles: + authenticated: authenticated + required: '0' + use_operator: '0' + exposed: '0' + field: uid_raw + group: '1' + group_info: + default_group: All + default_group_multiple: { } + description: '' + group_items: { } + identifier: '' + label: '' + multiple: '0' + optional: '1' + remember: '0' + widget: select + group_type: group + id: uid_raw + is_grouped: '0' + operator: '>' + relationship: none + table: users + value: + max: '' + min: '' + value: '1' + pager: + type: full + query: + type: views_query + row_plugin: fields + style_plugin: default + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_alias +module: views +name: test_alias +tag: default +uuid: 3bdfd3e6-15aa-4324-9005-5ad8b321d265 diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_argument_default_fixed.yml b/core/modules/views/tests/views_test_config/config/views.view.test_argument_default_fixed.yml new file mode 100644 index 0000000..061b209 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_argument_default_fixed.yml @@ -0,0 +1,56 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + arguments: + 'null': + default_action: default + default_argument_type: fixed + field: 'null' + id: 'null' + must_not_be: '0' + style_plugin: default_summary + table: views + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + alter_text: '0' + ellipsis: '1' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '1' + empty_zero: '0' + field: title + hide_empty: '0' + id: title + link_to_node: '0' + table: node + pager: + options: + id: '0' + items_per_page: '10' + offset: '0' + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_argument_default_fixed +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_comment_user_uid.yml b/core/modules/views/tests/views_test_config/config/views.view.test_comment_user_uid.yml new file mode 100644 index 0000000..4dca1cd --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_comment_user_uid.yml @@ -0,0 +1,48 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + arguments: + uid_touch: + default_argument_skip_url: '0' + default_argument_type: fixed + field: uid_touch + id: uid_touch + summary: + format: default_summary + number_of_records: '0' + summary_options: + items_per_page: '25' + table: node + cache: + type: none + exposed_form: + type: basic + fields: + nid: + field: nid + id: nid + table: node + pager: + type: full + query: + options: + query_comment: '0' + type: views_query + style: + type: default + row: + type: node + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_comment_user_uid +name: test_comment_user_uid +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_destroy.yml b/core/modules/views/tests/views_test_config/config/views.view.test_destroy.yml new file mode 100644 index 0000000..a28c6b1 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_destroy.yml @@ -0,0 +1,160 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '1' +display: + attachment_1: + display_options: + displays: + default: default + page_1: page_1 + pager: + type: some + display_plugin: attachment + display_title: Attachment + id: attachment_1 + position: '0' + attachment_2: + display_options: + displays: + default: default + page_1: page_1 + pager: + type: some + display_plugin: attachment + display_title: Attachment + id: attachment_2 + position: '0' + default: + display_options: + access: + type: none + arguments: + created_day: + default_argument_type: fixed + field: created_day + id: created_day + style_plugin: default_summary + table: node + created_fulldate: + default_argument_type: fixed + field: created_fulldate + id: created_fulldate + style_plugin: default_summary + table: node + created_month: + default_argument_type: fixed + field: created_month + id: created_month + style_plugin: default_summary + table: node + cache: + type: none + empty: + area: + empty: '0' + field: area + id: area + table: views + area_1: + empty: '0' + field: area + id: area_1 + table: views + exposed_form: + type: basic + fields: + created: + field: created + id: created + table: node + nid: + field: nid + id: nid + table: node + path: + field: path + id: path + table: node + filters: + nid: + field: nid + id: nid + table: node + status: + field: status + id: status + table: node + title: + field: title + id: title + table: node + footer: + area: + empty: '0' + field: area + id: area + table: views + area_1: + empty: '0' + field: area + id: area_1 + table: views + header: + area: + empty: '0' + field: area + id: area + table: views + area_1: + empty: '0' + field: area + id: area_1 + table: views + pager: + type: full + query: + type: views_query + relationships: + cid: + field: cid + id: cid + table: node + pid: + field: pid + id: pid + table: comment + relationship: cid + uid: + field: uid + id: uid + table: comment + relationship: cid + sorts: + last_comment_name: + field: last_comment_name + id: last_comment_name + table: node_comment_statistics + last_comment_timestamp: + field: last_comment_timestamp + id: last_comment_timestamp + table: node_comment_statistics + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test_destroy + display_plugin: page + display_title: Page + id: page_1 + position: '0' +human_name: '' +name: test_destroy +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_display.yml b/core/modules/views/tests/views_test_config/config/views.view.test_display.yml new file mode 100644 index 0000000..5c44bb2 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_display.yml @@ -0,0 +1,87 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '1' +display: + block: + display_options: + defaults: + pager: '0' + pager_options: '0' + row_options: '0' + row_plugin: '0' + style_options: '0' + style_plugin: '0' + field: + title: + link_to_node: '1' + pager: + options: + items_per_page: '5' + type: some + pager_options: { } + row_options: + build_mode: teaser + comments: '0' + links: '1' + row_plugin: fields + style_options: { } + style_plugin: default + display_plugin: block + display_title: Block + id: block + position: '2' + default: + display_options: + access: + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + title: + field: title + id: title + table: node + filters: + status: + field: status + group: '1' + id: status + table: node + value: '1' + pager: + options: + items_per_page: '10' + type: full + query: + type: views_query + row_options: + build_mode: teaser + comments: '0' + links: '1' + row_plugin: node + sorts: + created: + field: created + id: created + order: DESC + table: node + style_plugin: default + title: 'Test Display' + display_plugin: default + display_title: Master + id: default + position: '0' + page: + display_options: + path: test-display + display_plugin: page + display_title: Page + id: page + position: '1' +human_name: '' +name: test_display +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_executable_displays.yml b/core/modules/views/tests/views_test_config/config/views.view.test_executable_displays.yml new file mode 100644 index 0000000..8feb580 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_executable_displays.yml @@ -0,0 +1,24 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_plugin: default + display_title: Master + id: default + position: '0' + page: + display_plugin: page + display_title: Page + id: page + position: '1' + page_2: + display_plugin: page + display_title: Page 2 + id: page_2 + position: '2' +human_name: '' +name: test_executable_displays +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_exposed_form.yml b/core/modules/views/tests/views_test_config/config/views.view.test_exposed_form.yml new file mode 100644 index 0000000..736ba03 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_exposed_form.yml @@ -0,0 +1,27 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: test_exposed_form + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_exposed_form +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_feed_display.yml b/core/modules/views/tests/views_test_config/config/views.view.test_feed_display.yml new file mode 100644 index 0000000..3fd15c9 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_feed_display.yml @@ -0,0 +1,93 @@ +api_version: '3.0' +base_table: node +core: 8.0-dev +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + absolute: '0' + alter_text: '0' + ellipsis: '0' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '0' + empty_zero: '0' + field: title + hide_empty: '0' + id: title + label: '' + link_to_node: '1' + table: node + filters: + status: + expose: + operator: '0' + field: status + group: '1' + id: status + table: node + value: '1' + pager: + options: + items_per_page: '10' + type: full + query: + type: views_query + row: + options: + build_mode: teaser + comments: '0' + links: '1' + type: node + sorts: + created: + field: created + id: created + order: DESC + table: node + style: + type: default + title: test_feed_display + display_plugin: default + display_title: Master + id: default + position: '0' + feed: + display_options: + displays: + default: default + page: page + pager: + type: some + path: test-feed-display.xml + row: + type: node_rss + style: + type: rss + display_plugin: feed + display_title: Feed + id: feed + position: '0' + page: + display_options: + path: test-feed-display + display_plugin: page + display_title: Page + id: page + position: '0' +human_name: test_feed_display +module: views +name: test_feed_display +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_field_get_entity.yml b/core/modules/views/tests/views_test_config/config/views.view.test_field_get_entity.yml new file mode 100644 index 0000000..5ba2fc9 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_field_get_entity.yml @@ -0,0 +1,64 @@ +api_version: '3.0' +base_table: comment +core: 8.0-dev +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + cid: + field: cid + id: cid + table: comment + nid: + field: nid + id: nid + table: node + relationship: nid + uid: + field: uid + id: uid + table: users + relationship: uid + filter_groups: + groups: { } + operator: AND + filters: { } + pager: + type: full + query: + type: views_query + relationships: + nid: + field: nid + id: nid + required: '1' + table: comment + uid: + admin_label: '' + field: uid + group_type: group + id: uid + label: author + relationship: nid + required: '0' + table: node + sorts: { } + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_field_get_entity +name: test_field_get_entity +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_field_tokens.yml b/core/modules/views/tests/views_test_config/config/views.view.test_field_tokens.yml new file mode 100644 index 0000000..01431a6 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_field_tokens.yml @@ -0,0 +1,45 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + query: + type: views_query + fields: + name: + id: name + table: views_test_data + field: name + name_1: + id: name_1 + table: views_test_data + field: name + name_2: + id: name_2 + table: views_test_data + field: name + job: + id: job + table: views_test_data + field: job + style: + type: default + row: + type: fields + display_plugin: default + display_title: Defaults + id: default + position: '0' +name: test_field_tokens +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_field_type.yml b/core/modules/views/tests/views_test_config/config/views.view.test_field_type.yml new file mode 100644 index 0000000..c9172bc --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_field_type.yml @@ -0,0 +1,20 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + fields: + type: + field: type + id: type + table: node + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_field_type +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_filter_date_between.yml b/core/modules/views/tests/views_test_config/config/views.view.test_filter_date_between.yml new file mode 100644 index 0000000..71f4b2f --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_filter_date_between.yml @@ -0,0 +1,41 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + fields: + nid: + field: nid + id: nid + table: node + filters: + created: + field: created + id: created + table: node + pager: + type: full + query: + options: + query_comment: '0' + type: views_query + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_filter_date_between +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_filter_group_override.yml b/core/modules/views/tests/views_test_config/config/views.view.test_filter_group_override.yml new file mode 100644 index 0000000..379eff6 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_filter_group_override.yml @@ -0,0 +1,54 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + ellipsis: '0' + word_boundary: '0' + field: title + id: title + label: '' + table: node + filters: + status: + expose: + operator: '0' + field: status + group: '1' + id: status + table: node + value: '1' + pager: + type: full + query: + type: views_query + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test + display_plugin: page + display_title: Page + id: page_1 + position: '0' +human_name: test_filter_group_override +name: test_filter_group_override +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_filter_groups.yml b/core/modules/views/tests/views_test_config/config/views.view.test_filter_groups.yml new file mode 100644 index 0000000..d71c02e --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_filter_groups.yml @@ -0,0 +1,111 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + ellipsis: '0' + word_boundary: '0' + field: title + id: title + label: '' + table: node + filter_groups: + groups: + 1: AND + 2: AND + filters: + nid: + field: nid + group: '2' + id: nid + table: node + value: + value: '1' + nid_1: + field: nid + group: '2' + id: nid_1 + table: node + value: + value: '2' + status: + expose: + operator: '0' + field: status + group: '1' + id: status + table: node + value: '1' + pager: + options: + items_per_page: '10' + type: full + query: + type: views_query + sorts: + created: + field: created + id: created + order: DESC + table: node + title: test_filter_groups + style: + type: default + row: + type: node + display_plugin: default + display_title: Master + id: default + position: '0' + page: + display_options: + defaults: + filters: '0' + filter_groups: + groups: + 1: OR + 2: OR + operator: OR + filters: + nid: + field: nid + group: '2' + id: nid + table: node + value: + value: '1' + nid_1: + field: nid + group: '2' + id: nid_1 + table: node + value: + value: '2' + status: + expose: + operator: '0' + field: status + group: '1' + id: status + table: node + value: '1' + path: test-filter-groups + display_plugin: page + display_title: Page + id: page + position: '0' +human_name: test_filter_groups +name: test_filter_groups +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_filter_node_uid_revision.yml b/core/modules/views/tests/views_test_config/config/views.view.test_filter_node_uid_revision.yml new file mode 100644 index 0000000..69000ec --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_filter_node_uid_revision.yml @@ -0,0 +1,50 @@ +api_version: '3.0' +base_table: node +core: 8.0-dev +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + nid: + id: nid + table: node + field: nid + filter_groups: + groups: + 1: AND + operator: AND + filters: + uid_revision: + admin_label: '' + field: uid_revision + id: uid_revision + is_grouped: '0' + operator: in + relationship: none + table: node + value: + - '1' + sorts: { } + pager: + type: full + query: + type: views_query + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_filter_node_uid_revision +name: test_filter_node_uid_revision +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_glossary.yml b/core/modules/views/tests/views_test_config/config/views.view.test_glossary.yml new file mode 100644 index 0000000..33045e5 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_glossary.yml @@ -0,0 +1,48 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + arguments: + title: + default_argument_type: fixed + field: title + glossary: '1' + id: title + limit: '1' + summary: + format: default_summary + number_of_records: '0' + summary_options: + items_per_page: '25' + table: node + cache: + type: none + exposed_form: + type: basic + fields: + title: + field: title + id: title + label: '' + table: node + pager: + type: full + query: + type: views_query + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_glossary +name: test_glossary +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_group_by_count.yml b/core/modules/views/tests/views_test_config/config/views.view.test_group_by_count.yml new file mode 100644 index 0000000..7ea0ab8 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_group_by_count.yml @@ -0,0 +1,60 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + fields: + nid: + alter: + alter_text: '0' + ellipsis: '1' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '1' + empty_zero: '0' + field: nid + group_type: { } + hide_empty: '0' + id: nid + link_to_node: '0' + table: node + type: + alter: + alter_text: '0' + ellipsis: '1' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '1' + empty_zero: '0' + field: type + hide_empty: '0' + id: type + link_to_node: '0' + table: node + group_by: '1' + pager: + type: some + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_group_by_count +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_group_by_in_filters.yml b/core/modules/views/tests/views_test_config/config/views.view.test_group_by_in_filters.yml new file mode 100644 index 0000000..c494b09 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_group_by_in_filters.yml @@ -0,0 +1,53 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + fields: + type: + alter: + alter_text: '0' + ellipsis: '1' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '1' + empty_zero: '0' + field: type + hide_empty: '0' + id: type + link_to_node: '0' + table: node + filters: + nid: + field: nid + group_type: count + id: nid + operator: '>' + table: node + value: + value: '3' + group_by: '1' + pager: + type: some + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_group_by_in_filters +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_handler_relationships.yml b/core/modules/views/tests/views_test_config/config/views.view.test_handler_relationships.yml new file mode 100644 index 0000000..409fb2c --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_handler_relationships.yml @@ -0,0 +1,30 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + fields: + title: + id: title + table: node + field: title + relationships: + cid: + id: cid + table: node + field: cid + nid: + id: nid + table: comment + field: nid + relationship: cid + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_handler_relationships +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_node_revision_nid.yml b/core/modules/views/tests/views_test_config/config/views.view.test_node_revision_nid.yml new file mode 100644 index 0000000..cefedda --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_node_revision_nid.yml @@ -0,0 +1,36 @@ +name: test_node_revision_nid +base_table: node_revision +core: 8 +api_version: 3 +display: + default: + display_options: + relationships: + nid: + id: nid + table: node_revision + field: nid + required: TRUE + fields: + vid: + id: vid + table: node_revision + field: vid + nid_1: + id: nid_1 + table: node_revision + field: nid + nid: + id: nid + table: node + field: nid + relationship: nid + arguments: + nid: + id: nid: + table: node_revision + field: nid + display_plugin: default + display_title: Master + id: default + position: '0' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_node_revision_vid.yml b/core/modules/views/tests/views_test_config/config/views.view.test_node_revision_vid.yml new file mode 100644 index 0000000..b36f549 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_node_revision_vid.yml @@ -0,0 +1,36 @@ +name: test_node_revision_vid +base_table: node_revision +core: 8 +api_version: 3 +display: + default: + display_options: + relationships: + vid: + id: vid + table: node_revision + field: vid + required: TRUE + fields: + vid: + id: vid + table: node_revision + field: vid + nid_1: + id: nid_1 + table: node_revision + field: nid + nid: + id: nid + table: node + field: nid + relationship: vid + arguments: + nid: + id: nid: + table: node_revision + field: nid + display_plugin: default + display_title: Master + id: default + position: '0' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_pager_full.yml b/core/modules/views/tests/views_test_config/config/views.view.test_pager_full.yml new file mode 100644 index 0000000..08be354 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_pager_full.yml @@ -0,0 +1,31 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + options: + id: '0' + items_per_page: '5' + offset: '0' + type: full + style: + type: default + row: + type: node + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_pager_full +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_pager_none.yml b/core/modules/views/tests/views_test_config/config/views.view.test_pager_none.yml new file mode 100644 index 0000000..7b20316 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_pager_none.yml @@ -0,0 +1,27 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: none + style: + type: default + row: + type: node + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_pager_none +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_pager_some.yml b/core/modules/views/tests/views_test_config/config/views.view.test_pager_some.yml new file mode 100644 index 0000000..d9ecda5 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_pager_some.yml @@ -0,0 +1,30 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + options: + items_per_page: '5' + offset: '0' + type: some + style: + type: default + row: + type: node + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_pager_some +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_plugin_argument_default_current_user.yml b/core/modules/views/tests/views_test_config/config/views.view.test_plugin_argument_default_current_user.yml new file mode 100644 index 0000000..54180b6 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_plugin_argument_default_current_user.yml @@ -0,0 +1,56 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + arguments: + 'null': + default_action: default + default_argument_type: current_user + field: 'null' + id: 'null' + must_not_be: '0' + style_plugin: default_summary + table: views + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + alter_text: '0' + ellipsis: '1' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '1' + empty_zero: '0' + field: title + hide_empty: '0' + id: title + link_to_node: '0' + table: node + pager: + options: + id: '0' + items_per_page: '10' + offset: '0' + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_plugin_argument_default_current_user +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_store_pager_settings.yml b/core/modules/views/tests/views_test_config/config/views.view.test_store_pager_settings.yml new file mode 100644 index 0000000..0c4acfe --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_store_pager_settings.yml @@ -0,0 +1,27 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: none + style: + type: default + row: + type: node + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_store_pager_settings +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_taxonomy_node_term_data.yml b/core/modules/views/tests/views_test_config/config/views.view.test_taxonomy_node_term_data.yml new file mode 100644 index 0000000..2cecd63 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_taxonomy_node_term_data.yml @@ -0,0 +1,67 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + arguments: + tid: + default_argument_type: fixed + field: tid + id: tid + relationship: term_node_tid + summary: + format: default_summary + number_of_records: '0' + summary_options: + items_per_page: '25' + table: taxonomy_term_data + tid_1: + default_argument_type: fixed + field: tid + id: tid_1 + relationship: term_node_tid_1 + summary: + format: default_summary + number_of_records: '0' + summary_options: + items_per_page: '25' + table: taxonomy_term_data + cache: + type: none + exposed_form: + type: basic + pager: + type: full + query: + type: views_query + relationships: + term_node_tid: + field: term_node_tid + id: term_node_tid + label: 'Term #1' + table: node + vocabularies: + tags: '0' + term_node_tid_1: + field: term_node_tid + id: term_node_tid_1 + label: 'Term #2' + table: node + vocabularies: + tags: '0' + style: + type: default + row: + type: node + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_taxonomy_node_term_data +name: test_taxonomy_node_term_data +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_user_relationship.yml b/core/modules/views/tests/views_test_config/config/views.view.test_user_relationship.yml new file mode 100644 index 0000000..096fa16 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_user_relationship.yml @@ -0,0 +1,102 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + name: + alter: + absolute: '0' + alter_text: '0' + ellipsis: '1' + external: '0' + html: '0' + make_link: '0' + nl2br: '0' + replace_spaces: '0' + strip_tags: '0' + trim: '0' + trim_whitespace: '0' + word_boundary: '1' + element_default_classes: '1' + element_label_colon: '1' + empty_zero: '0' + field: name + hide_alter_empty: '0' + hide_empty: '0' + id: name + link_to_user: '1' + overwrite_anonymous: '0' + table: users + title: + alter: + absolute: '0' + alter_text: '0' + ellipsis: '0' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '0' + empty_zero: '0' + field: title + hide_empty: '0' + id: title + label: '' + link_to_node: '1' + table: node + uid: + alter: + absolute: '0' + alter_text: '0' + ellipsis: '1' + external: '0' + html: '0' + make_link: '0' + nl2br: '0' + replace_spaces: '0' + strip_tags: '0' + trim: '0' + trim_whitespace: '0' + word_boundary: '1' + element_default_classes: '1' + element_label_colon: '1' + empty_zero: '0' + field: uid + hide_alter_empty: '0' + hide_empty: '0' + id: uid + link_to_user: '1' + table: users + pager: + options: + items_per_page: '10' + type: full + query: + options: + query_comment: '0' + type: views_query + title: test_user_relationship + style: + type: default + row: + type: fields + options: + default_field_elements: '1' + hide_empty: '0' + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_user_relationship +name: test_user_relationship +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_view.yml b/core/modules/views/tests/views_test_config/config/views.view.test_view.yml new file mode 100644 index 0000000..57dd103 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_view.yml @@ -0,0 +1,48 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + defaults: + fields: '0' + pager: '0' + pager_options: '0' + sorts: '0' + fields: + age: + field: age + id: age + relationship: none + table: views_test_data + id: + field: id + id: id + relationship: none + table: views_test_data + name: + field: name + id: name + relationship: none + table: views_test_data + pager: + options: + offset: '0' + type: none + pager_options: { } + sorts: + id: + field: id + id: id + order: ASC + relationship: none + table: views_test_data + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_view +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_numeric.yml b/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_numeric.yml new file mode 100644 index 0000000..446aa63 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_numeric.yml @@ -0,0 +1,37 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + arguments: + 'null': + default_argument_type: fixed + field: 'null' + id: 'null' + must_not_be: '0' + style_plugin: default_summary + table: views + validate: + type: numeric + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_view_argument_validate_numeric +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_php.yml b/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_php.yml new file mode 100644 index 0000000..1b4dc28 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_php.yml @@ -0,0 +1,37 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + arguments: + 'null': + default_argument_type: fixed + field: 'null' + id: 'null' + must_not_be: '0' + style_plugin: default_summary + table: views + validate: + type: php + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_view_argument_validate_php +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_user.yml b/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_user.yml new file mode 100644 index 0000000..2750f43 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_view_argument_validate_user.yml @@ -0,0 +1,37 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + arguments: + 'null': + default_argument_type: fixed + field: 'null' + id: 'null' + must_not_be: '0' + style_plugin: default_summary + table: views + validate: + type: user + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_view_argument_validate_user +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_view_delete.yml b/core/modules/views/tests/views_test_config/config/views.view.test_view_delete.yml new file mode 100644 index 0000000..ca28db8 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_view_delete.yml @@ -0,0 +1,29 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: full + query: + type: views_query + style: + type: default + row: + type: fields + display_plugin: default + display_title: Defaults + id: default + position: '0' +human_name: test_view_delete +name: test_view_delete +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_view_fieldapi.yml b/core/modules/views/tests/views_test_config/config/views.view.test_view_fieldapi.yml new file mode 100644 index 0000000..fe2ecc0 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_view_fieldapi.yml @@ -0,0 +1,34 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: perm + fields: + nid: + field: nid + id: nid + table: node + cache: + type: none + exposed_form: + type: basic + pager: + type: full + query: + type: views_query + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_view_fieldapi +name: test_view_fieldapi +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_view_pager_full_zero_items_per_page.yml b/core/modules/views/tests/views_test_config/config/views.view.test_view_pager_full_zero_items_per_page.yml new file mode 100644 index 0000000..5b909ea --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_view_pager_full_zero_items_per_page.yml @@ -0,0 +1,47 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + alter_text: '0' + ellipsis: '1' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '1' + empty_zero: '0' + field: title + hide_empty: '0' + id: title + link_to_node: '0' + table: node + pager: + options: + id: '0' + items_per_page: '0' + offset: '0' + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_view_pager_full_zero_items_per_page +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_views_handler_field_user_name.yml b/core/modules/views/tests/views_test_config/config/views.view.test_views_handler_field_user_name.yml new file mode 100644 index 0000000..48522e0 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_views_handler_field_user_name.yml @@ -0,0 +1,50 @@ +api_version: '3.0' +base_table: users +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + fields: + name: + alter: + absolute: '0' + alter_text: '0' + ellipsis: '0' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '0' + empty_zero: '0' + field: name + hide_empty: '0' + id: name + label: '' + link_to_user: '1' + overwrite_anonymous: '0' + table: users + pager: + type: full + query: + options: + query_comment: '0' + type: views_query + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: test_views_handler_field_user_name +name: test_views_handler_field_user_name +tag: default diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_field.yml b/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_field.yml new file mode 100644 index 0000000..22642b5 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_field.yml @@ -0,0 +1,20 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + fields: + old_field_1: + field: old_field_1 + id: old_field_1 + table: views_test_data + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_views_move_to_field +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_handler.yml b/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_handler.yml new file mode 100644 index 0000000..d66c95d --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_handler.yml @@ -0,0 +1,25 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + fields: + old_field_2: + field: old_field_2 + id: old_field_2 + table: views_test_data + filters: + old_field_3: + field: old_field_3 + id: old_field_3 + table: views_test_data + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_views_move_to_handler +tag: '' diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_table.yml b/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_table.yml new file mode 100644 index 0000000..956a7f0 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_views_move_to_table.yml @@ -0,0 +1,20 @@ +api_version: '3.0' +base_table: views_old_table +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + fields: + id: + field: id + id: id + table: views_old_table + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_views_move_to_table +tag: '' diff --git a/core/modules/views/tests/views_test_config/views_test_config.info b/core/modules/views/tests/views_test_config/views_test_config.info new file mode 100644 index 0000000..6ab95c9 --- /dev/null +++ b/core/modules/views/tests/views_test_config/views_test_config.info @@ -0,0 +1,6 @@ +name = Views Test Config +description = Provides default views for tests. +package = Testing +version = VERSION +core = 8.x +hidden = TRUE diff --git a/core/modules/views/tests/views_test_config/views_test_config.module b/core/modules/views/tests/views_test_config/views_test_config.module new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/core/modules/views/tests/views_test_config/views_test_config.module @@ -0,0 +1 @@ +<?php diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_access_dynamic.yml b/core/modules/views/tests/views_test_data/config/views.view.test_access_dynamic.yml new file mode 100644 index 0000000..a78537d --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_access_dynamic.yml @@ -0,0 +1,34 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: test_dynamic + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test_access_dynamic + display_plugin: page + display_title: Page + id: page_1 + position: '0' +human_name: '' +name: test_access_dynamic +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_access_static.yml b/core/modules/views/tests/views_test_data/config/views.view.test_access_static.yml new file mode 100644 index 0000000..cd3a94c --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_access_static.yml @@ -0,0 +1,34 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: test_static + cache: + type: none + exposed_form: + type: basic + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test_access_static + display_plugin: page + display_title: Page + id: page_1 + position: '0' +human_name: '' +name: test_access_static +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_argument_default_current_user.yml b/core/modules/views/tests/views_test_data/config/views.view.test_argument_default_current_user.yml new file mode 100644 index 0000000..3cf754d --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_argument_default_current_user.yml @@ -0,0 +1,53 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + arguments: + uid: + default_action: default + field: uid + id: uid + table: node + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + alter_text: '0' + ellipsis: '1' + html: '0' + make_link: '0' + strip_tags: '0' + trim: '0' + word_boundary: '1' + empty_zero: '0' + field: title + hide_empty: '0' + id: title + link_to_node: '0' + table: node + pager: + options: + id: '0' + items_per_page: '10' + offset: '0' + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_argument_default_current_user +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_click_sort.yml b/core/modules/views/tests/views_test_data/config/views.view.test_click_sort.yml new file mode 100644 index 0000000..8aa2eb2 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_click_sort.yml @@ -0,0 +1,52 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + fields: + id: + id: id + table: views_test_data + field: id + name: + id: name + table: views_test_data + field: name + created: + id: created + table: views_test_data + field: created + display_options: + access: + type: none + cache: + type: none + style: + type: table + options: + info: + id: + sortable: 1 + default_sort_order: asc + name: + sortable: 1 + default_sort_order: desc + created: + sortable: 0 + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test_click_sort + display_plugin: page + display_title: Page + id: page_1 + position: '0' +human_name: { } +name: test_click_sort +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_example_area.yml b/core/modules/views/tests/views_test_data/config/views.view.test_example_area.yml new file mode 100644 index 0000000..2a8fb0b --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_example_area.yml @@ -0,0 +1,34 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + header: + test_example: + field: test_example + id: test_example + table: views + footer: + test_example: + field: test_example + id: test_example + table: views + empty: + test_example: + field: test_example + id: test_example + table: views + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: { } +name: test_example_area +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_exposed_admin_ui.yml b/core/modules/views/tests/views_test_data/config/views.view.test_exposed_admin_ui.yml new file mode 100644 index 0000000..d73dfb8 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_exposed_admin_ui.yml @@ -0,0 +1,53 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + options: + reset_button: '1' + type: basic + filters: + type: + expose: + label: 'Content: Type' + operator_id: type_op + use_operator: '1' + field: type + id: type + table: node + pager: + type: full + sorts: + created: + field: created + id: created + table: node + style: + type: default + row: + type: node + options: + comments: '0' + links: '1' + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test_exposed_admin_ui + display_plugin: page + display_title: Page + id: page_1 + position: '0' +human_name: '' +name: test_exposed_admin_ui +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_field_classes.yml b/core/modules/views/tests/views_test_data/config/views.view.test_field_classes.yml new file mode 100644 index 0000000..d2fe5b2 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_field_classes.yml @@ -0,0 +1,33 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + fields: + id: + id: id + table: views_test_data + field: id + style: + type: html_list + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test_field_classes + display_plugin: page + display_title: Page + id: page_1 + position: '0' +human_name: { } +name: test_field_classes +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_field_output.yml b/core/modules/views/tests/views_test_data/config/views.view.test_field_output.yml new file mode 100644 index 0000000..6d0d73f --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_field_output.yml @@ -0,0 +1,26 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + fields: + name: + id: name + table: views_test_data + field: name + style: + type: html_list + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_field_output +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_filter.yml b/core/modules/views/tests/views_test_data/config/views.view.test_filter.yml new file mode 100644 index 0000000..3f11e83 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_filter.yml @@ -0,0 +1,40 @@ +api_version: '3.0' +base_table: views_test_data +core: 8 +disabled: false +display: + default: + display_options: + access: + type: perm + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + ellipsis: '0' + word_boundary: '0' + field: name + id: name + label: '' + table: views_test_data + filters: + type: + field: name + id: test_filter + table: views_test_data + query: + type: views_query + use_more_always: '0' + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: 'Test filters' +name: test_filter diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_filter_in_operator_ui.yml b/core/modules/views/tests/views_test_data/config/views.view.test_filter_in_operator_ui.yml new file mode 100644 index 0000000..a68c790 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_filter_in_operator_ui.yml @@ -0,0 +1,39 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + filters: + type: + expose: + identifier: type + label: 'Content: Type' + operator_id: type_op + reduce: '0' + use_operator: '0' + exposed: '1' + field: type + id: type + table: node + pager: + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_filter_in_operator_ui +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_handler_test_access.yml b/core/modules/views/tests/views_test_data/config/views.view.test_handler_test_access.yml new file mode 100644 index 0000000..f7cbce5 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_handler_test_access.yml @@ -0,0 +1,47 @@ +api_version: '3.0' +base_table: views_test_data +core: 8 +display: + default: + display_options: + fields: + access_callback: + id: access_callback + table: views_test_data + field: access_callback + access_callback_arguments: + id: access_callback_arguments + table: views_test_data + field: access_callback_arguments + filters: + access_callback: + id: access_callback + table: views_test_data + field: access_callback + access_callback_arguments: + id: access_callback_arguments + table: views_test_data + field: access_callback_arguments + arguments: + access_callback: + id: access_callback + table: views_test_data + field: access_callback + access_callback_arguments: + id: access_callback_arguments + table: views_test_data + field: access_callback_arguments + sorts: + access_callback: + id: access_callback + table: views_test_data + field: access_callback + access_callback_arguments: + id: access_callback_arguments + table: views_test_data + field: access_callback_arguments + display_plugin: default + display_title: Master + id: default + position: '0' +name: test_handler_test_access diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_page_display.yml b/core/modules/views/tests/views_test_data/config/views.view.test_page_display.yml new file mode 100644 index 0000000..92ed6c7 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_page_display.yml @@ -0,0 +1,33 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test_page_display_403 + display_plugin: page + display_title: Page + id: page_1 + position: '0' + page_2: + display_options: + path: test_page_display_404 + display_plugin: page + display_title: Page + id: page_2 + position: '0' +human_name: '' +name: test_page_display +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_rename_reset_button.yml b/core/modules/views/tests/views_test_data/config/views.view.test_rename_reset_button.yml new file mode 100644 index 0000000..e03d666 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_rename_reset_button.yml @@ -0,0 +1,54 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + options: + reset_button: '1' + type: basic + filters: + type: + expose: + identifier: type + label: 'Content: Type' + operator_id: type_op + reduce: '0' + exposed: '1' + field: type + id: type + table: node + pager: + type: full + query: + options: + query_comment: '0' + type: views_query + style: + type: default + row: + type: node + options: + comments: '0' + links: '1' + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test_rename_reset_button + display_plugin: page + display_title: Page + id: page_1 + position: '0' +human_name: '' +name: test_rename_reset_button +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_style_mapping.yml b/core/modules/views/tests/views_test_data/config/views.view.test_style_mapping.yml new file mode 100644 index 0000000..30d4dd9 --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_style_mapping.yml @@ -0,0 +1,58 @@ +api_version: '3.0' +base_table: views_test_data +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + defaults: + fields: '0' + pager: '0' + pager_options: '0' + sorts: '0' + fields: + age: + field: age + id: age + relationship: none + table: views_test_data + job: + field: job + id: job + relationship: none + table: views_test_data + name: + field: name + id: name + relationship: none + table: views_test_data + pager: + options: + offset: '0' + type: none + pager_options: { } + sorts: + id: + field: id + id: id + order: ASC + relationship: none + table: views_test_data + style: + type: mapping_test + options: + mapping: + name_field: name + numeric_field: + age: age + title_field: name + toggle_numeric_field: '1' + toggle_title_field: '1' + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_style_mapping +tag: '' diff --git a/core/modules/views/tests/views_test_data/config/views.view.test_views_groupby_save.yml b/core/modules/views/tests/views_test_data/config/views.view.test_views_groupby_save.yml new file mode 100644 index 0000000..05f9b5c --- /dev/null +++ b/core/modules/views/tests/views_test_data/config/views.view.test_views_groupby_save.yml @@ -0,0 +1,27 @@ +api_version: '3.0' +base_table: node +core: '8' +description: '' +disabled: '0' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + pager: + type: none + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' +human_name: '' +name: test_views_groupby_save +tag: '' diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/access/DynamicTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/access/DynamicTest.php new file mode 100644 index 0000000..1704661 --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/access/DynamicTest.php @@ -0,0 +1,40 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\access\DynamicTest. + */ + +namespace Drupal\views_test_data\Plugin\views\access; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\Plugin\views\access\AccessPluginBase; + +/** + * Tests a dynamic access plugin. + * + * @Plugin( + * id = "test_dynamic", + * title = @Translation("Dynamic test access plugin."), + * help = @Translation("Provides a dynamic test access plugin.") + * ) + */ +class DynamicTest extends AccessPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['access'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function access($account) { + return !empty($this->options['access']) && isset($this->view->args[0]) && $this->view->args[0] == variable_get('test_dynamic_access_argument1', NULL) && isset($this->view->args[1]) && $this->view->args[1] == variable_get('test_dynamic_access_argument2', NULL); + } + + function get_access_callback() { + return array('views_test_data_test_dynamic_access_callback', array(!empty($options['access']), 1, 2)); + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/access/StaticTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/access/StaticTest.php new file mode 100644 index 0000000..5f5770c --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/access/StaticTest.php @@ -0,0 +1,40 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\access\StaticTest. + */ + +namespace Drupal\views_test_data\Plugin\views\access; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\Plugin\views\access\AccessPluginBase; + +/** + * Tests a static access plugin. + * + * @Plugin( + * id = "test_static", + * title = @Translation("Static test access plugin"), + * help = @Translation("Provides a static test access plugin.") + * ) + */ +class StaticTest extends AccessPluginBase { + + protected function defineOptions() { + $options = parent::defineOptions(); + $options['access'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + public function access($account) { + return !empty($this->options['access']); + } + + function get_access_callback() { + return array('views_test_data_test_static_access_callback', array(!empty($options['access']))); + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/area/TestExample.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/area/TestExample.php new file mode 100644 index 0000000..713c84f --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/area/TestExample.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\area\TestExample + */ + +namespace Drupal\views_test_data\Plugin\views\area; + +use Drupal\views\Plugin\views\area\AreaPluginBase; +use Drupal\Core\Annotation\Plugin; + +/** + * Test area plugin. + * + * @see Drupal\views\Tests\Handler\AreaTest + * + * @Plugin( + * id = "test_example" + * ) + */ +class TestExample extends AreaPluginBase { + + /** + * Overrides Drupal\views\Plugin\views\area\AreaPluginBase::option_definition(). + */ + public function defineOptions() { + $options = parent::defineOptions(); + $options['string'] = array('default' => ''); + + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\area\AreaPluginBase::render(). + */ + public function render($empty = FALSE) { + return $this->options['string']; + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/argument_default/ArgumentDefaultTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/argument_default/ArgumentDefaultTest.php new file mode 100644 index 0000000..c29fc8d --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/argument_default/ArgumentDefaultTest.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\argument_default\ArgumentDefaultTest. + */ + +namespace Drupal\views_test_data\Plugin\views\argument_default; + +use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Defines a argument default test plugin. + * + * @Plugin( + * id = "argument_default_test", + * title = @Translation("Argument default test") + * ) + */ +class ArgumentDefaultTest extends ArgumentDefaultPluginBase { + + /** + * Overrides Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase::defineOptions(). + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['value'] = array('default' => ''); + + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase::get_argument(). + */ + public function get_argument() { + return $this->options['value']; + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display/DisplayTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display/DisplayTest.php new file mode 100644 index 0000000..1cc7ef5 --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display/DisplayTest.php @@ -0,0 +1,134 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\display\DisplayTest. + */ + +namespace Drupal\views_test_data\Plugin\views\display; + +use Drupal\views\Plugin\views\display\DisplayPluginBase; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Defines a Display test plugin. + * + * @Plugin( + * id = "display_test", + * title = @Translation("Display test"), + * theme = "views_view", + * contextual_links_locations = {"view"} + * ) + */ +class DisplayTest extends DisplayPluginBase { + + /** + * Whether the display allows attachments. + * + * @var bool + */ + protected $usesAttachments = TRUE; + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::defineOptions(). + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['test_option'] = array('default' => ''); + + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::optionsSummaryv(). + */ + public function optionsSummary(&$categories, &$options) { + parent::optionsSummary($categories, $options); + + $categories['display_test'] = array( + 'title' => t('Display test settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -100, + ), + ); + + $test_option = $this->getOption('test_option') ?: t('Empty'); + + $options['test_option'] = array( + 'category' => 'display_test', + 'title' => t('Test option'), + 'value' => views_ui_truncate($test_option, 24), + ); + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::buildOptionsForm(). + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + switch ($form_state['section']) { + case 'test_option': + $form['#title'] .= t('Test option'); + $form['test_option'] = array( + '#type' => 'textfield', + '#description' => t('This is a textfield for test_option.'), + '#default_value' => $this->getOption('test_option'), + ); + break; + } + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::validateOptionsForm(). + */ + public function validateOptionsForm(&$form, &$form_state) { + parent::validateOptionsForm($form, $form_state); + watchdog('views', $form_state['values']['test_option']); + switch ($form_state['section']) { + case 'test_option': + if (!trim($form_state['values']['test_option'])) { + form_error($form['test_option'], t('You cannot have an empty option.')); + } + break; + } + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::submitOptionsForm(). + */ + public function submitOptionsForm(&$form, &$form_state) { + parent::submitOptionsForm($form, $form_state); + switch ($form_state['section']) { + case 'test_option': + $this->setOption('test_option', $form_state['values']['test_option']); + break; + } + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::execute(). + */ + public function execute() { + $this->view->build(); + + // Render the test option as the title before the view output. + $render = '<h1>' . filter_xss_admin($this->options['test_option']) . '</h1>'; + // And now render the view. + $render .= $this->view->render(); + + return $render; + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::preview(). + * + * Override so preview and execute are the same output. + */ + public function preview() { + return $this->execute(); + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display_extender/DisplayExtenderTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display_extender/DisplayExtenderTest.php new file mode 100644 index 0000000..63c9f6d --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display_extender/DisplayExtenderTest.php @@ -0,0 +1,111 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\display_extender\DisplayExtenderTest. + */ + +namespace Drupal\views_test_data\Plugin\views\display_extender; + +use Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Defines a display extender test plugin. + * + * @Plugin( + * id = "display_extender_test", + * title = @Translation("Display extender test") + * ) + */ +class DisplayExtenderTest extends DisplayExtenderPluginBase { + + /** + * Stores some state booleans to be sure a certain method got called. + * + * @var array + */ + public $testState; + + /** + * Overrides Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase::defineOptionsAlter(). + */ + public function defineOptionsAlter(&$options) { + $options['test_extender_test_option'] = array('default' => ''); + + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::optionsSummary(). + */ + public function optionsSummary(&$categories, &$options) { + parent::optionsSummary($categories, $options); + + $categories['display_extender_test'] = array( + 'title' => t('Display extender test settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -100, + ), + ); + + $test_option = $this->displayHandler->getOption('test_extender_test_option') ?: t('Empty'); + + $options['test_extender_test_option'] = array( + 'category' => 'display_extender_test', + 'title' => t('Test option'), + 'value' => views_ui_truncate($test_option, 24), + ); + } + + /** + * Overrides Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase::buildOptionsForm(). + */ + public function buildOptionsForm(&$form, &$form_state) { + switch ($form_state['section']) { + case 'test_extender_test_option': + $form['#title'] .= t('Test option'); + $form['test_extender_test_option'] = array( + '#type' => 'textfield', + '#description' => t('This is a textfield for test_option.'), + '#default_value' => $this->displayHandler->getOption('test_extender_test_option'), + ); + } + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayExtenderPluginBase::submitOptionsForm(). + */ + public function submitOptionsForm(&$form, &$form_state) { + parent::submitOptionsForm($form, $form_state); + switch ($form_state['section']) { + case 'test_extender_test_option': + $this->displayHandler->setOption('test_extender_test_option', $form_state['values']['test_extender_test_option']); + break; + } + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayExtenderPluginBase::defaultableSections(). + */ + public function defaultableSections(&$sections, $section = NULL) { + $sections['test_extender_test_option'] = array('test_extender_test_option'); + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayExtenderPluginBase::query(). + */ + public function query() { + $this->testState['query'] = TRUE; + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayExtenderPluginBase::pre_execute(). + */ + public function pre_execute() { + $this->testState['pre_execute'] = TRUE; + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display_extender/DisplayExtenderTest2.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display_extender/DisplayExtenderTest2.php new file mode 100644 index 0000000..aa83221 --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/display_extender/DisplayExtenderTest2.php @@ -0,0 +1,23 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\display_extender\DisplayExtenderTest2. + */ + +namespace Drupal\views_test_data\Plugin\views\display_extender; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Defines another display extender test plugin. + * + * @Plugin( + * id = "display_extender_test_2", + * title = @Translation("Display extender test number two") + * ) + */ +class DisplayExtenderTest2 extends DisplayExtenderTest { + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/field/FieldTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/field/FieldTest.php new file mode 100644 index 0000000..929d9a4 --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/field/FieldTest.php @@ -0,0 +1,74 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\field\FieldTest. + */ + +namespace Drupal\views_test_data\Plugin\views\field; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\Plugin\views\field\FieldPluginBase; + +/** + * @Plugin( + * id = "test_field", + * title = @Translation("Test field plugin"), + * help = @Translation("Provides a generic field test plugin.") + * ) + */ +class FieldTest extends FieldPluginBase { + + + /** + * A temporary stored test value for the test. + * + * @var string + */ + protected $testValue; + + /** + * Sets the testValue property. + * + * @param string $value + * The test value to set. + */ + public function setTestValue($value) { + $this->testValue = $value; + } + + /** + * Returns the testValue property. + * + * @return string + */ + public function getTestValue() { + return $this->testValue; + } + + /** + * Overrides Drupal\views\Plugin\views\field\FieldPluginBase::add_self_tokens(). + */ + function add_self_tokens(&$tokens, $item) { + $tokens['[test-token]'] = $this->getTestValue(); + } + + /** + * Overrides Drupal\views\Plugin\views\field\FieldPluginBase::render(). + */ + function render($values) { + return $this->sanitizeValue($this->getTestValue()); + } + + /** + * A mock function which allows to call placeholder from public. + * + * @return string + * The result of the placeholder method. + */ + public function getPlaceholder() { + return $this->placeholder(); + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/filter/FilterTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/filter/FilterTest.php new file mode 100644 index 0000000..fa92dc2 --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/filter/FilterTest.php @@ -0,0 +1,62 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\filter\FilterTest. + */ + +namespace Drupal\views_test_data\Plugin\views\filter; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\Plugin\views\filter\FilterPluginBase; + +/** + * @Plugin( + * id = "test_filter", + * title = @Translation("Test filter plugin"), + * help = @Translation("Provides a generic filter test plugin."), + * base = "node", + * type = "type" + * ) + */ +class FilterTest extends FilterPluginBase { + + /** + * Overrides Drupal\views\Plugin\views\row\RowPluginBase::defineOptions(). + * + * @return array + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['test_enable'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\row\RowPluginBase::buildOptionsForm(). + * + * @return array + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['test_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Controls whether the filter plugin should be active.'), + '#default_value' => $this->options['test_enable'], + ); + } + + /** + * Overrides Drupal\views\Plugin\views\filter\FilterPluginBase::query(). + */ + public function query() { + // Call the parent if this option is enabled. + if ($this->options['test_enable']) { + parent::query(); + } + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/join/JoinTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/join/JoinTest.php new file mode 100644 index 0000000..5d0cb73 --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/join/JoinTest.php @@ -0,0 +1,58 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\views\join\JoinTest. + */ + +namespace Drupal\views_test_data\Plugin\views\join; + +use Drupal\views\Plugin\views\join\JoinPluginBase; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Defines a join test plugin. + * + * @Plugin( + * id = "join_test", + * title = @Translation("Join test") + * ) + */ +class JoinTest extends JoinPluginBase { + /** + * A value which is used to build an additional join condition. + * + * @var int + */ + protected $joinValue; + + /** + * Returns the joinValue property. + * + * @return int + */ + public function getJoinValue() { + return $this->joinValue; + } + + /** + * Sets the joinValue property. + * + * @param int $join_value + */ + public function setJoinValue($join_value) { + $this->joinValue = $join_value; + } + + + /** + * Overrides Drupal\views\Plugin\views\join\JoinPluginBase::buildJoin(). + */ + public function buildJoin($select_query, $table, $view_query) { + // Add an additional hardcoded condition to the query. + $this->extra = 'node.uid = ' . $this->getJoinValue(); + parent::buildJoin($select_query, $table, $view_query); + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/query/QueryTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/query/QueryTest.php new file mode 100644 index 0000000..eab54df --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/query/QueryTest.php @@ -0,0 +1,127 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\query\QueryTest. + */ + +namespace Drupal\views_test_data\Plugin\views\query; + +use Drupal\views\Plugin\views\query\QueryPluginBase; +use Drupal\views\Plugin\views\join\JoinPluginBase; +use Drupal\views\ViewExecutable; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + +/** + * Defines a query test plugin. + * + * @Plugin( + * id = "query_test", + * title = @Translation("Query test") + * ) + */ +class QueryTest extends QueryPluginBase { + protected $conditions = array(); + protected $fields = array(); + protected $allItems = array(); + protected $orderBy = array(); + + /** + * Sets the allItems property. + * + * @param array $allItems + * An array of stdClasses. + */ + public function setAllItems($allItems) { + $this->allItems = $allItems; + } + + public function add_where($group, $field, $value = NULL, $operator = NULL) { + $this->conditions[] = array( + 'field' => $field, + 'value' => $value, + 'operator' => $operator + ); + + } + + public function add_field($table, $field, $alias = '', $params = array()) { + $this->fields[$field] = $field; + return $field; + } + + public function add_orderby($table, $field = NULL, $order = 'ASC', $alias = '', $params = array()) { + $this->orderBy = array('field' => $field, 'order' => $order); + } + + + public function ensure_table($table, $relationship = NULL, JoinPluginBase $join = NULL) { + // There is no concept of joins. + } + + /** + * Implements Drupal\views\Plugin\views\query\QueryPluginBase::build(). + * + * @param Drupal\views\ViewExecutable $view + */ + public function build(ViewExecutable $view) { + $this->view = $view; + // @todo Support pagers for know, a php based one would probably match. + // @todo You could add a string representatin of the query. + $this->view->build_info['query'] = ""; + $this->view->build_info['count_query'] = ""; +} + + /** + * Implements Drupal\views\Plugin\views\query\QueryPluginBase::execute(). + */ + public function execute(ViewExecutable $view) { + $result = array(); + foreach ($this->allItems as $element) { + // Run all conditions on the element, and add it to the result if they + // match. + $match = TRUE; + foreach ($this->conditions as $condition) { + $match &= $this->match($element, $condition); + } + if ($match) { + // If the query explicit defines fields to use, filter all others out. + // Filter out fields + if ($this->fields) { + $element = array_intersect_key($element, $this->fields); + } + $result[] = (object) $element; + } + } + $this->view->result = $result; + } + + /** + * Check a single condition for a single element. + * + * @param array $element + * The element which should be checked. + * @param array $condition + * An associative array containing: + * - field: The field to by, for example id. + * - value: The expected value of the element. + * - operator: The operator to compare the element value with the expected + * value. + * + * @return bool + * Returns whether the condition matches with the element. + */ + public function match($element, $condition) { + $value = $element[$condition['field']]; + switch ($condition['operator']) { + case '=': + return $value == $condition['value']; + case 'IN': + return in_array($value, $condition['value']); + } + return FALSE; + } + + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/row/RowTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/row/RowTest.php new file mode 100644 index 0000000..771ac8f --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/row/RowTest.php @@ -0,0 +1,85 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\row\RowTest. + */ + +namespace Drupal\views_test_data\Plugin\views\row; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\Plugin\views\row\RowPluginBase; + +/** + * Provides a general test row plugin. + * + * @ingroup views_row_plugins + * + * @Plugin( + * id = "test_row", + * title = @Translation("Test row plugin"), + * help = @Translation("Provides a generic row test plugin."), + * theme = "views_view_row_test", + * type = "normal" + * ) + */ +class RowTest extends RowPluginBase { + + /** + * A string which will be output when the view is rendered. + * + * @var string + */ + public $output; + + /** + * Overrides Drupal\views\Plugin\views\row\RowPluginBase::defineOptions(). + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['test_option'] = array('default' => ''); + + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\row\RowPluginBase::buildOptionsForm(). + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['test_option'] = array( + '#type' => 'textfield', + '#description' => t('This is a textfield for test_option.'), + '#default_value' => $this->options['test_option'], + ); + } + + /** + * Sets the output property. + * + * @param string $output + * The string to output by this plugin. + */ + public function setOutput($output) { + $this->output = $output; + } + + /** + * Returns the output property. + * + * @return string + */ + public function getOutput() { + return $this->output; + } + + /** + * Overrides Drupal\views\Plugin\views\row\RowPluginBase::render() + */ + public function render($row) { + return $this->getOutput(); + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/style/MappingTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/style/MappingTest.php new file mode 100644 index 0000000..2336d04 --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/style/MappingTest.php @@ -0,0 +1,70 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\style\MappingTest; + */ + +namespace Drupal\views_test_data\Plugin\views\style; + +use Drupal\views\Plugin\views\style\Mapping; +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\Plugin\views\field\Numeric; + +/** + * Provides a test plugin for the mapping style. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "mapping_test", + * title = @Translation("Field mapping"), + * help = @Translation("Maps specific fields to specific purposes."), + * theme = "views_view_mapping_test", + * type = "normal" + * ) + */ +class MappingTest extends Mapping { + + /** + * Overrides Drupal\views\Plugin\views\style\Mapping::defineMapping(). + */ + protected function defineMapping() { + return array( + 'title_field' => array( + '#title' => t('Title field'), + '#description' => t('Choose the field with the custom title.'), + '#toggle' => TRUE, + '#required' => TRUE, + ), + 'name_field' => array( + '#title' => t('Name field'), + '#description' => t('Choose the field with the custom name.'), + ), + 'numeric_field' => array( + '#title' => t('Numeric field'), + '#description' => t('Select one or more numeric fields.'), + '#multiple' => TRUE, + '#toggle' => TRUE, + '#filter' => 'filterNumericFields', + '#required' => TRUE, + ), + ); + } + + /** + * Restricts the allowed fields to only numeric fields. + * + * @param array $fields + * An array of field labels, keyed by the field ID. + */ + protected function filterNumericFields(&$fields) { + foreach ($this->view->field as $id => $field) { + if (!($field instanceof Numeric)) { + unset($fields[$id]); + } + } + } + +} diff --git a/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/style/StyleTest.php b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/style/StyleTest.php new file mode 100644 index 0000000..59dc396 --- /dev/null +++ b/core/modules/views/tests/views_test_data/lib/Drupal/views_test_data/Plugin/views/style/StyleTest.php @@ -0,0 +1,110 @@ +<?php + +/** + * @file + * Definition of Drupal\views_test_data\Plugin\views\style\StyleTest. + */ + +namespace Drupal\views_test_data\Plugin\views\style; + +use Drupal\Core\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; +use Drupal\views\Plugin\views\style\StylePluginBase; + +/** + * Provides a general test style plugin. + * + * @ingroup views_style_plugins + * + * @Plugin( + * id = "test_style", + * title = @Translation("Test style plugin"), + * help = @Translation("Provides a generic style test plugin."), + * theme = "views_view_style_test", + * type = "normal" + * ) + */ +class StyleTest extends StylePluginBase { + + /** + * A string which will be output when the view is rendered. + * + * @var string + */ + public $output; + + /** + * Overrides Drupal\views\Plugin\views\style\StylePluginBase::defineOptions(). + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['test_option'] = array('default' => ''); + + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\style\StylePluginBase::buildOptionsForm(). + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['test_option'] = array( + '#type' => 'textfield', + '#description' => t('This is a textfield for test_option.'), + '#default_value' => $this->options['test_option'], + ); + } + + function usesRowPlugin() { + return parent::usesRowPlugin(); + } + + /** + * Sets the usesRowPlugin property. + * + * @param bool $status + * TRUE if this style plugin should use rows. + */ + public function setUsesRowPlugin($status) { + $this->usesRowPlugin = $status; + } + + /** + * Sets the output property. + * + * @param string $output + * The string to output by this plugin. + */ + public function setOutput($output) { + $this->output = $output; + } + + /** + * Returns the output property. + * + * @return string + */ + public function getOutput() { + return $this->output; + } + + /** + * Overrides Drupal\views\Plugin\views\style\StylePluginBase::render() + */ + public function render() { + $output = ''; + if (!$this->usesRowPlugin()) { + $output = $this->getOutput(); + } + else { + foreach ($this->view->result as $index => $row) { + $this->view->row_index = $index; + $output .= $this->row_plugin->render($row) . "\n"; + } + } + + return $output; + } + +} diff --git a/core/modules/views/tests/views_test_data/templates/views-view--frontpage.tpl.php b/core/modules/views/tests/views_test_data/templates/views-view--frontpage.tpl.php new file mode 100644 index 0000000..5e6c8fa --- /dev/null +++ b/core/modules/views/tests/views_test_data/templates/views-view--frontpage.tpl.php @@ -0,0 +1,85 @@ +<?php + +/** + * @file + * Main view template. + * + * Variables available: + * - $attributes: An instance of Attributes class that can be manipulated as an + * array and printed as a string. + * It includes the 'class' information, which includes: + * .view + * .view-[css_name] + * .view-id-[view_name] + * .view-display-id-[display_name] + * .view-dom-id-[dom_id] + * - $css_name: A css-safe version of the view name. + * - $css_class: The user-specified classes names, if any + * - $header: The view header + * - $footer: The view footer + * - $rows: The results of the view query, if any + * - $empty: The empty text to display if the view is empty + * - $pager: The pager next/prev links to display, if any + * - $exposed: Exposed widget form/info to display + * - $feed_icon: Feed icon to display, if any + * - $more: A link to view more, if any + * + * @ingroup views_templates + */ +?> +<div <?php print $attributes; ?>> + <?php if ($header): ?> + <div class="view-header"> + <?php print $header; ?> + </div> + <?php endif; ?> + + <?php if ($exposed): ?> + <div class="view-filters"> + <?php print $exposed; ?> + </div> + <?php endif; ?> + + <?php if ($attachment_before): ?> + <div class="attachment attachment-before"> + <?php print $attachment_before; ?> + </div> + <?php endif; ?> + + <?php if ($rows): ?> + <div class="view-content"> + <?php print $rows; ?> + </div> + <?php elseif ($empty): ?> + <div class="view-empty"> + <?php print $empty; ?> + </div> + <?php endif; ?> + + <?php if ($pager): ?> + <?php print $pager; ?> + <?php endif; ?> + + <?php if ($attachment_after): ?> + <div class="attachment attachment-after"> + <?php print $attachment_after; ?> + </div> + <?php endif; ?> + + <?php if ($more): ?> + <?php print $more; ?> + <?php endif; ?> + + <?php if ($footer): ?> + <div class="view-footer"> + <?php print $footer; ?> + </div> + <?php endif; ?> + + <?php if ($feed_icon): ?> + <div class="feed-icon"> + <?php print $feed_icon; ?> + </div> + <?php endif; ?> + +</div> <?php /* class view */ ?> diff --git a/core/modules/views/tests/views_test_data/views_cache.test.css b/core/modules/views/tests/views_test_data/views_cache.test.css new file mode 100644 index 0000000..8dd17c1 --- /dev/null +++ b/core/modules/views/tests/views_test_data/views_cache.test.css @@ -0,0 +1,5 @@ +/** + * @file + * Just a placeholder file for the test. + * @see ViewsCacheTest::testHeaderStorage + */ diff --git a/core/modules/views/tests/views_test_data/views_cache.test.js b/core/modules/views/tests/views_test_data/views_cache.test.js new file mode 100644 index 0000000..8dd17c1 --- /dev/null +++ b/core/modules/views/tests/views_test_data/views_cache.test.js @@ -0,0 +1,5 @@ +/** + * @file + * Just a placeholder file for the test. + * @see ViewsCacheTest::testHeaderStorage + */ diff --git a/core/modules/views/tests/views_test_data/views_test_data.info b/core/modules/views/tests/views_test_data/views_test_data.info new file mode 100644 index 0000000..7d2ae10 --- /dev/null +++ b/core/modules/views/tests/views_test_data/views_test_data.info @@ -0,0 +1,7 @@ +name = Views Test +description = Test module for Views. +package = Testing +version = VERSION +core = 8.x +dependencies[] = views +hidden = TRUE diff --git a/core/modules/views/tests/views_test_data/views_test_data.install b/core/modules/views/tests/views_test_data/views_test_data.install new file mode 100644 index 0000000..bb13531 --- /dev/null +++ b/core/modules/views/tests/views_test_data/views_test_data.install @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Install, update, and uninstall functions for the Views Test module. + */ + +/** + * Implements hook_schema(). + */ +function views_test_data_schema() { + return variable_get('views_test_data_schema', array()); +} + +/** + * Implements hook_install(). + */ +function views_test_data_install() { + // Add the marquee tag to possible html elements to test the field handler. + $values = array( + 'div' => 'DIV', + 'span' => 'SPAN', + 'h1' => 'H1', + 'h2' => 'H2', + 'h3' => 'H3', + 'h4' => 'H4', + 'h5' => 'H5', + 'h6' => 'H6', + 'p' => 'P', + 'strong' => 'STRONG', + 'em' => 'EM', + 'marquee' => 'MARQUEE' + ); + config('views.settings')->set('field_rewrite_elements', $values)->save(); +} diff --git a/core/modules/views/tests/views_test_data/views_test_data.module b/core/modules/views/tests/views_test_data/views_test_data.module new file mode 100644 index 0000000..c4a28f9 --- /dev/null +++ b/core/modules/views/tests/views_test_data/views_test_data.module @@ -0,0 +1,194 @@ +<?php + +/** + * @file + * Helper module for Views tests. + */ + +use Drupal\views\ViewExecutable; + +/** + * Implements hook_permission(). + */ +function views_test_data_permission() { + return array( + 'views_test_data test permission' => array( + 'title' => t('Test permission'), + 'description' => t('views_test_data test permission'), + ), + ); +} + +/** + * Implements hook_views_api(). + */ +function views_test_data_views_api() { + return array( + 'api' => 3.0, + 'template path' => drupal_get_path('module', 'views_test_data') . '/templates', + ); +} + +/** + * Implements hook_views_data(). + */ +function views_test_data_views_data() { + return variable_get('views_test_data_views_data', array()); +} + +function views_test_data_test_static_access_callback($access) { + return $access; +} + +function views_test_data_test_dynamic_access_callback($access, $argument1, $argument2) { + return $access && $argument1 == variable_get('test_dynamic_access_argument1', NULL) && $argument2 == variable_get('test_dynamic_access_argument2', NULL); +} + +/**+ + * Access callback for the generic handler test. + * + * @return bool + * Returns views_test_data.tests->handler_access_callback config. so the user + * has access to the handler. + * + * @see Drupal\views\Tests\Handler\HandlerTest + */ +function views_test_data_handler_test_access_callback() { + return config('views_test_data.tests')->get('handler_access_callback'); +} + +/** + * Access callback with an argument for the generic handler test. + * + * @param bool $argument + * A parameter to test that an argument got passed. + * + * @return bool + * Returns views_test_data.tests->handler_access_callback_argument, so the + * use has access to the handler. + * + * @see Drupal\views\Tests\Handler\HandlerTest + */ +function views_test_data_handler_test_access_callback_argument($argument = FALSE) { + // Check the argument to be sure that access arguments are passed into the + // callback. + if ($argument) { + return config('views_test_data.tests')->get('handler_access_callback_argument'); + } + else { + return FALSE; + } +} + + +/** + * Implements hook_views_pre_render(). + */ +function views_test_data_views_pre_render(ViewExecutable $view) { + if ($view->storage->name == 'test_cache_header_storage') { + drupal_add_js(drupal_get_path('module', 'views_test_data') . '/views_cache.test.js'); + drupal_add_css(drupal_get_path('module', 'views_test_data') . '/views_cache.test.css'); + $view->build_info['pre_render_called'] = TRUE; + } +} + +/** + * Implements hook_views_post_build(). + */ +function views_test_data_views_post_build(ViewExecutable $view) { + if ($view->storage->name == 'test_page_display') { + if ($view->current_display == 'page_1') { + $view->build_info['denied'] = TRUE; + } + elseif ($view->current_display == 'page_2') { + $view->build_info['fail'] = TRUE; + } + } +} + +/** + * Implements hook_preprocess_HOOK() for theme_views_view_mapping_test(). + */ +function template_preprocess_views_view_mapping_test(&$variables) { + $variables['element'] = array(); + + foreach ($variables['rows'] as $delta => $row) { + $fields = array(); + foreach ($variables['options']['mapping'] as $type => $field_names) { + if (!is_array($field_names)) { + $field_names = array($field_names); + } + foreach ($field_names as $field_name) { + if ($value = $variables['view']->style_plugin->get_field($delta, $field_name)) { + $fields[$type . '-' . $field_name] = $type . ':' . $value; + } + } + } + + // If there are no fields in this row, skip to the next one. + if (empty($fields)) { + continue; + } + + // Build a container for the row. + $variables['element'][$delta] = array( + '#type' => 'container', + '#attributes' => array( + 'class' => array( + 'views-row-mapping-test', + ), + ), + ); + + // Add each field to the row. + foreach ($fields as $key => $render) { + $variables['element'][$delta][$key] = array( + '#children' => $render, + '#type' => 'container', + '#attributes' => array( + 'class' => array( + $key, + ), + ), + ); + } + } +} + +/** + * Returns HTML for the Mapping Test style. + */ +function theme_views_view_mapping_test($variables) { + return drupal_render($variables['element']); +} + +/** + * Implements hook_menu(). + */ +function views_test_data_menu() { + $items = array(); + + $items['views_test_data_element_form'] = array( + 'title' => 'Views test data element form', + 'description' => 'Views test data element form callback', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('views_test_data_element_form'), + 'access callback' => TRUE, + ); + + return $items; +} + +/** + * Simple form page callback to test the view element. + */ +function views_test_data_element_form() { + $form['view'] = array( + '#type' => 'view', + '#name' => 'test_view', + '#display_id' => 'default', + '#arguments' => array(25), + ); + + return $form; +} diff --git a/core/modules/views/theme/theme.inc b/core/modules/views/theme/theme.inc new file mode 100644 index 0000000..4203074 --- /dev/null +++ b/core/modules/views/theme/theme.inc @@ -0,0 +1,1161 @@ +<?php + +/** + * @file + * Preprocessors and helper functions to make theming easier. + */ + +use Drupal\Core\Template\Attribute; +use Drupal\views\ViewExecutable; + +/** + * Provide a full array of possible themes to try for a given hook. + * + * @param $hook + * The hook to use. This is the base theme/template name. + * @param $view + * The view being rendered. + * @param $display + * The display being rendered, if applicable. + */ +function _views_theme_functions($hook, ViewExecutable $view, $display = NULL) { + $themes = array(); + + if ($display) { + $themes[] = $hook . '__' . $view->storage->name . '__' . $display['id']; + $themes[] = $hook . '__' . $display['id']; + // Add theme suggestions for each single tag. + foreach (drupal_explode_tags($view->storage->tag) as $tag) { + $themes[] = $hook . '__' . preg_replace('/[^a-z0-9]/', '_', strtolower($tag)); + } + + if ($display['id'] != $display['display_plugin']) { + $themes[] = $hook . '__' . $view->storage->name . '__' . $display['display_plugin']; + $themes[] = $hook . '__' . $display['display_plugin']; + } + } + $themes[] = $hook . '__' . $view->storage->name; + $themes[] = $hook; + return $themes; +} + +/** + * Preprocess the primary theme implementation for a view. + */ +function template_preprocess_views_view(&$vars) { + global $base_path; + + $view = $vars['view']; + + $vars['rows'] = (!empty($view->result) || $view->style_plugin->even_empty()) ? $view->style_plugin->render($view->result) : ''; + + $vars['css_name'] = drupal_clean_css_identifier($view->storage->name); + $vars['name'] = $view->storage->name; + $vars['display_id'] = $view->current_display; + + // Basic classes + $vars['css_class'] = ''; + + $vars['attributes']['class'] = array(); + $vars['attributes']['class'][] = 'view'; + $vars['attributes']['class'][] = 'view-' . drupal_clean_css_identifier($vars['name']); + $vars['attributes']['class'][] = 'view-id-' . $vars['name']; + $vars['attributes']['class'][] = 'view-display-id-' . $vars['display_id']; + + $css_class = $view->display_handler->getOption('css_class'); + if (!empty($css_class)) { + $vars['css_class'] = preg_replace('/[^a-zA-Z0-9- ]/', '-', $css_class); + $vars['attributes']['class'][] = $vars['css_class']; + } + + $empty = empty($vars['rows']); + + $vars['header'] = $view->display_handler->renderArea('header', $empty); + $vars['footer'] = $view->display_handler->renderArea('footer', $empty); + if ($empty) { + $vars['empty'] = $view->display_handler->renderArea('empty', $empty); + } + + $vars['exposed'] = !empty($view->exposed_widgets) ? $view->exposed_widgets : ''; + $vars['more'] = $view->display_handler->renderMoreLink(); + $vars['feed_icon'] = !empty($view->feed_icon) ? $view->feed_icon : ''; + + $vars['pager'] = ''; + + // @todo: Figure out whether this belongs into views_ui_preprocess_views_view. + // Render title for the admin preview. + $vars['title'] = !empty($view->views_ui_context) ? filter_xss_admin($view->getTitle()) : ''; + + if ($view->display_handler->renderPager()) { + $exposed_input = isset($view->exposed_raw_input) ? $view->exposed_raw_input : NULL; + $vars['pager'] = $view->renderPager($exposed_input); + } + + $vars['attachment_before'] = !empty($view->attachment_before) ? $view->attachment_before : ''; + $vars['attachment_after'] = !empty($view->attachment_after) ? $view->attachment_after : ''; + + // Add contextual links to the view. We need to attach them to the dummy + // $view_array variable, since contextual_preprocess() requires that they be + // attached to an array (not an object) in order to process them. For our + // purposes, it doesn't matter what we attach them to, since once they are + // processed by contextual_preprocess() they will appear in the $title_suffix + // variable (which we will then render in views-view.tpl.php). + views_add_contextual_links($vars['view_array'], 'view', $view, $view->current_display); + + // Attachments are always updated with the outer view, never by themselves, + // so they do not have dom ids. + if (empty($view->is_attachment)) { + // Our JavaScript needs to have some means to find the HTML belonging to this + // view. + // + // It is true that the DIV wrapper has classes denoting the name of the view + // and its display ID, but this is not enough to unequivocally match a view + // with its HTML, because one view may appear several times on the page. So + // we set up a hash with the current time, $dom_id, to issue a "unique" identifier for + // each view. This identifier is written to both Drupal.settings and the DIV + // wrapper. + $vars['dom_id'] = $view->dom_id; + $vars['attributes']['class'][] = 'view-dom-id-' . $vars['dom_id']; + } + + // If using AJAX, send identifying data about this view. + if ($view->use_ajax && empty($view->is_attachment) && empty($view->live_preview)) { + $settings = array( + 'views' => array( + 'ajax_path' => url('views/ajax'), + 'ajaxViews' => array( + 'views_dom_id:' . $vars['dom_id'] => array( + 'view_name' => $view->storage->name, + 'view_display_id' => $view->current_display, + 'view_args' => check_plain(implode('/', $view->args)), + 'view_path' => check_plain(current_path()), + // Pass through URL to ensure we get e.g. language prefixes. +// 'view_base_path' => isset($view->display['page']) ? substr(url($view->display['page']['display_options']['path']), strlen($base_path)) : '', + 'view_base_path' => $view->getPath(), + 'view_dom_id' => $vars['dom_id'], + // To fit multiple views on a page, the programmer may have + // overridden the display's pager_element. + 'pager_element' => isset($view->pager) ? $view->pager->get_pager_id() : 0, + ), + ), + ), + ); + + drupal_add_js($settings, 'setting'); + views_add_js('ajax_view'); + } + + // If form fields were found in the View, reformat the View output as a form. + if (views_view_has_form_elements($view)) { + $output = !empty($vars['rows']) ? $vars['rows'] : $vars['empty']; + $form = drupal_get_form(views_form_id($view), $view, $output); + // The form is requesting that all non-essential views elements be hidden, + // usually because the rendered step is not a view result. + if ($form['show_view_elements']['#value'] == FALSE) { + $vars['header'] = ''; + $vars['exposed'] = ''; + $vars['pager'] = ''; + $vars['footer'] = ''; + $vars['more'] = ''; + $vars['feed_icon'] = ''; + } + $vars['rows'] = $form; + } +} + +/** + * Process function to render certain elements into the view. + */ +function template_process_views_view(&$vars) { + if (is_array($vars['rows'])) { + $vars['rows'] = drupal_render($vars['rows']); + } +} + +/** + * Preprocess theme function to print a single record from a row, with fields + */ +function template_preprocess_views_view_fields(&$vars) { + $view = $vars['view']; + + // Loop through the fields for this view. + $previous_inline = FALSE; + $vars['fields'] = array(); // ensure it's at least an empty array. + foreach ($view->field as $id => $field) { + // render this even if set to exclude so it can be used elsewhere. + $field_output = $view->style_plugin->get_field($view->row_index, $id); + $empty = $field->is_value_empty($field_output, $field->options['empty_zero']); + if (empty($field->options['exclude']) && (!$empty || (empty($field->options['hide_empty']) && empty($vars['options']['hide_empty'])))) { + $object = new stdClass(); + $object->handler = &$view->field[$id]; + $object->inline = !empty($vars['options']['inline'][$id]); + + $object->element_type = $object->handler->element_type(TRUE, !$vars['options']['default_field_elements'], $object->inline); + if ($object->element_type) { + $class = ''; + if ($object->handler->options['element_default_classes']) { + $class = 'field-content'; + } + + if ($classes = $object->handler->element_classes($view->row_index)) { + if ($class) { + $class .= ' '; + } + $class .= $classes; + } + + $pre = '<' . $object->element_type; + if ($class) { + $pre .= ' class="' . $class . '"'; + } + $field_output = $pre . '>' . $field_output . '</' . $object->element_type . '>'; + } + + // Protect ourself somewhat for backward compatibility. This will prevent + // old templates from producing invalid HTML when no element type is selected. + if (empty($object->element_type)) { + $object->element_type = 'span'; + } + + $object->content = $field_output; + if (isset($view->field[$id]->field_alias) && isset($vars['row']->{$view->field[$id]->field_alias})) { + $object->raw = $vars['row']->{$view->field[$id]->field_alias}; + } + else { + $object->raw = NULL; // make sure it exists to reduce NOTICE + } + + if (!empty($vars['options']['separator']) && $previous_inline && $object->inline && $object->content) { + $object->separator = filter_xss_admin($vars['options']['separator']); + } + + $object->class = drupal_clean_css_identifier($id); + + $previous_inline = $object->inline; + $object->inline_html = $object->handler->element_wrapper_type(TRUE, TRUE); + if ($object->inline_html === '' && $vars['options']['default_field_elements']) { + $object->inline_html = $object->inline ? 'span' : 'div'; + } + + // Set up the wrapper HTML. + $object->wrapper_prefix = ''; + $object->wrapper_suffix = ''; + + if ($object->inline_html) { + $class = ''; + if ($object->handler->options['element_default_classes']) { + $class = "views-field views-field-" . $object->class; + } + + if ($classes = $object->handler->element_wrapper_classes($view->row_index)) { + if ($class) { + $class .= ' '; + } + $class .= $classes; + } + + $object->wrapper_prefix = '<' . $object->inline_html; + if ($class) { + $object->wrapper_prefix .= ' class="' . $class . '"'; + } + $object->wrapper_prefix .= '>'; + $object->wrapper_suffix = '</' . $object->inline_html . '>'; + } + + // Set up the label for the value and the HTML to make it easier + // on the template. + $object->label = check_plain($view->field[$id]->label()); + $object->label_html = ''; + if ($object->label) { + $object->label_html .= $object->label; + if ($object->handler->options['element_label_colon']) { + $object->label_html .= ': '; + } + + $object->element_label_type = $object->handler->element_label_type(TRUE, !$vars['options']['default_field_elements']); + if ($object->element_label_type) { + $class = ''; + if ($object->handler->options['element_default_classes']) { + $class = 'views-label views-label-' . $object->class; + } + + $element_label_class = $object->handler->element_label_classes($view->row_index); + if ($element_label_class) { + if ($class) { + $class .= ' '; + } + + $class .= $element_label_class; + } + + $pre = '<' . $object->element_label_type; + if ($class) { + $pre .= ' class="' . $class . '"'; + } + $pre .= '>'; + + $object->label_html = $pre . $object->label_html . '</' . $object->element_label_type . '>'; + } + } + + $vars['fields'][$id] = $object; + } + } + +} + +/** + * Display a single views grouping. + */ +function theme_views_view_grouping($vars) { + $view = $vars['view']; + $title = $vars['title']; + $content = $vars['content']; + + $output = '<div class="view-grouping">'; + $output .= '<div class="view-grouping-header">' . $title . '</div>'; + $output .= '<div class="view-grouping-content">' . $content . '</div>' ; + $output .= '</div>'; + + return $output; +} + +/** + * Process a single grouping within a view. + */ +function template_preprocess_views_view_grouping(&$vars) { + $vars['content'] = $vars['view']->style_plugin->render_grouping_sets($vars['rows'], $vars['grouping_level']); +} + +/** + * Display a single views field. + * + * Interesting bits of info: + * $field->field_alias says what the raw value in $row will be. Reach it like + * this: @code { $row->{$field->field_alias} @endcode + */ +function theme_views_view_field($vars) { + $view = $vars['view']; + $field = $vars['field']; + $row = $vars['row']; + return $vars['output']; +} + +/** + * Process a single field within a view. + * + * This preprocess function isn't normally run, as a function is used by + * default, for performance. However, by creating a template, this + * preprocess should get picked up. + */ +function template_preprocess_views_view_field(&$vars) { + $vars['output'] = $vars['field']->advanced_render($vars['row']); +} + +/** + * Preprocess theme function to print a single record from a row, with fields + */ +function template_preprocess_views_view_summary(&$vars) { + $view = $vars['view']; + $argument = $view->argument[$view->build_info['summary_level']]; + $vars['row_classes'] = array(); + + $url_options = array(); + + if (!empty($view->exposed_raw_input)) { + $url_options['query'] = $view->exposed_raw_input; + } + + $active_urls = drupal_map_assoc(array( + url(current_path(), array('alias' => TRUE)), // force system path + url(current_path()), // could be an alias + )); + + // Collect all arguments foreach row, to be able to alter them for example by the validator. + // This is not done per single argument value, because this could cause performance problems. + $row_args = array(); + + foreach ($vars['rows'] as $id => $row) { + $row_args[$id] = $argument->summary_argument($row); + } + $argument->process_summary_arguments($row_args); + + foreach ($vars['rows'] as $id => $row) { + $vars['row_classes'][$id] = ''; + + $vars['rows'][$id]->link = $argument->summary_name($row); + $args = $view->args; + $args[$argument->position] = $row_args[$id]; + + $base_path = NULL; + if (!empty($argument->options['summary_options']['base_path'])) { + $base_path = $argument->options['summary_options']['base_path']; + } + $vars['rows'][$id]->url = url($view->getUrl($args, $base_path), $url_options); + $vars['rows'][$id]->count = intval($row->{$argument->count_alias}); + if (isset($active_urls[$vars['rows'][$id]->url])) { + $vars['row_classes'][$id] = 'active'; + } + } +} + +/** + * Template preprocess theme function to print summary basically + * unformatted. + */ +function template_preprocess_views_view_summary_unformatted(&$vars) { + $view = $vars['view']; + $argument = $view->argument[$view->build_info['summary_level']]; + $vars['row_classes'] = array(); + + $url_options = array(); + + if (!empty($view->exposed_raw_input)) { + $url_options['query'] = $view->exposed_raw_input; + } + + $count = 0; + $active_urls = drupal_map_assoc(array( + url(current_path(), array('alias' => TRUE)), // force system path + url(current_path()), // could be an alias + )); + + // Collect all arguments foreach row, to be able to alter them for example by the validator. + // This is not done per single argument value, because this could cause performance problems. + $row_args = array(); + foreach ($vars['rows'] as $id => $row) { + $row_args[$id] = $argument->summary_argument($row); + } + $argument->process_summary_arguments($row_args); + + foreach ($vars['rows'] as $id => $row) { + // only false on first time: + if ($count++) { + $vars['rows'][$id]->separator = filter_xss_admin($vars['options']['separator']); + } + $vars['rows'][$id]->link = $argument->summary_name($row); + $args = $view->args; + $args[$argument->position] = $row_args[$id]; + + $base_path = NULL; + if (!empty($argument->options['summary_options']['base_path'])) { + $base_path = $argument->options['summary_options']['base_path']; + } + $vars['rows'][$id]->url = url($view->getUrl($args, $base_path), $url_options); + $vars['rows'][$id]->count = intval($row->{$argument->count_alias}); + if (isset($active_urls[$vars['rows'][$id]->url])) { + $vars['row_classes'][$id] = 'active'; + } + } +} + +/** + * Display a view as a table style. + */ +function template_preprocess_views_view_table(&$vars) { + $view = $vars['view']; + + // We need the raw data for this grouping, which is passed in as $vars['rows']. + // However, the template also needs to use for the rendered fields. We + // therefore swap the raw data out to a new variable and reset $vars['rows'] + // so that it can get rebuilt. + // Store rows so that they may be used by further preprocess functions. + $result = $vars['result'] = $vars['rows']; + $vars['rows'] = array(); + $vars['field_classes'] = array(); + $vars['header'] = array(); + + $options = $view->style_plugin->options; + $handler = $view->style_plugin; + + $default_row_class = isset($options['default_row_class']) ? $options['default_row_class'] : TRUE; + $row_class_special = isset($options['row_class_special']) ? $options['row_class_special'] : TRUE; + + $fields = &$view->field; + $columns = $handler->sanitize_columns($options['columns'], $fields); + + $active = !empty($handler->active) ? $handler->active : ''; + $order = !empty($handler->order) ? $handler->order : 'asc'; + + // A boolean variable which stores whether the table has a responsive class. + $responsive = FALSE; + + $query = tablesort_get_query_parameters(); + if (isset($view->exposed_raw_input)) { + $query += $view->exposed_raw_input; + } + + // Fields must be rendered in order as of Views 2.3, so we will pre-render + // everything. + $renders = $handler->render_fields($result); + + foreach ($columns as $field => $column) { + // Create a second variable so we can easily find what fields we have and what the + // CSS classes should be. + $vars['fields'][$field] = drupal_clean_css_identifier($field); + if ($active == $field) { + $vars['fields'][$field] .= ' active'; + } + + // render the header labels + if ($field == $column && empty($fields[$field]->options['exclude'])) { + $label = check_plain(!empty($fields[$field]) ? $fields[$field]->label() : ''); + if (empty($options['info'][$field]['sortable']) || !$fields[$field]->click_sortable()) { + $vars['header'][$field] = $label; + } + else { + $initial = !empty($options['info'][$field]['default_sort_order']) ? $options['info'][$field]['default_sort_order'] : 'asc'; + + if ($active == $field) { + $initial = ($order == 'asc') ? 'desc' : 'asc'; + } + + $title = t('sort by @s', array('@s' => $label)); + if ($active == $field) { + $label .= theme('tablesort_indicator', array('style' => $initial)); + } + + $query['order'] = $field; + $query['sort'] = $initial; + $link_options = array( + 'html' => TRUE, + 'attributes' => array('title' => $title), + 'query' => $query, + ); + $vars['header'][$field] = l($label, current_path(), $link_options); + } + + $vars['header_classes'][$field] = ''; + // Set up the header label class. + if ($fields[$field]->options['element_default_classes']) { + $vars['header_classes'][$field] .= "views-field views-field-" . $vars['fields'][$field]; + } + $class = $fields[$field]->element_label_classes(0); + if ($class) { + if ($vars['header_classes'][$field]) { + $vars['header_classes'][$field] .= ' '; + } + $vars['header_classes'][$field] .= $class; + } + // Add responsive header classes. + if (!empty($options['info'][$field]['responsive'])) { + $vars['header_classes'][$field] .= ' ' . $options['info'][$field]['responsive']; + $responsive = TRUE; + } + // Add a CSS align class to each field if one was set + if (!empty($options['info'][$field]['align'])) { + $vars['header_classes'][$field] .= ' ' . drupal_clean_css_identifier($options['info'][$field]['align']); + } + + // Add a header label wrapper if one was selected. + if ($vars['header'][$field]) { + $element_label_type = $fields[$field]->element_label_type(TRUE, TRUE); + if ($element_label_type) { + $vars['header'][$field] = '<' . $element_label_type . '>' . $vars['header'][$field] . '</' . $element_label_type . '>'; + } + } + + } + + // Add a CSS align class to each field if one was set + if (!empty($options['info'][$field]['align'])) { + $vars['fields'][$field] .= ' ' . drupal_clean_css_identifier($options['info'][$field]['align']); + } + + // Render each field into its appropriate column. + foreach ($result as $num => $row) { + // Add field classes + $vars['field_classes'][$field][$num] = ''; + if ($fields[$field]->options['element_default_classes']) { + $vars['field_classes'][$field][$num] = "views-field views-field-" . $vars['fields'][$field]; + } + if ($classes = $fields[$field]->element_classes($num)) { + if ($vars['field_classes'][$field][$num]) { + $vars['field_classes'][$field][$num] .= ' '; + } + + $vars['field_classes'][$field][$num] .= $classes; + } + // Add responsive header classes. + if (!empty($options['info'][$field]['responsive'])) { + $vars['field_classes'][$field][$num] .= ' ' . $options['info'][$field]['responsive']; + } + + $vars['field_attributes'][$field][$num] = array(); + + if (!empty($fields[$field]) && empty($fields[$field]->options['exclude'])) { + $field_output = $renders[$num][$field]; + $element_type = $fields[$field]->element_type(TRUE, TRUE); + if ($element_type) { + $field_output = '<' . $element_type . '>' . $field_output . '</' . $element_type . '>'; + } + + // Don't bother with separators and stuff if the field does not show up. + if (empty($field_output) && !empty($vars['rows'][$num][$column])) { + continue; + } + + // Place the field into the column, along with an optional separator. + if (!empty($vars['rows'][$num][$column])) { + if (!empty($options['info'][$column]['separator'])) { + $vars['rows'][$num][$column] .= filter_xss_admin($options['info'][$column]['separator']); + } + } + else { + $vars['rows'][$num][$column] = ''; + } + + $vars['rows'][$num][$column] .= $field_output; + } + } + + // Remove columns if the option is hide empty column is checked and the field is not empty. + if (!empty($options['info'][$field]['empty_column'])) { + $empty = TRUE; + foreach ($vars['rows'] as $num => $columns) { + $empty &= empty($columns[$column]); + } + if ($empty) { + foreach ($vars['rows'] as $num => &$column_items) { + unset($column_items[$column]); + unset($vars['header'][$column]); + } + } + } + } + + // Hide table header if all labels are empty. + if (!array_filter($vars['header'])) { + $vars['header'] = array(); + } + + $count = 0; + foreach ($vars['rows'] as $num => $row) { + $vars['row_classes'][$num] = array(); + if ($row_class_special) { + $vars['row_classes'][$num][] = ($count++ % 2 == 0) ? 'odd' : 'even'; + } + if ($row_class = $handler->get_row_class($num)) { + $vars['row_classes'][$num][] = $row_class; + } + } + + if ($row_class_special) { + $vars['row_classes'][0][] = 'views-row-first'; + $vars['row_classes'][count($vars['row_classes']) - 1][] = 'views-row-last'; + } + + $vars['attributes']['class'] = array('views-table'); + if (empty($vars['rows']) && !empty($options['empty_table'])) { + $vars['rows'][0][0] = $view->display_handler->renderArea('empty'); + // Calculate the amounts of rows with output. + $vars['field_attributes'][0][0]['colspan'] = count($vars['header']); + $vars['field_classes'][0][0] = 'views-empty'; + } + + if (!empty($options['sticky'])) { + drupal_add_js('misc/tableheader.js'); + $vars['attributes']['class'][] = "sticky-enabled"; + } + $vars['attributes']['class'][] = 'cols-'. count($vars['header']); + + if (!empty($handler->options['summary'])) { + $vars['attributes_array'] = array('summary' => $handler->options['summary']); + } + // If the table has headers and it should react responsively to columns hidden + // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM + // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors. + if (count($vars['header']) && $responsive) { + drupal_add_library('system', 'drupal.tableresponsive'); + // Add 'responsive-enabled' class to the table to identify it for JS. + // This is needed to target tables constructed by this function. + $vars['attributes']['class'][] = 'responsive-enabled'; + } +} + +/** + * Display a view as a grid style. + */ +function template_preprocess_views_view_grid(&$vars) { + $view = $vars['view']; + $result = $view->result; + $options = $view->style_plugin->options; + $handler = $view->style_plugin; + $default_row_class = isset($options['default_row_class']) ? $options['default_row_class'] : TRUE; + $row_class_special = isset($options['row_class_special']) ? $options['row_class_special'] : TRUE; + + $columns = $options['columns']; + $vars['attributes']['class'][] = 'views-view-grid cols-' . $columns; + + $rows = array(); + $row_indexes = array(); + + if ($options['alignment'] == 'horizontal') { + $row = array(); + $col_count = 0; + $row_count = 0; + $count = 0; + foreach ($vars['rows'] as $row_index => $item) { + $count++; + $row[] = $item; + $row_indexes[$row_count][$col_count] = $row_index; + $col_count++; + if ($count % $columns == 0) { + $rows[] = $row; + $row = array(); + $col_count = 0; + $row_count++; + } + } + if ($row) { + // Fill up the last line only if it's configured, but this is default. + if (!empty($handler->options['fill_single_line']) && count($rows)) { + for ($i = 0; $i < ($columns - $col_count); $i++) { + $row[] = ''; + } + } + $rows[] = $row; + } + } + else { + $num_rows = floor(count($vars['rows']) / $columns); + // The remainders are the 'odd' columns that are slightly longer. + $remainders = count($vars['rows']) % $columns; + $row = 0; + $col = 0; + foreach ($vars['rows'] as $count => $item) { + $rows[$row][$col] = $item; + $row_indexes[$row][$col] = $count; + $row++; + + if (!$remainders && $row == $num_rows) { + $row = 0; + $col++; + } + elseif ($remainders && $row == $num_rows + 1) { + $row = 0; + $col++; + $remainders--; + } + } + for ($i = 0; $i < count($rows[0]); $i++) { + // This should be string so that's okay :) + if (!isset($rows[count($rows) - 1][$i])) { + $rows[count($rows) - 1][$i] = ''; + } + } + } + + // Apply the row classes + foreach ($rows as $row_number => $row) { + $row_classes = array(); + if ($default_row_class) { + $row_classes[] = 'row-' . ($row_number + 1); + } + if ($row_class_special) { + if ($row_number == 0) { + $row_classes[] = 'row-first'; + } + if (count($rows) == ($row_number + 1)) { + $row_classes[] = 'row-last'; + } + } + $vars['row_classes'][$row_number] = implode(' ', $row_classes); + foreach ($rows[$row_number] as $column_number => $item) { + $column_classes = array(); + if ($default_row_class) { + $column_classes[] = 'col-'. ($column_number + 1); + } + if ($row_class_special) { + if ($column_number == 0) { + $column_classes[] = 'col-first'; + } + elseif (count($rows[$row_number]) == ($column_number + 1)) { + $column_classes[] = 'col-last'; + } + } + if (isset($row_indexes[$row_number][$column_number]) && $column_class = $view->style_plugin->get_row_class($row_indexes[$row_number][$column_number])) { + $column_classes[] = $column_class; + } + $vars['column_classes'][$row_number][$column_number] = implode(' ', $column_classes); + } + } + $vars['rows'] = $rows; + if (!empty($handler->options['summary'])) { + $vars['attributes_array'] = array('summary' => $handler->options['summary']); + } + // If the table has headers and it should react responsively to columns hidden + // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM + // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors. + if (count($vars['header']) && $responsive) { + drupal_add_library('system', 'drupal.tableresponsive'); + // Add 'responsive-enabled' class to the table to identify it for JS. + // This is needed to target tables constructed by this function. + $vars['attributes']['class'][] = 'responsive-enabled'; + } +} + +/** + * Display the simple view of rows one after another + */ +function template_preprocess_views_view_unformatted(&$vars) { + $view = $vars['view']; + $rows = $vars['rows']; + $style = $view->style_plugin; + $options = $style->options; + + $vars['row_classes'] = array(); + $vars['classes'] = array(); + $default_row_class = isset($options['default_row_class']) ? $options['default_row_class'] : FALSE; + $row_class_special = isset($options['row_class_special']) ? $options['row_class_special'] : FALSE; + // Set up striping values. + $count = 0; + $max = count($rows); + foreach ($rows as $id => $row) { + $count++; + if ($default_row_class) { + $vars['classes'][$id][] = 'views-row'; + $vars['classes'][$id][] = 'views-row-' . $count; + } + if ($row_class_special) { + $vars['classes'][$id][] = 'views-row-' . ($count % 2 ? 'odd' : 'even'); + if ($count == 1) { + $vars['classes'][$id][] = 'views-row-first'; + } + if ($count == $max) { + $vars['classes'][$id][] = 'views-row-last'; + } + } + + if ($row_class = $view->style_plugin->get_row_class($id)) { + $vars['classes'][$id][] = $row_class; + } + + // Flatten the classes to a string for each row for the template file. + $vars['row_classes'][$id] = isset($vars['classes'][$id]) ? implode(' ', $vars['classes'][$id]) : ''; + } +} + +/** + * Display the view as an HTML list element + */ +function template_preprocess_views_view_list(&$vars) { + $handler = $vars['view']->style_plugin; + + $class = explode(' ', $handler->options['class']); + $class = array_map('drupal_clean_css_identifier', $class); + + $wrapper_class = explode(' ', $handler->options['wrapper_class']); + $wrapper_class = array_map('drupal_clean_css_identifier', $wrapper_class); + + $vars['class'] = implode(' ', $class); + $vars['wrapper_class'] = implode(' ', $wrapper_class); + $vars['wrapper_prefix'] = ''; + $vars['wrapper_suffix'] = ''; + $vars['list_type_prefix'] = '<' . $handler->options['type'] . '>'; + $vars['list_type_suffix'] = '</' . $handler->options['type'] . '>'; + if ($vars['wrapper_class']) { + $vars['wrapper_prefix'] = '<div class="' . $vars['wrapper_class'] . '">'; + $vars['wrapper_suffix'] = '</div>'; + } + + if ($vars['class']) { + $vars['list_type_prefix'] = '<' . $handler->options['type'] . ' class="' . $vars['class'] . '">'; + } + template_preprocess_views_view_unformatted($vars); +} + +/** + * Preprocess an RSS feed + */ +function template_preprocess_views_view_rss(&$vars) { + global $base_url; + + $view = &$vars['view']; + $options = &$vars['options']; + $items = &$vars['rows']; + + $style = &$view->style_plugin; + + $config = config('system.site'); + + // The RSS 2.0 "spec" doesn't indicate HTML can be used in the description. + // We strip all HTML tags, but need to prevent double encoding from properly + // escaped source data (such as & becoming &amp;). + $vars['description'] = check_plain(decode_entities(strip_tags($style->get_description()))); + + if ($view->display_handler->getOption('sitename_title')) { + $title = $config->get('name'); + if ($slogan = $config->get('slogan')) { + $title .= ' - ' . $slogan; + } + } + else { + $title = $view->getTitle(); + } + $vars['title'] = check_plain($title); + + // Figure out which display which has a path we're using for this feed. If there isn't + // one, use the global $base_url + $link_display_id = $view->display_handler->getLinkDisplay(); + if ($link_display_id && !empty($view->displayHandlers[$link_display_id])) { + $path = $view->displayHandlers[$link_display_id]->getPath(); + } + + if ($path) { + $path = $view->getUrl(NULL, $path); + $url_options = array('absolute' => TRUE); + if (!empty($view->exposed_raw_input)) { + $url_options['query'] = $view->exposed_raw_input; + } + + // Compare the link to the default home page; if it's the default home page, just use $base_url. + if ($path == $config->get('page.front')) { + $path = ''; + } + + $vars['link'] = check_url(url($path, $url_options)); + } + + $vars['langcode'] = check_plain(language(LANGUAGE_TYPE_INTERFACE)->langcode); + $vars['namespaces'] = new Attribute($style->namespaces); + $vars['items'] = $items; + $vars['channel_elements'] = format_xml_elements($style->channel_elements); + + // During live preview we don't want to output the header since the contents + // of the feed are being displayed inside a normal HTML page. + if (empty($vars['view']->live_preview)) { + $vars['view']->getResponse()->headers->set('Content-Type', 'application/rss+xml; charset=utf-8'); + } +} + +/** + * Default theme function for all RSS rows. + */ +function template_preprocess_views_view_row_rss(&$vars) { + $view = &$vars['view']; + $options = &$vars['options']; + $item = &$vars['row']; + + $vars['title'] = check_plain($item->title); + $vars['link'] = check_url($item->link); + $vars['description'] = check_plain($item->description); + $vars['item_elements'] = empty($item->elements) ? '' : format_xml_elements($item->elements); +} + +/** + * Default theme function for all filter forms. + */ +function template_preprocess_views_exposed_form(&$vars) { + $form = &$vars['form']; + + // Put all single checkboxes together in the last spot. + $checkboxes = ''; + + if (!empty($form['q'])) { + $vars['q'] = drupal_render($form['q']); + } + + $vars['widgets'] = array(); + foreach ($form['#info'] as $id => $info) { + // Set aside checkboxes. + if (isset($form[$info['value']]['#type']) && $form[$info['value']]['#type'] == 'checkbox') { + $checkboxes .= drupal_render($form[$info['value']]); + continue; + } + $widget = new stdClass; + // set up defaults so that there's always something there. + $widget->label = $widget->operator = $widget->widget = $widget->description = NULL; + + $widget->id = isset($form[$info['value']]['#id']) ? $form[$info['value']]['#id'] : ''; + + if (!empty($info['label'])) { + $widget->label = check_plain($info['label']); + } + if (!empty($info['operator'])) { + $widget->operator = drupal_render($form[$info['operator']]); + } + + $widget->widget = drupal_render($form[$info['value']]); + + if (!empty($info['description'])) { + $widget->description = check_plain($info['description']); + } + + $vars['widgets'][$id] = $widget; + } + + // Wrap up all the checkboxes we set aside into a widget. + if ($checkboxes) { + $widget = new stdClass; + // set up defaults so that there's always something there. + $widget->label = $widget->operator = $widget->widget = NULL; + $widget->id = 'checkboxes'; + $widget->widget = $checkboxes; + $vars['widgets']['checkboxes'] = $widget; + } + + if (isset($form['sort_by'])) { + $vars['sort_by'] = drupal_render($form['sort_by']); + $vars['sort_order'] = drupal_render($form['sort_order']); + } + if (isset($form['items_per_page'])) { + $vars['items_per_page'] = drupal_render($form['items_per_page']); + } + if (isset($form['offset'])) { + $vars['offset'] = drupal_render($form['offset']); + } + if (isset($form['reset'])) { + $vars['reset_button'] = drupal_render($form['reset']); + } + // This includes the submit button. + $vars['button'] = drupal_render_children($form); +} + +/** + * Theme function for a View with form elements: replace the placeholders. + */ +function theme_views_form_views_form($variables) { + $form = $variables['form']; + + // Placeholders and their substitutions (usually rendered form elements). + $search = array(); + $replace = array(); + + // Add in substitutions provided by the form. + foreach ($form['#substitutions']['#value'] as $substitution) { + $field_name = $substitution['field_name']; + $row_id = $substitution['row_id']; + + $search[] = $substitution['placeholder']; + $replace[] = isset($form[$field_name][$row_id]) ? drupal_render($form[$field_name][$row_id]) : ''; + } + // Add in substitutions from hook_views_form_substitutions(). + $substitutions = module_invoke_all('views_form_substitutions'); + foreach ($substitutions as $placeholder => $substitution) { + $search[] = $placeholder; + $replace[] = $substitution; + } + + // Apply substitutions to the rendered output. + $form['output']['#markup'] = str_replace($search, $replace, $form['output']['#markup']); + + // Render and add remaining form fields. + return drupal_render_children($form); +} + +function theme_views_mini_pager($vars) { + global $pager_page_array, $pager_total; + + $tags = $vars['tags']; + $element = $vars['element']; + $parameters = $vars['parameters']; + $quantity = $vars['quantity']; + + // Calculate various markers within this pager piece: + // Middle is used to "center" pages around the current page. + $pager_middle = ceil($quantity / 2); + // current is the page we are currently paged to + $pager_current = $pager_page_array[$element] + 1; + // max is the maximum page number + $pager_max = $pager_total[$element]; + // End of marker calculations. + + if ($pager_total[$element] > 1) { + + $li_previous = theme('pager_previous', + array( + 'text' => (isset($tags[1]) ? $tags[1] : t('‹‹')), + 'element' => $element, + 'interval' => 1, + 'parameters' => $parameters, + ) + ); + if (empty($li_previous)) { + $li_previous = " "; + } + + $li_next = theme('pager_next', + array( + 'text' => (isset($tags[3]) ? $tags[3] : t('››')), + 'element' => $element, + 'interval' => 1, + 'parameters' => $parameters, + ) + ); + + if (empty($li_next)) { + $li_next = " "; + } + + $items[] = array( + 'class' => array('pager-previous'), + 'data' => $li_previous, + ); + + $items[] = array( + 'class' => array('pager-current'), + 'data' => t('@current of @max', array('@current' => $pager_current, '@max' => $pager_max)), + ); + + $items[] = array( + 'class' => array('pager-next'), + 'data' => $li_next, + ); + return theme('item_list', + array( + 'items' => $items, + 'title' => NULL, + 'type' => 'ul', + 'attributes' => array('class' => array('pager')), + ) + ); + } +} + +/** + * @defgroup views_templates Views template files + * @{ + * All views templates can be overridden with a variety of names, using + * the view, the display ID of the view, the display type of the view, + * or some combination thereof. + * + * For each view, there will be a minimum of two templates used. The first + * is used for all views: views-view.tpl.php. + * + * The second template is determined by the style selected for the view. Note + * that certain aspects of the view can also change which style is used; for + * example, arguments which provide a summary view might change the style to + * one of the special summary styles. + * + * The default style for all views is views-view-unformatted.tpl.php + * + * Many styles will then farm out the actual display of each row to a row + * style; the default row style is views-view-fields.tpl.php. + * + * Here is an example of all the templates that will be tried in the following + * case: + * + * View, named foobar. Style: unformatted. Row style: Fields. Display: Page. + * + * - views-view--foobar--page.tpl.php + * - views-view--page.tpl.php + * - views-view--foobar.tpl.php + * - views-view.tpl.php + * + * - views-view-unformatted--foobar--page.tpl.php + * - views-view-unformatted--page.tpl.php + * - views-view-unformatted--foobar.tpl.php + * - views-view-unformatted.tpl.php + * + * - views-view-fields--foobar--page.tpl.php + * - views-view-fields--page.tpl.php + * - views-view-fields--foobar.tpl.php + * - views-view-fields.tpl.php + * + * Important! When adding a new template to your theme, be sure to flush the + * theme registry cache! + * + * @see _views_theme_functions() + * @} + */ diff --git a/core/modules/views/theme/views-exposed-form.tpl.php b/core/modules/views/theme/views-exposed-form.tpl.php new file mode 100644 index 0000000..bdd570c --- /dev/null +++ b/core/modules/views/theme/views-exposed-form.tpl.php @@ -0,0 +1,80 @@ +<?php + +/** + * @file + * This template handles the layout of the views exposed filter form. + * + * Variables available: + * - $widgets: An array of exposed form widgets. Each widget contains: + * - $widget->label: The visible label to print. May be optional. + * - $widget->operator: The operator for the widget. May be optional. + * - $widget->widget: The widget itself. + * - $sort_by: The select box to sort the view using an exposed form. + * - $sort_order: The select box with the ASC, DESC options to define order. May be optional. + * - $items_per_page: The select box with the available items per page. May be optional. + * - $offset: A textfield to define the offset of the view. May be optional. + * - $reset_button: A button to reset the exposed filter applied. May be optional. + * - $button: The submit button for the form. + * + * @ingroup views_templates + */ +?> +<?php if (!empty($q)): ?> + <?php + // This ensures that, if clean URLs are off, the 'q' is added first so that + // it shows up first in the URL. + print $q; + ?> +<?php endif; ?> +<div class="views-exposed-form"> + <div class="views-exposed-widgets clearfix"> + <?php foreach ($widgets as $id => $widget): ?> + <div id="<?php print $widget->id; ?>-wrapper" class="views-exposed-widget views-widget-<?php print $id; ?>"> + <?php if (!empty($widget->label)): ?> + <label for="<?php print $widget->id; ?>"> + <?php print $widget->label; ?> + </label> + <?php endif; ?> + <?php if (!empty($widget->operator)): ?> + <div class="views-operator"> + <?php print $widget->operator; ?> + </div> + <?php endif; ?> + <div class="views-widget"> + <?php print $widget->widget; ?> + </div> + <?php if (!empty($widget->description)): ?> + <div class="description"> + <?php print $widget->description; ?> + </div> + <?php endif; ?> + </div> + <?php endforeach; ?> + <?php if (!empty($sort_by)): ?> + <div class="views-exposed-widget views-widget-sort-by"> + <?php print $sort_by; ?> + </div> + <div class="views-exposed-widget views-widget-sort-order"> + <?php print $sort_order; ?> + </div> + <?php endif; ?> + <?php if (!empty($items_per_page)): ?> + <div class="views-exposed-widget views-widget-per-page"> + <?php print $items_per_page; ?> + </div> + <?php endif; ?> + <?php if (!empty($offset)): ?> + <div class="views-exposed-widget views-widget-offset"> + <?php print $offset; ?> + </div> + <?php endif; ?> + <div class="views-exposed-widget views-submit-button"> + <?php print $button; ?> + </div> + <?php if (!empty($reset_button)): ?> + <div class="views-exposed-widget views-reset-button"> + <?php print $reset_button; ?> + </div> + <?php endif; ?> + </div> +</div> diff --git a/core/modules/views/theme/views-more.tpl.php b/core/modules/views/theme/views-more.tpl.php new file mode 100644 index 0000000..0b7080b --- /dev/null +++ b/core/modules/views/theme/views-more.tpl.php @@ -0,0 +1,19 @@ +<?php + +/** + * @file + * Theme the more link. + * + * - $view: The view object. + * - $more_url: the url for the more link. + * - $link_text: the text for the more link. + * + * @ingroup views_templates + */ +?> + +<div class="more-link"> + <a href="<?php print $more_url ?>"> + <?php print $link_text; ?> + </a> +</div> diff --git a/core/modules/views/theme/views-view-field.tpl.php b/core/modules/views/theme/views-view-field.tpl.php new file mode 100644 index 0000000..91d92ee --- /dev/null +++ b/core/modules/views/theme/views-view-field.tpl.php @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * This template is used to print a single field in a view. + * + * It is not actually used in default Views, as this is registered as a theme + * function which has better performance. For single overrides, the template is + * perfectly okay. + * + * Variables available: + * - $view: The view object + * - $field: The field handler object that can process the input + * - $row: The raw SQL result that can be used + * - $output: The processed output that will normally be used. + * + * When fetching output from the $row, this construct should be used: + * $data = $row->{$field->field_alias} + * + * The above will guarantee that you'll always get the correct data, + * regardless of any changes in the aliasing that might happen if + * the view is modified. + */ +?> +<?php print $output; ?> diff --git a/core/modules/views/theme/views-view-fields.tpl.php b/core/modules/views/theme/views-view-fields.tpl.php new file mode 100644 index 0000000..ae3a4c6 --- /dev/null +++ b/core/modules/views/theme/views-view-fields.tpl.php @@ -0,0 +1,36 @@ +<?php + +/** + * @file + * Default simple view template to all the fields as a row. + * + * - $view: The view in use. + * - $fields: an array of $field objects. Each one contains: + * - $field->content: The output of the field. + * - $field->raw: The raw data for the field, if it exists. This is NOT output safe. + * - $field->class: The safe class id to use. + * - $field->handler: The Views field handler object controlling this field. Do not use + * var_export to dump this object, as it can't handle the recursion. + * - $field->inline: Whether or not the field should be inline. + * - $field->inline_html: either div or span based on the above flag. + * - $field->wrapper_prefix: A complete wrapper containing the inline_html to use. + * - $field->wrapper_suffix: The closing tag for the wrapper. + * - $field->separator: an optional separator that may appear before a field. + * - $field->label: The wrap label text to use. + * - $field->label_html: The full HTML of the label to use including + * configured element type. + * - $row: The raw result object from the query, with all data it fetched. + * + * @ingroup views_templates + */ +?> +<?php foreach ($fields as $id => $field): ?> + <?php if (!empty($field->separator)): ?> + <?php print $field->separator; ?> + <?php endif; ?> + + <?php print $field->wrapper_prefix; ?> + <?php print $field->label_html; ?> + <?php print $field->content; ?> + <?php print $field->wrapper_suffix; ?> +<?php endforeach; ?> diff --git a/core/modules/views/theme/views-view-grid.tpl.php b/core/modules/views/theme/views-view-grid.tpl.php new file mode 100644 index 0000000..603eed5 --- /dev/null +++ b/core/modules/views/theme/views-view-grid.tpl.php @@ -0,0 +1,28 @@ +<?php + +/** + * @file + * Default simple view template to display a rows in a grid. + * + * - $rows contains a nested array of rows. Each row contains an array of + * columns. + * + * @ingroup views_templates + */ +?> +<?php if (!empty($title)) : ?> + <h3><?php print $title; ?></h3> +<?php endif; ?> +<table <?php print $attributes; ?>> + <tbody> + <?php foreach ($rows as $row_number => $columns): ?> + <tr <?php if ($row_classes[$row_number]) { print 'class="' . $row_classes[$row_number] .'"'; } ?>> + <?php foreach ($columns as $column_number => $item): ?> + <td <?php if ($column_classes[$row_number][$column_number]) { print 'class="' . $column_classes[$row_number][$column_number] .'"'; } ?>> + <?php print $item; ?> + </td> + <?php endforeach; ?> + </tr> + <?php endforeach; ?> + </tbody> +</table> diff --git a/core/modules/views/theme/views-view-grouping.tpl.php b/core/modules/views/theme/views-view-grouping.tpl.php new file mode 100644 index 0000000..ebf7bc2 --- /dev/null +++ b/core/modules/views/theme/views-view-grouping.tpl.php @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * This template is used to print a single grouping in a view. + * + * It is not actually used in default Views, as this is registered as a theme + * function which has better performance. For single overrides, the template is + * perfectly okay. + * + * Variables available: + * - $view: The view object + * - $grouping: The grouping instruction. + * - $grouping_level: Integer indicating the hierarchical level of the grouping. + * - $rows: The rows contained in this grouping. + * - $title: The title of this grouping. + * - $content: The processed content output that will normally be used. + */ +?> +<div class="view-grouping"> + <div class="view-grouping-header"><?php print $title; ?></div> + <div class="view-grouping-content"> + <?php print $content; ?> + </div> +</div> diff --git a/core/modules/views/theme/views-view-list.tpl.php b/core/modules/views/theme/views-view-list.tpl.php new file mode 100644 index 0000000..2c4b9c5 --- /dev/null +++ b/core/modules/views/theme/views-view-list.tpl.php @@ -0,0 +1,21 @@ +<?php + +/** + * @file + * Default simple view template to display a list of rows. + * + * - $title : The title of this group of rows. May be empty. + * - $options['type'] will either be ul or ol. + * @ingroup views_templates + */ +?> +<?php print $wrapper_prefix; ?> + <?php if (!empty($title)) : ?> + <h3><?php print $title; ?></h3> + <?php endif; ?> + <?php print $list_type_prefix; ?> + <?php foreach ($rows as $id => $row): ?> + <li <?php print $row_classes[$id]; ?>><?php print $row; ?></li> + <?php endforeach; ?> + <?php print $list_type_suffix; ?> +<?php print $wrapper_suffix; ?> diff --git a/core/modules/views/theme/views-view-row-comment.tpl.php b/core/modules/views/theme/views-view-row-comment.tpl.php new file mode 100644 index 0000000..7fe2e81 --- /dev/null +++ b/core/modules/views/theme/views-view-row-comment.tpl.php @@ -0,0 +1,18 @@ +<?php + +/** + * @file + * Default simple view template to display a single comment. + * + * Rather than doing anything with this particular template, it is more + * efficient to use a variant of the comment.tpl.php based upon the view, + * which will be named comment-view-VIEWNAME.tpl.php. This isn't actually + * a views template, which is why it's not used here, but is a template + * 'suggestion' given to the comment template, and is used exactly + * the same as any other variant of the comment template file, such as + * node-nodeTYPE.tpl.php + * + * @ingroup views_templates + */ +?> +<?php print $comment; ?> diff --git a/core/modules/views/theme/views-view-row-rss.tpl.php b/core/modules/views/theme/views-view-row-rss.tpl.php new file mode 100644 index 0000000..01e0696 --- /dev/null +++ b/core/modules/views/theme/views-view-row-rss.tpl.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Default view template to display a item in an RSS feed. + * + * @ingroup views_templates + */ +?> + <item> + <title><?php print $title; ?> + + + + diff --git a/core/modules/views/theme/views-view-rss.tpl.php b/core/modules/views/theme/views-view-rss.tpl.php new file mode 100644 index 0000000..18ca73e --- /dev/null +++ b/core/modules/views/theme/views-view-rss.tpl.php @@ -0,0 +1,20 @@ + + version="1.0" encoding="utf-8" "; ?> +> + + <?php print $title; ?> + + + + + + + diff --git a/core/modules/views/theme/views-view-summary-unformatted.tpl.php b/core/modules/views/theme/views-view-summary-unformatted.tpl.php new file mode 100644 index 0000000..306d76f --- /dev/null +++ b/core/modules/views/theme/views-view-summary-unformatted.tpl.php @@ -0,0 +1,20 @@ + + $row): ?> + '; ?> + separator)) { print $row->separator; } ?> + >link; ?> + + (count; ?>) + + ' : ''; ?> + diff --git a/core/modules/views/theme/views-view-summary.tpl.php b/core/modules/views/theme/views-view-summary.tpl.php new file mode 100644 index 0000000..22969eb --- /dev/null +++ b/core/modules/views/theme/views-view-summary.tpl.php @@ -0,0 +1,20 @@ + +
+ +
diff --git a/core/modules/views/theme/views-view-table.tpl.php b/core/modules/views/theme/views-view-table.tpl.php new file mode 100644 index 0000000..548866c --- /dev/null +++ b/core/modules/views/theme/views-view-table.tpl.php @@ -0,0 +1,50 @@ + +> + + + + + + + $label): ?> + + + + + + + $row): ?> + > + $content): ?> + + + + + +
> + +
> + +
diff --git a/core/modules/views/theme/views-view-unformatted.tpl.php b/core/modules/views/theme/views-view-unformatted.tpl.php new file mode 100644 index 0000000..c5a34ae --- /dev/null +++ b/core/modules/views/theme/views-view-unformatted.tpl.php @@ -0,0 +1,17 @@ + + +

+ + $row): ?> +
> + +
+ diff --git a/core/modules/views/theme/views-view.tpl.php b/core/modules/views/theme/views-view.tpl.php new file mode 100644 index 0000000..8e645a6 --- /dev/null +++ b/core/modules/views/theme/views-view.tpl.php @@ -0,0 +1,90 @@ + +
> + + + + + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +
+ +
+ + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+ + +
diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php new file mode 100644 index 0000000..4479cd0 --- /dev/null +++ b/core/modules/views/views.api.php @@ -0,0 +1,1093 @@ +construct() + * - Create the initial handler; at this time it is not yet attached to a + * view. It is here that you can set basic defaults if needed, but there + * will be no knowledge of the environment yet. + * - handler->setDefinition() + * - Set the data from hook_views_data() relevant to the handler. + * - handler->init() + * - Attach the handler to a view, and usually provides the options from the + * display. + * - handler->preQuery() + * - Run prior to the query() stage to do early processing. + * - handler->query() + * - Do the bulk of the work this handler needs to do to add itself to the + * query. + * + * Fields, being the only handlers concerned with output, also have an extended + * piece of the flow: + * + * - handler->pre_render(&$values) + * - Called prior to the actual rendering, this allows handlers to query for + * extra data; the entire resultset is available here, and this is where + * items that have "multiple values" per record can do their extra query for + * all of the records available. There are several examples of this at work + * in the code, see for example views_handler_field_user_roles. + * - handler->render() + * - This does the actual work of rendering the field. + * + * Most handlers are just extensions of existing classes with a few tweaks that + * are specific to the field in question. For example, + * views_handler_filter_in_operator provides a simple mechanism to set a + * multiple-value list for setting filter values. Below, + * views_handler_filter_node_type overrides the list options, but inherits + * everything else. + * + * @code + * class views_handler_filter_node_type extends views_handler_filter_in_operator { + * function get_value_options() { + * if (!isset($this->value_options)) { + * $this->value_title = t('Node type'); + * $types = node_get_types(); + * foreach ($types as $type => $info) { + * $options[$type] = $info->name; + * } + * $this->value_options = $options; + * } + * } + * } + * @endcode + * + * Handlers are stored in their own files and loaded on demand. Like all other + * module files, they must first be registered through the module's info file. + * For example: + * + * @code + * name = Example module + * description = "Gives an example of a module." + * core = 8.x + * files[] = example.module + * files[] = example.install + * + * ; Views handlers + * files[] = includes/views/handlers/example_handler_argument_string.inc + * @endcode + * + * The best place to learn more about handlers and how they work is to explore + * @link views_handlers Views' handlers @endlink and use existing handlers as a + * guide and a model. Understanding how views_handler and its child classes work + * is handy but you can do a lot just following these models. You can also + * explore the views module directory, particularly node.views.inc. + * + * Please note that while all handler names in views are prefixed with views_, + * you should use your own module's name to prefix your handler names in order + * to ensure namespace safety. Note that the basic pattern for handler naming + * goes like this: + * + * [module]_handler_[type]_[tablename]_[fieldname]. + * + * Sometimes table and fieldname are not appropriate, but something that + * resembles what the table/field would be can be used. + * + * See also: + * - @link views_field_handlers Views field handlers @endlink + * - @link views_sort_handlers Views sort handlers @endlink + * - @link views_filter_handlers Views filter handlers @endlink + * - @link views_argument_handlers Views argument handlers @endlink + * - @link views_relationship_handlers Views relationship handlers @endlink + * - @link views_area_handlers Views area handlers @endlink + * @} + */ + +/** + * @defgroup views_plugins About Views plugins + * + * In Views, a plugin is a bit like a handler, but plugins are not directly + * responsible for building the query. Instead, they are objects that are used + * to display the view or make other modifications. + * + * There are 10 types of plugins in Views: + * - Display: Display plugins are responsible for controlling *where* a view + * lives; that is, how they are being exposed to other parts of Drupal. Page + * and block are the most common displays, as well as the ubiquitous 'master' + * (or 'default') display. + * - Style: Style plugins control how a view is displayed. For the most part + * they are object wrappers around theme templates. Styles could for example + * be HTML lists or tables. + * - Row style: Row styles handle each individual record from the main view + * table. The two included by default render the entire entity (nodes only), + * or selected fields. + * - Argument default: Argument default plugins allow pluggable ways of + * providing default values for contextual filters (previously 'arguments'). + * This is useful for blocks and other display types lacking a natural + * argument input. Examples are plugins to extract node and user IDs from the + * URL. + * - Argument validator: Validator plugins can ensure arguments are valid, and + * even do transformations on the arguments. They can also provide replacement + * patterns for the view title. For example, the 'content' validator + * verifies verifies that the argument value corresponds to a node, loads + * that node and provides the node title as a replacement pattern. + * - Access: Access plugins are responsible for controlling access to the view. + * Views includes plugins for checking user roles and individual permissions. + * - Query: Query plugins generate and execute a query, so they can be seen as + * a data backend. The default implementation is using SQL. There are + * contributed modules reading data from other sources, see for example the + * Views XML Backend module. + * - Cache: Cache plugins control the storage and loading of caches. Currently + * they can do both result and render caching, but maybe one day cache the + * generated query. + * - Pager plugins: Pager plugins take care of everything regarding pagers. + * From getting and setting the total amount of items to render the pager and + * setting the global pager arrays. + * - Exposed form plugins: Exposed form plugins are responsible for building, + * rendering and controlling exposed forms. They can expose new parts of the + * view to the user and more. + * - Localization plugins: Localization plugins take care how the view options + * are translated. There are example implementations for t(), 'no + * translation' and i18n. + * - Display extenders: Display extender plugins allow scaling of views options + * horizontally. This means that you can add options and do stuff on all + * views displays. One theoretical example is metatags for views. + * + * Plugins are registered by implementing hook_views_plugins() in your + * modulename.views.inc file and returning an array of data. + * For examples please look at views_views_plugins() in + * views/includes/plugins.inc as it has examples for all of them. + * + * Similar to handlers, make sure that you add your plugin files to the + * module.info file. + * + * The array defining plugins will look something like this: + * @code + * return array( + * 'display' => array( + * // ... list of display plugins, + * ), + * 'style' => array( + * // ... list of style plugins, + * ), + * 'row' => array( + * // ... list of row style plugins, + * ), + * 'argument default' => array( + * // ... list of argument default plugins, + * ), + * 'argument validator' => array( + * // ... list of argument validator plugins, + * ), + * 'access' => array( + * // ... list of access plugins, + * ), + * 'query' => array( + * // ... list of query plugins, + * ),, + * 'cache' => array( + * // ... list of cache plugins, + * ),, + * 'pager' => array( + * // ... list of pager plugins, + * ),, + * 'exposed_form' => array( + * // ... list of exposed_form plugins, + * ),, + * 'localization' => array( + * // ... list of localization plugins, + * ), + * 'display_extender' => array( + * // ... list of display extender plugins, + * ), + * ); + * @endcode + * + * Each plugin will be registered with an identifier for the plugin, plus a + * fairly lengthy list of items that can define how and where the plugin is + * used. Here is an example of a row style plugin from Views core: + * @code + * 'node' => array( + * 'title' => t('Node'), + * 'help' => t('Display the node with standard node view.'), + * 'handler' => 'views_plugin_row_node_view', + * 'path' => drupal_get_path('module', 'views') . '/modules/node', // not necessary for most modules + * 'theme' => 'views_view_row_node', + * 'base' => array('node'), // only works with 'node' as base. + * 'type' => 'normal', + * ), + * @endcode + * + * Of particular interest is the *path* directive, which works a little + * differently from handler registration; each plugin must define its own path, + * rather than relying on a global info for the paths. For example: + * @code + * 'feed' => array( + * 'title' => t('Feed'), + * 'help' => t('Display the view as a feed, such as an RSS feed.'), + * 'handler' => 'views_plugin_display_feed', + * 'uses_hook_menu' => TRUE, + * 'use_ajax' => FALSE, + * 'use_pager' => FALSE, + * 'accept_attachments' => FALSE, + * 'admin' => t('Feed'), + * ), + * @endcode + * + * Please be sure to prefix your plugin identifiers with your module name to + * ensure namespace safety; after all, two different modules could try to + * implement the 'grid2' plugin, and that would cause one plugin to completely + * fail. + * + * @todo Finish this document. + * + * See also: + * - @link views_display_plugins Views display plugins @endlink + * - @link views_style_plugins Views style plugins @endlink + * - @link views_row_plugins Views row plugins @endlink + */ + +/** + * @defgroup views_hooks Views hooks + * @{ + * Hooks that can be implemented by other modules in order to implement the + * Views API. + */ + +/** + * Describes data tables (or the equivalent) to Views. + * + * This hook should be placed in MODULENAME.views.inc and it will be + * auto-loaded. MODULENAME.views.inc must be in the directory specified by the + * 'path' key returned by MODULENAME_views_api(), or the same directory as the + * .module file, if 'path' is unspecified. + * + * @return + * An associative array describing the data structure. Primary key is the + * name used internally by Views for the table(s) – usually the actual table + * name. The values for the key entries are described in detail below. + */ +function hook_views_data() { + // This example describes how to write hook_views_data() for the following + // table: + // + // CREATE TABLE example_table ( + // nid INT(11) NOT NULL COMMENT 'Primary key; refers to {node}.nid.', + // plain_text_field VARCHAR(32) COMMENT 'Just a plain text field.', + // numeric_field INT(11) COMMENT 'Just a numeric field.', + // boolean_field INT(1) COMMENT 'Just an on/off field.', + // timestamp_field INT(8) COMMENT 'Just a timestamp field.', + // PRIMARY KEY(nid) + // ); + + // First, the entry $data['example_table']['table'] describes properties of + // the actual table – not its content. + + // The 'group' index will be used as a prefix in the UI for any of this + // table's fields, sort criteria, etc. so it's easy to tell where they came + // from. + $data['example_table']['table']['group'] = t('Example table'); + + // Define this as a base table – a table that can be described in itself by + // views (and not just being brought in as a relationship). In reality this + // is not very useful for this table, as it isn't really a distinct object of + // its own, but it makes a good example. + $data['example_table']['table']['base'] = array( + 'field' => 'nid', // This is the identifier field for the view. + 'title' => t('Example table'), + 'help' => t('Example table contains example content and can be related to nodes.'), + 'weight' => -10, + ); + + // This table references the {node} table. The declaration below creates an + // 'implicit' relationship to the node table, so that when 'node' is the base + // table, the fields are automatically available. + $data['example_table']['table']['join'] = array( + // Index this array by the table name to which this table refers. + // 'left_field' is the primary key in the referenced table. + // 'field' is the foreign key in this table. + 'node' => array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + // Next, describe each of the individual fields in this table to Views. This + // is done by describing $data['example_table']['FIELD_NAME']. This part of + // the array may then have further entries: + // - title: The label for the table field, as presented in Views. + // - help: The description text for the table field. + // - relationship: A description of any relationship handler for the table + // field. + // - field: A description of any field handler for the table field. + // - sort: A description of any sort handler for the table field. + // - filter: A description of any filter handler for the table field. + // - argument: A description of any argument handler for the table field. + // - area: A description of any handler for adding content to header, + // footer or as no result behaviour. + // + // The handler descriptions are described with examples below. + + // Node ID table field. + $data['example_table']['nid'] = array( + 'title' => t('Example content'), + 'help' => t('Some example content that references a node.'), + // Define a relationship to the {node} table, so example_table views can + // add a relationship to nodes. If you want to define a relationship the + // other direction, use hook_views_data_alter(), or use the 'implicit' join + // method described above. + 'relationship' => array( + 'base' => 'node', // The name of the table to join with + 'field' => 'nid', // The name of the field to join with + 'id' => 'standard', + 'label' => t('Example node'), + ), + ); + + // Example plain text field. + $data['example_table']['plain_text_field'] = array( + 'title' => t('Plain text field'), + 'help' => t('Just a plain text field.'), + 'field' => array( + 'id' => 'standard', + 'click sortable' => TRUE, // This is use by the table display plugin. + ), + 'sort' => array( + 'id' => 'standard', + ), + 'filter' => array( + 'id' => 'string', + ), + 'argument' => array( + 'id' => 'string', + ), + ); + + // Example numeric text field. + $data['example_table']['numeric_field'] = array( + 'title' => t('Numeric field'), + 'help' => t('Just a numeric field.'), + 'field' => array( + 'id' => 'numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // Example boolean field. + $data['example_table']['boolean_field'] = array( + 'title' => t('Boolean field'), + 'help' => t('Just an on/off field.'), + 'field' => array( + 'id' => 'boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'id' => 'boolean', + // Note that you can override the field-wide label: + 'label' => t('Published'), + // This setting is used by the boolean filter handler, as possible option. + 'type' => 'yes-no', + // use boolean_field = 1 instead of boolean_field <> 0 in WHERE statment. + 'use_equal' => TRUE, + ), + 'sort' => array( + 'id' => 'standard', + ), + ); + + // Example timestamp field. + $data['example_table']['timestamp_field'] = array( + 'title' => t('Timestamp field'), + 'help' => t('Just a timestamp field.'), + 'field' => array( + 'id' => 'date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'id' => 'date', + ), + 'filter' => array( + 'id' => 'date', + ), + ); + + return $data; +} + +/** + * Alter table structure. + * + * You can add/edit/remove existing tables defined by hook_views_data(). + * + * This hook should be placed in MODULENAME.views.inc and it will be + * auto-loaded. MODULENAME.views.inc must be in the directory specified by the + * 'path' key returned by MODULENAME_views_api(), or the same directory as the + * .module file, if 'path' is unspecified. + * + * @param $data + * An array of all Views data, passed by reference. See hook_views_data() for + * structure. + * + * @see hook_views_data() + */ +function hook_views_data_alter(&$data) { + // This example alters the title of the node:nid field in the Views UI. + $data['node']['nid']['title'] = t('Node-Nid'); + + // This example adds an example field to the users table. + $data['users']['example_field'] = array( + 'title' => t('Example field'), + 'help' => t('Some example content that references a user'), + 'handler' => 'hook_handlers_field_example_field', + ); + + // This example changes the handler of the node title field. + // In this handler you could do stuff, like preview of the node when clicking + // the node title. + $data['node']['title']['handler'] = 'modulename_handlers_field_node_title'; + + // This example adds a relationship to table {foo}, so that 'foo' views can + // add this table using a relationship. Because we don't want to write over + // the primary key field definition for the {foo}.fid field, we use a dummy + // field name as the key. + $data['foo']['dummy_name'] = array( + 'title' => t('Example relationship'), + 'help' => t('Example help'), + 'relationship' => array( + 'base' => 'example_table', // Table we're joining to. + 'base field' => 'eid', // Field on the joined table. + 'field' => 'fid', // Real field name on the 'foo' table. + 'id' => 'standard', + 'label' => t('Default label for relationship'), + 'title' => t('Title seen when adding relationship'), + 'help' => t('More information about relationship.'), + ), + ); + + // Note that the $data array is not returned – it is modified by reference. +} + +/** + * Describes plugins defined by the module. + * + * This hook should be placed in MODULENAME.views.inc and it will be + * auto-loaded. MODULENAME.views.inc must be in the directory specified by the + * 'path' key returned by MODULENAME_views_api(), or the same directory as the + * .module file, if 'path' is unspecified. + * + * @return + * An array on the form $plugins['PLUGIN TYPE']['PLUGIN NAME']. The plugin + * must be one of row, display, display_extender, style, argument default, + * argument validator, access, query, cache, pager, exposed_form or + * localization. The plugin name should be prefixed with your module name. + * The value for each entry is an associateive array that may contain the + * following entries: + * - Used by all plugin types: + * - title (required): The name of the plugin, as shown in Views. Wrap in + * t(). + * - handler (required): The name of the file containing the class + * describing the handler, which must also be the name of the handler's + * class. + * - path: Path to the handler. Only required if the handler is not placed + * in the same folder as the .module file or in the subfolder 'views'. + * - parent: The name of the plugin this plugin extends. Since Drupal 7 this + * is no longer required, but may still be useful from a code readability + * perspective. + * - no_ui: Set to TRUE to denote that the plugin doesn't appear to be + * selectable in the ui, though on the api side they still exists. + * - help: A short help text, wrapped in t() used as description on the plugin settings form. + * - theme: The name of a theme suggestion to use for the display. + * - js: An array with paths to js files that should be included for the + * display. Note that the path should be relative Drupal root, not module + * root. + * - type: Each plugin can specify a type parameter to group certain + * plugins together. For example all row plugins related to feeds are + * grouped together, because a rss style plugin only accepts feed row + * plugins. + * + * - Used by display plugins: + * - admin: The administrative name of the display, as displayed on the + * Views overview and also used as default name for new displays. Wrap in + * t(). + * - no remove: Set to TRUE to make the display non-removable. (Basically + * only used for the master/default display.) + * - use_ajax: Set to TRUE to allow AJAX loads in the display. If it's + * disabled there will be no ajax option in the ui. + * - use_pager: Set to TRUE to allow paging in the display. + * - use_more: Set to TRUE to allow the 'use_more' setting in the display. + * - accept_attachments: Set to TRUE to allow attachment displays to be + * attached to this display type. + * - contextual_links_locations: An array with places where contextual links + * should be added. Can for example be 'page' or 'block'. If you don't + * specify it there will be contextual links around the rendered view. + * - uses_hook_menu: Set to TRUE to have the display included by + * views_menu_alter(). views_menu_alter executes then execute_hook_menu + * on the display object. + * - uses_hook_block: Set to TRUE to have the display included by + * views_block_info(). + * - theme: The name of a theme suggestion to use for the display. + * - js: An array with paths to js files that should be included for the + * display. Note that the path should be relative Drupal root, not module + * root. + * + * - Used by style plugins: + * - uses_row_plugin: Set to TRUE to allow row plugins for this style. + * - uses_row_class: Set to TRUE to allow the CSS class settings for rows. + * - uses_fields: Set to TRUE to have the style plugin accept field + * handlers. + * - uses grouping: Set to TRUE to allow the grouping settings for rows. + * - even empty: May have the value 'even empty' to tell Views that the style + * should be rendered even if there are no results. + * + * - Used by row plugins: + * - uses_fields: Set to TRUE to have the row plugin accept field handlers. + */ +function hook_views_plugins() { + $plugins = array(); + $plugins['argument validator'] = array( + 'taxonomy_term' => array( + 'title' => t('Taxonomy term'), + 'handler' => 'views_plugin_argument_validate_taxonomy_term', + // Declaring path explicitly not necessary for most modules. + 'path' => drupal_get_path('module', 'views') . '/modules/taxonomy', + ), + ); + + return array( + 'module' => 'views', // This just tells our themes are elsewhere. + 'argument validator' => array( + 'taxonomy_term' => array( + 'title' => t('Taxonomy term'), + 'handler' => 'views_plugin_argument_validate_taxonomy_term', + 'path' => drupal_get_path('module', 'views') . '/modules/taxonomy', // not necessary for most modules + ), + ), + 'argument default' => array( + 'taxonomy_tid' => array( + 'title' => t('Taxonomy term ID from URL'), + 'handler' => 'views_plugin_argument_default_taxonomy_tid', + 'path' => drupal_get_path('module', 'views') . '/modules/taxonomy', + 'parent' => 'fixed', + ), + ), + ); +} + +/** + * Alter existing plugins data, defined by modules. + * + * @see hook_views_plugins() + */ +function hook_views_plugins_alter(&$plugins) { + // Add apachesolr to the base of the node row plugin. + $plugins['row']['node']['base'][] = 'apachesolr'; +} + +/** + * Register View API information. + * + * This is required for your module to have its include files loaded; for + * example, when implementing hook_views_default_views(). + * + * @return + * An array with the following possible keys: + * - api: (required) The version of the Views API the module implements. + * - path: (optional) If includes are stored somewhere other than within the + * root module directory, specify its path here. + * - template path: (optional) A path where the module has stored it's views + * template files. When you have specificed this key views automatically + * uses the template files for the views. You can use the same naming + * conventions like for normal views template files. + */ +function hook_views_api() { + return array( + 'api' => 3, + 'path' => drupal_get_path('module', 'example') . '/includes/views', + 'template path' => drupal_get_path('module', 'example') . '/themes', + ); +} + +/** + * This hook allows modules to provide their own views which can either be used + * as-is or as a "starter" for users to build from. + * + * This hook should be placed in MODULENAME.views.inc and it will be + * auto-loaded. MODULENAME.views.inc must be in the directory specified by the + * 'path' key returned by MODULENAME_views_api(), or the same directory as the + * .module file, if 'path' is unspecified. + * + * The $view->disabled boolean flag indicates whether the View should be + * enabled (FALSE) or disabled (TRUE) by default. + * + * @return + * An associative array containing the structures of views, as generated from + * the Export tab, keyed by the view name. A best practice is to go through + * and add t() to all title and label strings, with the exception of menu + * strings. + */ +function hook_views_default_views() { + // Begin copy and paste of output from the Export tab of a view. + $view = new Drupal\views\ViewExecutable(); + $view->name = 'frontpage'; + $view->description = 'Emulates the default Drupal front page; you may set the default home page path to this view to make it your front page.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Front page'; + $view->core = 8; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->newDisplay('default', 'Master', 'default'); + $handler->display['display_options']['access']['type'] = 'none'; + $handler->display['display_options']['cache']['type'] = 'none'; + $handler->display['display_options']['query']['type'] = 'views_query'; + $handler->display['display_options']['query']['options']['query_comment'] = FALSE; + $handler->display['display_options']['exposed_form']['type'] = 'basic'; + $handler->display['display_options']['pager']['type'] = 'full'; + $handler->display['display_options']['style_plugin'] = 'default'; + $handler->display['display_options']['row_plugin'] = 'node'; + /* Sort criterion: Content: Sticky */ + $handler->display['display_options']['sorts']['sticky']['id'] = 'sticky'; + $handler->display['display_options']['sorts']['sticky']['table'] = 'node'; + $handler->display['display_options']['sorts']['sticky']['field'] = 'sticky'; + $handler->display['display_options']['sorts']['sticky']['order'] = 'DESC'; + /* Sort criterion: Content: Post date */ + $handler->display['display_options']['sorts']['created']['id'] = 'created'; + $handler->display['display_options']['sorts']['created']['table'] = 'node'; + $handler->display['display_options']['sorts']['created']['field'] = 'created'; + $handler->display['display_options']['sorts']['created']['order'] = 'DESC'; + /* Filter criterion: Content: Promoted to front page */ + $handler->display['display_options']['filters']['promote']['id'] = 'promote'; + $handler->display['display_options']['filters']['promote']['table'] = 'node'; + $handler->display['display_options']['filters']['promote']['field'] = 'promote'; + $handler->display['display_options']['filters']['promote']['value'] = '1'; + $handler->display['display_options']['filters']['promote']['group'] = 0; + $handler->display['display_options']['filters']['promote']['expose']['operator'] = FALSE; + /* Filter criterion: Content: Published */ + $handler->display['display_options']['filters']['status']['id'] = 'status'; + $handler->display['display_options']['filters']['status']['table'] = 'node'; + $handler->display['display_options']['filters']['status']['field'] = 'status'; + $handler->display['display_options']['filters']['status']['value'] = '1'; + $handler->display['display_options']['filters']['status']['group'] = 0; + $handler->display['display_options']['filters']['status']['expose']['operator'] = FALSE; + + /* Display: Page */ + $handler = $view->newDisplay('page', 'Page', 'page'); + $handler->display['display_options']['path'] = 'frontpage'; + + /* Display: Feed */ + $handler = $view->newDisplay('feed', 'Feed', 'feed'); + $handler->display['display_options']['defaults']['title'] = FALSE; + $handler->display['display_options']['title'] = 'Front page feed'; + $handler->display['display_options']['pager']['type'] = 'some'; + $handler->display['display_options']['style_plugin'] = 'rss'; + $handler->display['display_options']['row_plugin'] = 'node_rss'; + $handler->display['display_options']['path'] = 'rss.xml'; + $handler->display['display_options']['displays'] = array( + 'default' => 'default', + 'page' => 'page', + ); + $handler->display['display_options']['sitename_title'] = '1'; + + // (Export ends here.) + + // Add view to list of views to provide. + $views[$view->name] = $view; + + // ...Repeat all of the above for each view the module should provide. + + // At the end, return array of default views. + return $views; +} + +/** + * Alter default views defined by other modules. + * + * This hook is called right before all default views are cached to the + * database. It takes a keyed array of views by reference. + * + * Example usage to add a field to a view: + * @code + * $handler =& $view->display['DISPLAY_ID']->handler; + * // Add the user name field to the view. + * $handler->display['display_options']['fields']['name']['id'] = 'name'; + * $handler->display['display_options']['fields']['name']['table'] = 'users'; + * $handler->display['display_options']['fields']['name']['field'] = 'name'; + * $handler->display['display_options']['fields']['name']['label'] = 'Author'; + * $handler->display['display_options']['fields']['name']['link_to_user'] = 1; + * @endcode + */ +function hook_views_default_views_alter(&$views) { + if (isset($views['taxonomy_term'])) { + $views['taxonomy_term']->display['default']['display_options']['title'] = 'Categories'; + } +} + +/** + * Performs replacements in the query before being performed. + * + * @param $view + * The View being executed. + * @return + * An array with keys being the strings to replace, and the values the strings + * to replace them with. The strings to replace are ofted surrounded with + * '***', as illustrated in the example implementation. + */ +function hook_views_query_substitutions($view) { + // Example from views_views_query_substitutions(). + return array( + '***CURRENT_VERSION***' => VERSION, + '***CURRENT_TIME***' => REQUEST_TIME, + '***CURRENT_LANGUAGE***' => language(LANGUAGE_TYPE_CONTENT)->langcode, + '***DEFAULT_LANGUAGE***' => language_default()->langcode, + ); +} + +/** + * This hook is called to get a list of placeholders and their substitutions, + * used when preprocessing a View with form elements. + * + * @return + * An array with keys being the strings to replace, and the values the strings + * to replace them with. + */ +function hook_views_form_substitutions() { + return array( + '' => 'Example Substitution', + ); +} + +/** + * Allows altering a view at the very beginning of views processing, before + * anything is done. + * + * Adding output to the view can be accomplished by placing text on + * $view->attachment_before and $view->attachment_after. + * @param $view + * The view object about to be processed. + * @param $display_id + * The machine name of the active display. + * @param $args + * An array of arguments passed into the view. + */ +function hook_views_pre_view(&$view, &$display_id, &$args) { + // Change the display if the acting user has 'administer site configuration' + // permission, to display something radically different. + // (Note that this is not necessarily the best way to solve that task. Feel + // free to contribute another example!) + if ( + $view->name == 'my_special_view' && + user_access('administer site configuration') && + $display_id == 'public_display' + ) { + $display_id = 'private_display'; + } +} + +/** + * This hook is called right before the build process, but after displays + * are attached and the display performs its pre_execute phase. + * + * Adding output to the view can be accomplished by placing text on + * $view->attachment_before and $view->attachment_after. + * @param $view + * The view object about to be processed. + */ +function hook_views_pre_build(&$view) { + // Because of some unexplicable business logic, we should remove all + // attachments from all views on Mondays. + // (This alter could be done later in the execution process as well.) + if (date('D') == 'Mon') { + unset($view->attachment_before); + unset($view->attachment_after); + } +} + +/** + * This hook is called right after the build process. The query is now fully + * built, but it has not yet been run through db_rewrite_sql. + * + * Adding output to the view can be accomplished by placing text on + * $view->attachment_before and $view->attachment_after. + * @param $view + * The view object about to be processed. + */ +function hook_views_post_build(&$view) { + // If the exposed field 'type' is set, hide the column containing the content + // type. (Note that this is a solution for a particular view, and makes + // assumptions about both exposed filter settings and the fields in the view. + // Also note that this alter could be done at any point before the view being + // rendered.) + if ($view->name == 'my_view' && isset($view->exposed_raw_input['type']) && $view->exposed_raw_input['type'] != 'All') { + // 'Type' should be interpreted as content type. + if (isset($view->field['type'])) { + $view->field['type']->options['exclude'] = TRUE; + } + } +} + +/** + * This hook is called right before the execute process. The query is now fully + * built, but it has not yet been run through db_rewrite_sql. + * + * Adding output to the view can be accomplished by placing text on + * $view->attachment_before and $view->attachment_after. + * @param $view + * The view object about to be processed. + */ +function hook_views_pre_execute(&$view) { + // Whenever a view queries more than two tables, show a message that notifies + // view administrators that the query might be heavy. + // (This action could be performed later in the execution process, but not + // earlier.) + if (count($view->query->tables) > 2 && user_access('administer views')) { + drupal_set_message(t('The view %view may be heavy to execute.', array('%view' => $view->name)), 'warning'); + } +} + +/** + * This hook is called right after the execute process. The query has + * been executed, but the pre_render() phase has not yet happened for + * handlers. + * + * Adding output to the view can be accomplished by placing text on + * $view->attachment_before and $view->attachment_after. Altering the + * content can be achieved by editing the items of $view->result. + * @param $view + * The view object about to be processed. + */ +function hook_views_post_execute(&$view) { + // If there are more than 100 results, show a message that encourages the user + // to change the filter settings. + // (This action could be performed later in the execution process, but not + // earlier.) + if ($view->total_rows > 100) { + drupal_set_message(t('You have more than 100 hits. Use the filter settings to narrow down your list.')); + } +} + +/** + * This hook is called right before the render process. The query has been + * executed, and the pre_render() phase has already happened for handlers, so + * all data should be available. + * + * Adding output to the view can be accomplished by placing text on + * $view->attachment_before and $view->attachment_after. Altering the content + * can be achieved by editing the items of $view->result. + * + * This hook can be utilized by themes. + * @param $view + * The view object about to be processed. + */ +function hook_views_pre_render(&$view) { + // Scramble the order of the rows shown on this result page. + // Note that this could be done earlier, but not later in the view execution + // process. + shuffle($view->result); +} + +/** + * Post process any rendered data. + * + * This can be valuable to be able to cache a view and still have some level of + * dynamic output. In an ideal world, the actual output will include HTML + * comment based tokens, and then the post process can replace those tokens. + * + * Example usage. If it is known that the view is a node view and that the + * primary field will be a nid, you can do something like this: + * + * + * + * And then in the post render, create an array with the text that should + * go there: + * + * strtr($output, array('' => 'output for FIELD of nid 1'); + * + * All of the cached result data will be available in $view->result, as well, + * so all ids used in the query should be discoverable. + * + * This hook can be utilized by themes. + * @param $view + * The view object about to be processed. + * @param $output + * A flat string with the rendered output of the view. + * @param $cache + * The cache settings. + */ +function hook_views_post_render(&$view, &$output, &$cache) { + // When using full pager, disable any time-based caching if there are less + // then 10 results. + if ($view->pager instanceof Drupal\views\Plugin\views\pager\Full && $cache->options['type'] == 'time' && count($view->result) < 10) { + $cache['options']['results_lifespan'] = 0; + $cache['options']['output_lifespan'] = 0; + } +} + +/** + * Alter the query before executing the query. + * + * This hook should be placed in MODULENAME.views.inc and it will be + * auto-loaded. MODULENAME.views.inc must be in the directory specified by the + * 'path' key returned by MODULENAME_views_api(), or the same directory as the + * .module file, if 'path' is unspecified. + * + * @param $view + * The view object about to be processed. + * @param $query + * An object describing the query. + * @see hook_views_query_substitutions() + */ +function hook_views_query_alter(&$view, &$query) { + // (Example assuming a view with an exposed filter on node title.) + // If the input for the title filter is a positive integer, filter against + // node ID instead of node title. + if ($view->name == 'my_view' && is_numeric($view->exposed_raw_input['title']) && $view->exposed_raw_input['title'] > 0) { + // Traverse through the 'where' part of the query. + foreach ($query->where as &$condition_group) { + foreach ($condition_group['conditions'] as &$condition) { + // If this is the part of the query filtering on title, chang the + // condition to filter on node ID. + if ($condition['field'] == 'node.title') { + $condition = array( + 'field' => 'node.nid', + 'value' => $view->exposed_raw_input['title'], + 'operator' => '=', + ); + } + } + } + } +} + +/** + * Alter the information box that (optionally) appears with a view preview, + * including query and performance statistics. + * + * This hook should be placed in MODULENAME.views.inc and it will be + * auto-loaded. MODULENAME.views.inc must be in the directory specified by the + * 'path' key returned by MODULENAME_views_api(), or the same directory as the + * .module file, if 'path' is unspecified. + * + * Warning: $view is not a reference in PHP4 and cannot be modified here. But it + * IS a reference in PHP5, and can be modified. Please be careful with it. + * + * @param $rows + * An associative array with two keys: + * - query: An array of rows suitable for theme('table'), containing + * information about the query and the display title and path. + * - statistics: An array of rows suitable for theme('table'), containing + * performance statistics. + * @param $view + * The view object. + * @see theme_table() + */ +function hook_views_preview_info_alter(&$rows, $view) { + // Adds information about the tables being queried by the view to the query + // part of the info box. + $rows['query'][] = array( + t('Table queue'), + count($view->query->table_queue) . ': (' . implode(', ', array_keys($view->query->table_queue)) . ')', + ); +} + +/** + * This hooks allows to alter the links at the top of the view edit form. Some + * modules might want to add links there. + * + * @param $links + * An array of links which will be displayed at the top of the view edit form. + * Each entry should be on a form suitable for theme('link'). + * @param view $view + * The full view object which is currently edited. + * @param $display_id + * The current display id which is edited. For example that's 'default' or + * 'page_1'. + */ +function hook_views_ui_display_top_links_alter(&$links, $view, $display_id) { + // Put the export link first in the list. + if (isset($links['export'])) { + $links = array('export' => $links['export']) + $links; + } +} + +/** + * This hook allows to alter the commands which are used on a views ajax + * request. + * + * @param $commands + * An array of ajax commands + * @param $view view + * The view which is requested. + */ +function hook_views_ajax_data_alter(&$commands, $view) { + // Replace Views' method for scrolling to the top of the element with your + // custom scrolling method. + foreach ($commands as &$command) { + if ($command['method'] == 'viewsScrollTop') { + $command['method'] .= 'myScrollTop'; + } + } +} + +/** + * Allow modules to respond to the Views cache being invalidated. + * + * This hook should fire whenever a view is enabled, disabled, created, + * updated, or deleted. + * + * @see views_invalidate_cache() + */ +function hook_views_invalidate_cache() { + cache('mymodule')->invalidateTags(array('views' => TRUE)); +} + +/** + * @} + */ + +/** + * @defgroup views_module_handlers Views module handlers + * @{ + * Handlers exposed by various modules to Views. + * @} + */ diff --git a/core/modules/views/views.info b/core/modules/views/views.info new file mode 100644 index 0000000..18845c8 --- /dev/null +++ b/core/modules/views/views.info @@ -0,0 +1,7 @@ +name = Views +description = Create customized lists and queries from your database. +package = Core +version = VERSION +core = 8.x +dependencies[] = config +stylesheets[all][] = css/views.base.css diff --git a/core/modules/views/views.install b/core/modules/views/views.install new file mode 100644 index 0000000..9469340 --- /dev/null +++ b/core/modules/views/views.install @@ -0,0 +1,78 @@ + 'ui.show.listing_filters', + 'views_ui_show_master_display' => 'ui.show.master_display', + 'views_ui_show_advanced_column' => 'ui.show.advanced_column', + 'views_ui_display_embed' => 'ui.show.display_embed', + 'views_ui_custom_theme' => 'ui.custom_theme', + 'views_exposed_filter_any_label' => 'exposed_filter_any_label', + 'views_ui_always_live_preview' => 'ui.always_live_preview', + 'views_ui_always_live_preview_button' => 'ui.always_live_preview_button', + 'views_ui_show_preview_information' => 'ui.show.preview_information', + 'views_ui_show_sql_query_where' => 'ui.show.sql_query.where', + 'views_ui_show_sql_query' => 'ui.show_sql.query.enabled', + 'views_ui_show_performance_statistics' => 'ui.show.performance_statistics', + 'views_show_additional_queries' => 'ui.show.additional_queries', + 'views_skip_cache' => 'skip_cache', + 'views_sql_signature' => 'sql_signature', + 'views_no_javascript' => 'no_javascript', + 'views_devel_output' => 'debug.output', + 'views_devel_region' => 'debug.region', + 'views_display_extenders' => 'display_extenders', + )); +} + +/** + * Rename the {cache_views} and {cache_views_data} tables. + */ +function views_update_8001() { + db_rename_table('cache_views', 'cache_views_info'); + db_rename_table('cache_views_data', 'cache_views_results'); +} + +/** + * Remove the {views_view} and {views_display} table. + */ +function views_update_8002() { + db_drop_table('views_view'); + db_drop_table('views_display'); +} diff --git a/core/modules/views/views.module b/core/modules/views/views.module new file mode 100644 index 0000000..6e72762 --- /dev/null +++ b/core/modules/views/views.module @@ -0,0 +1,2273 @@ + array( + 'label' => t('View'), + 'entity class' => 'Drupal\views\ViewStorage', + 'controller class' => 'Drupal\views\ViewStorageController', + 'list controller class' => 'Drupal\views_ui\ViewListController', + 'list path' => 'admin/structure/views', + 'form controller class' => array( + 'default' => 'Drupal\node\NodeFormController', + ), + 'config prefix' => 'views.view', + 'fieldable' => FALSE, + 'entity keys' => array( + 'id' => 'name', + 'label' => 'human_name', + 'uuid' => 'uuid', + ), + ), + ); + + return $return; +} + +/** + * Implements hook_forms(). + * + * To provide distinct form IDs for Views forms, the View name and + * specific display name are appended to the base ID, + * views_form_views_form. When such a form is built or submitted, this + * function will return the proper callback function to use for the given form. + */ +function views_forms($form_id, $args) { + if (strpos($form_id, 'views_form_') === 0) { + return array( + $form_id => array( + 'callback' => 'views_form', + ), + ); + } +} + +/** + * Returns a form ID for a Views form using the name and display of the View. + */ +function views_form_id($view) { + $parts = array( + 'views_form', + $view->storage->name, + $view->current_display, + ); + + return implode('_', $parts); +} + +/** + * Implements hook_element_info(). + */ +function views_element_info() { + $types['view'] = array( + '#theme_wrappers' => array('container'), + '#pre_render' => array('views_pre_render_view_element'), + '#name' => NULL, + '#display_id' => 'default', + '#arguments' => array(), + ); + return $types; +} + +/** + * View element pre render callback. + */ +function views_pre_render_view_element($element) { + $element['#attributes']['class'][] = 'views-element-container'; + + $view = views_get_view($element['#name']); + if ($view && $view->access($element['#display_id'])) { + $element['view']['#markup'] = $view->preview($element['#display_id'], $element['#arguments']); + } + + return $element; +} + +/** + * Implement hook_theme(). Register views theming functions. + */ +function views_theme($existing, $type, $theme, $path) { + $path = drupal_get_path('module', 'views'); + module_load_include('inc', 'views', 'theme/theme'); + + // Some quasi clever array merging here. + $base = array( + 'file' => 'theme.inc', + 'path' => $path . '/theme', + ); + + // Our extra version of pager from pager.inc + $hooks['views_mini_pager'] = $base + array( + 'variables' => array('tags' => array(), 'quantity' => 10, 'element' => 0, 'parameters' => array()), + 'pattern' => 'views_mini_pager__', + ); + + $variables = array( + // For displays, we pass in a dummy array as the first parameter, since + // $view is an object but the core contextual_preprocess() function only + // attaches contextual links when the primary theme argument is an array. + 'display' => array('view_array' => array(), 'view' => NULL), + 'style' => array('view' => NULL, 'options' => NULL, 'rows' => NULL, 'title' => NULL), + 'row' => array('view' => NULL, 'options' => NULL, 'row' => NULL, 'field_alias' => NULL), + 'exposed_form' => array('view' => NULL, 'options' => NULL), + 'pager' => array( + 'view' => NULL, 'options' => NULL, + 'tags' => array(), 'quantity' => 10, 'element' => 0, 'parameters' => array() + ), + ); + + // Default view themes + $hooks['views_view_field'] = $base + array( + 'pattern' => 'views_view_field__', + 'variables' => array('view' => NULL, 'field' => NULL, 'row' => NULL), + ); + $hooks['views_view_grouping'] = $base + array( + 'pattern' => 'views_view_grouping__', + 'variables' => array('view' => NULL, 'grouping' => NULL, 'grouping_level' => NULL, 'rows' => NULL, 'title' => NULL), + ); + + $plugins = views_get_plugin_definitions(); + + // Register theme functions for all style plugins + foreach ($plugins as $type => $info) { + foreach ($info as $plugin => $def) { + if (isset($def['theme']) && (!isset($def['register theme']) || !empty($def['register theme']))) { + $hooks[$def['theme']] = array( + 'pattern' => $def['theme'] . '__', + 'file' => $def['theme file'], + 'path' => $def['theme path'], + 'variables' => $variables[$type], + ); + + $include = DRUPAL_ROOT . '/' . $def['theme path'] . '/' . $def['theme file']; + if (file_exists($include)) { + require_once $include; + } + + if (!function_exists('theme_' . $def['theme'])) { + $hooks[$def['theme']]['template'] = drupal_clean_css_identifier($def['theme']); + } + } + if (isset($def['additional themes'])) { + foreach ($def['additional themes'] as $theme => $theme_type) { + if (empty($theme_type)) { + $theme = $theme_type; + $theme_type = $type; + } + + $hooks[$theme] = array( + 'pattern' => $theme . '__', + 'file' => $def['theme file'], + 'path' => $def['theme path'], + 'variables' => $variables[$theme_type], + ); + + if (!function_exists('theme_' . $theme)) { + $hooks[$theme]['template'] = drupal_clean_css_identifier($theme); + } + } + } + } + } + + $hooks['views_form_views_form'] = $base + array( + 'render element' => 'form', + ); + + $hooks['views_exposed_form'] = $base + array( + 'template' => 'views-exposed-form', + 'pattern' => 'views_exposed_form__', + 'render element' => 'form', + ); + + $hooks['views_more'] = $base + array( + 'template' => 'views-more', + 'pattern' => 'views_more__', + 'variables' => array('more_url' => NULL, 'link_text' => 'more', 'view' => NULL), + ); + + return $hooks; +} + +/** + * Scans a directory of a module for template files. + * + * @param $cache + * The existing cache of theme hooks to test against. + * @param $path + * The path to search. + * + * @see drupal_find_theme_templates() + */ +function _views_find_module_templates($cache, $path) { + $templates = array(); + $regex = '/' . '\.tpl\.php' . '$' . '/'; + + // @todo Remove this once #1626580 is committed. For now, We need to remove + // the sites/* part of the path because drupal_system_listing() is already + // adding that. + $path = preg_replace('/^sites\/all\//', '', $path); + $config = str_replace('/', '\/', conf_path()); + $path = preg_replace('/^' . $config . '\//', '', $path); + + // Because drupal_system_listing works the way it does, we check for real + // templates separately from checking for patterns. + $files = drupal_system_listing($regex, $path, 'name', 0); + foreach ($files as $template => $file) { + // Chop off the remaining extensions if there are any. $template already + // has the rightmost extension removed, but there might still be more, + // such as with .tpl.php, which still has .tpl in $template at this point. + if (($pos = strpos($template, '.')) !== FALSE) { + $template = substr($template, 0, $pos); + } + // Transform - in filenames to _ to match function naming scheme + // for the purposes of searching. + $hook = strtr($template, '-', '_'); + if (isset($cache[$hook])) { + $templates[$hook] = array( + 'template' => $template, + 'path' => dirname($file->filename), + 'includes' => isset($cache[$hook]['includes']) ? $cache[$hook]['includes'] : NULL, + ); + } + // Ensure that the pattern is maintained from base themes to its sub-themes. + // Each sub-theme will have their templates scanned so the pattern must be + // held for subsequent runs. + if (isset($cache[$hook]['pattern'])) { + $templates[$hook]['pattern'] = $cache[$hook]['pattern']; + } + } + + $patterns = array_keys($files); + + foreach ($cache as $hook => $info) { + if (!empty($info['pattern'])) { + // Transform _ in pattern to - to match file naming scheme + // for the purposes of searching. + $pattern = strtr($info['pattern'], '_', '-'); + + $matches = preg_grep('/^'. $pattern .'/', $patterns); + if ($matches) { + foreach ($matches as $match) { + $file = substr($match, 0, strpos($match, '.')); + // Put the underscores back in for the hook name and register this pattern. + $templates[strtr($file, '-', '_')] = array( + 'template' => $file, + 'path' => dirname($files[$match]->uri), + 'variables' => isset($info['variables']) ? $info['variables'] : NULL, + 'render element' => isset($info['render element']) ? $info['render element'] : NULL, + 'base hook' => $hook, + 'includes' => isset($info['includes']) ? $info['includes'] : NULL, + ); + } + } + } + } + + return $templates; +} + +/** + * Returns a list of plugins and metadata about them. + * + * @return array + * An array keyed by PLUGIN_TYPE:PLUGIN_NAME, like 'display:page' or + * 'pager:full', containing an array with the following keys: + * - title: The plugin's title. + * - type: The plugin type. + * - module: The module providing the plugin. + * - views: An array of enabled Views that are currently using this plugin, + * keyed by machine name. + */ +function views_plugin_list() { + $plugin_data = views_get_plugin_definitions(); + $plugins = array(); + foreach (views_get_enabled_views() as $view) { + foreach ($view->display as $display_id => $display) { + foreach ($plugin_data as $type => $info) { + if ($type == 'display' && isset($display['display_plugin'])) { + $name = $display['display_plugin']; + } + elseif (isset($display['display_options']["{$type}_plugin"])) { + $name = $display['display_options']["{$type}_plugin"]; + } + elseif (isset($display['display_options'][$type]['type'])) { + $name = $display['display_options'][$type]['type']; + } + else { + continue; + } + + // Key first by the plugin type, then the name. + $key = $type . ':' . $name; + // Add info for this plugin. + if (!isset($plugins[$key])) { + $plugins[$key] = array( + 'type' => $type, + 'title' => check_plain($info[$name]['title']), + 'module' => check_plain($info[$name]['module']), + 'views' => array(), + ); + } + + // Add this view to the list for this plugin. + $plugins[$key]['views'][$view->storage->name] = $view->storage->name; + } + } + } + return $plugins; +} + +/** + * A theme preprocess function to automatically allow view-based node + * templates if called from a view. + * + * The 'modules/node.views.inc' file is a better place for this, but + * we haven't got a chance to load that file before Drupal builds the + * node portion of the theme registry. + */ +function views_preprocess_node(&$vars) { + // The 'view' attribute of the node is added in views_preprocess_node() + if (!empty($vars['node']->view) && !empty($vars['node']->view->storage->name)) { + $vars['view'] = $vars['node']->view; + $vars['theme_hook_suggestions'][] = 'node__view__' . $vars['node']->view->storage->name; + if (!empty($vars['node']->view->current_display)) { + $vars['theme_hook_suggestions'][] = 'node__view__' . $vars['node']->view->storage->name . '__' . $vars['node']->view->current_display; + + // If a node is being rendered in a view, and the view does not have a path, + // prevent drupal from accidentally setting the $page variable: + if ($vars['page'] && $vars['view_mode'] == 'full' && !$vars['view']->display_handler->hasPath()) { + $vars['page'] = FALSE; + } + } + } + + // Allow to alter comments and links based on the settings in the row plugin. + if (!empty($vars['view']->style_plugin->row_plugin) && get_class($vars['view']->style_plugin->row_plugin) == 'views_plugin_row_node_view') { + node_row_node_view_preprocess_node($vars); + } +} + +/** + * A theme preprocess function to automatically allow view-based node + * templates if called from a view. + */ +function views_preprocess_comment(&$vars) { + // The 'view' attribute of the node is added in template_preprocess_views_view_row_comment() + if (!empty($vars['node']->view) && !empty($vars['node']->view->storage->name)) { + $vars['view'] = &$vars['node']->view; + $vars['theme_hook_suggestions'][] = 'comment__view__' . $vars['node']->view->storage->name; + if (!empty($vars['node']->view->current_display)) { + $vars['theme_hook_suggestions'][] = 'comment__view__' . $vars['node']->view->storage->name . '__' . $vars['node']->view->current_display; + } + } +} + +/** + * Implement hook_permission(). + */ +function views_permission() { + return array( + 'administer views' => array( + 'title' => t('Administer views'), + 'description' => t('Access the views administration pages.'), + ), + 'access all views' => array( + 'title' => t('Bypass views access control'), + 'description' => t('Bypass access control when accessing views.'), + ), + ); +} + +/** + * Implement hook_menu(). + */ +function views_menu() { + // Any event which causes a menu rebuild could potentially mean that the + // Views data is updated -- module changes, profile changes, etc. + views_invalidate_cache(); + $items = array(); + $items['views/ajax'] = array( + 'title' => 'Views', + 'page callback' => 'views_ajax', + 'theme callback' => 'ajax_base_page_theme', + 'delivery callback' => 'ajax_deliver', + 'access callback' => TRUE, + 'description' => 'Ajax callback for view loading.', + 'type' => MENU_CALLBACK, + 'file' => 'includes/ajax.inc', + ); + // Path is not admin/structure/views due to menu complications with the wildcards from + // the generic ajax callback. + $items['admin/views/ajax/autocomplete/user'] = array( + 'page callback' => 'views_ajax_autocomplete_user', + 'theme callback' => 'ajax_base_page_theme', + 'access callback' => 'user_access', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + 'file' => 'includes/ajax.inc', + ); + // Define another taxonomy autocomplete because the default one of drupal + // does not support a vid a argument anymore + $items['admin/views/ajax/autocomplete/taxonomy'] = array( + 'page callback' => 'views_ajax_autocomplete_taxonomy', + 'theme callback' => 'ajax_base_page_theme', + 'access callback' => 'user_access', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + 'file' => 'includes/ajax.inc', + ); + return $items; +} + +/** + * Implement hook_menu_alter(). + */ +function views_menu_alter(&$callbacks) { + $our_paths = array(); + $views = views_get_applicable_views('uses_hook_menu'); + foreach ($views as $data) { + list($view, $display_id) = $data; + $result = $view->executeHookMenu($display_id, $callbacks); + if (is_array($result)) { + // The menu system doesn't support having two otherwise + // identical paths with different placeholders. So we + // want to remove the existing items from the menu whose + // paths would conflict with ours. + + // First, we must find any existing menu items that may + // conflict. We use a regular expression because we don't + // know what placeholders they might use. Note that we + // first construct the regex itself by replacing %views_arg + // in the display path, then we use this constructed regex + // (which will be something like '#^(foo/%[^/]*/bar)$#') to + // search through the existing paths. + $regex = '#^(' . preg_replace('#%views_arg#', '%[^/]*', implode('|', array_keys($result))) . ')$#'; + $matches = preg_grep($regex, array_keys($callbacks)); + + // Remove any conflicting items that were found. + foreach ($matches as $path) { + // Don't remove the paths we just added! + if (!isset($our_paths[$path])) { + unset($callbacks[$path]); + } + } + foreach ($result as $path => $item) { + if (!isset($callbacks[$path])) { + // Add a new item, possibly replacing (and thus effectively + // overriding) one that we removed above. + $callbacks[$path] = $item; + } + else { + // This item already exists, so it must be one that we added. + // We change the various callback arguments to pass an array + // of possible display IDs instead of a single ID. + $callbacks[$path]['page arguments'][1] = (array)$callbacks[$path]['page arguments'][1]; + $callbacks[$path]['page arguments'][1][] = $display_id; + $callbacks[$path]['access arguments'][] = $item['access arguments'][0]; + $callbacks[$path]['load arguments'][1] = (array)$callbacks[$path]['load arguments'][1]; + $callbacks[$path]['load arguments'][1][] = $display_id; + } + $our_paths[$path] = TRUE; + } + } + } + + // Save memory: Destroy those views. + foreach ($views as $data) { + list($view, $display_id) = $data; + $view->destroy(); + } +} + +/** + * Helper function for menu loading. This will automatically be + * called in order to 'load' a views argument; primarily it + * will be used to perform validation. + * + * @param $value + * The actual value passed. + * @param $name + * The name of the view. This needs to be specified in the 'load function' + * of the menu entry. + * @param $display_id + * The display id that will be loaded for this menu item. + * @param $index + * The menu argument index. This counts from 1. + */ +function views_arg_load($value, $name, $display_id, $index) { + static $views = array(); + + // Make sure we haven't already loaded this views argument for a similar menu + // item elsewhere. + $key = $name . ':' . $display_id . ':' . $value . ':' . $index; + if (isset($views[$key])) { + return $views[$key]; + } + + if ($view = views_get_view($name)) { + $view->setDisplay($display_id); + $view->initHandlers(); + + $ids = array_keys($view->argument); + + $indexes = array(); + $path = explode('/', $view->getPath()); + + foreach ($path as $id => $piece) { + if ($piece == '%' && !empty($ids)) { + $indexes[$id] = array_shift($ids); + } + } + + if (isset($indexes[$index])) { + if (isset($view->argument[$indexes[$index]])) { + $arg = $view->argument[$indexes[$index]]->validate_argument($value) ? $value : FALSE; + $view->destroy(); + + // Store the output in case we load this same menu item again. + $views[$key] = $arg; + return $arg; + } + } + $view->destroy(); + } +} + +/** + * Page callback: Displays a page view, given a name and display id. + * + * @param $name + * The name of a view. + * @param $display_id + * The display id of a view. + * + * @return + * Either the HTML of a fully-executed view, or MENU_NOT_FOUND. + */ +function views_page($name, $display_id) { + $args = func_get_args(); + // Remove $name and $display_id from the arguments. + array_shift($args); + array_shift($args); + + // Load the view and render it. + if ($view = views_get_view($name)) { + return $view->executeDisplay($display_id, $args); + } + + // Fallback; if we get here no view was found or handler was not valid. + return MENU_NOT_FOUND; +} + +/** + * Implements hook_page_alter(). + */ +function views_page_alter(&$page) { + // If the main content of this page contains a view, attach its contextual + // links to the overall page array. This allows them to be rendered directly + // next to the page title. + $view = views_get_page_view(); + if (!empty($view)) { + // If a module is still putting in the display like we used to, catch that. + if (is_subclass_of($view, 'views_plugin_display')) { + $view = $view->view; + } + + views_add_contextual_links($page, 'page', $view, $view->current_display); + } +} + +/** + * Implements MODULE_preprocess_HOOK(). + */ +function views_preprocess_html(&$variables) { + // If the page contains a view as its main content, contextual links may have + // been attached to the page as a whole; for example, by views_page_alter(). + // This allows them to be associated with the page and rendered by default + // next to the page title (which we want). However, it also causes the + // Contextual Links module to treat the wrapper for the entire page (i.e., + // the tag) as the HTML element that these contextual links are + // associated with. This we don't want; for better visual highlighting, we + // prefer a smaller region to be chosen. The region we prefer differs from + // theme to theme and depends on the details of the theme's markup in + // page.tpl.php, so we can only find it using JavaScript. We therefore remove + // the "contextual-region" class from the tag here and add + // JavaScript that will insert it back in the correct place. + if (!empty($variables['page']['#views_contextual_links_info'])) { + $key = array_search('contextual-region', $variables['attributes']['class']->value()); + if ($key !== FALSE) { + unset($variables['attributes']['class'][$key]); + // Add the JavaScript, with a group and weight such that it will run + // before modules/contextual/contextual.js. + drupal_add_js(drupal_get_path('module', 'views') . '/js/views-contextual.js', array('group' => JS_LIBRARY, 'weight' => -1)); + } + } +} + +/** + * Implements hook_contextual_links_view_alter(). + */ +function views_contextual_links_view_alter(&$element, $items) { + // If we are rendering views-related contextual links attached to the overall + // page array, add a class to the list of contextual links. This will be used + // by the JavaScript added in views_preprocess_html(). + if (!empty($element['#element']['#views_contextual_links_info']) && !empty($element['#element']['#type']) && $element['#element']['#type'] == 'page') { + $element['#attributes']['class'][] = 'views-contextual-links-page'; + } +} + +/** + * Implement hook_block_info(). + */ +function views_block_info() { + // Try to avoid instantiating all the views just to get the blocks info. + views_include('cache'); + $cache = views_cache_get('views_block_items', TRUE); + if ($cache && is_array($cache->data)) { + return $cache->data; + } + + $items = array(); + $views = views_get_all_views(); + foreach ($views as $view) { + // disabled views get nothing. + if (!$view->isEnabled()) { + continue; + } + + $view->initDisplay(); + foreach ($view->getExecutable()->displayHandlers as $display) { + + if (isset($display) && !empty($display->definition['uses_hook_block'])) { + $result = $display->executeHookBlockList(); + if (is_array($result)) { + $items = array_merge($items, $result); + } + } + + if (isset($display) && $display->getOption('exposed_block')) { + $result = $display->getSpecialBlocks(); + if (is_array($result)) { + $items = array_merge($items, $result); + } + } + } + } + + // block.module has a delta length limit of 32, but our deltas can + // unfortunately be longer because view names can be 32 and display IDs + // can also be 32. So for very long deltas, change to md5 hashes. + $hashes = array(); + + // get the keys because we're modifying the array and we don't want to + // confuse PHP too much. + $keys = array_keys($items); + foreach ($keys as $delta) { + if (strlen($delta) >= 32) { + $hash = md5($delta); + $hashes[$hash] = $delta; + $items[$hash] = $items[$delta]; + unset($items[$delta]); + } + } + + // Only save hashes if they have changed. + $old_hashes = variable_get('views_block_hashes', array()); + if ($hashes != $old_hashes) { + variable_set('views_block_hashes', $hashes); + } + + views_cache_set('views_block_items', $items, TRUE); + + return $items; +} + +/** + * Implement hook_block_view(). + */ +function views_block_view($delta) { + $start = microtime(TRUE); + // if this is 32, this should be an md5 hash. + if (strlen($delta) == 32) { + $hashes = variable_get('views_block_hashes', array()); + if (!empty($hashes[$delta])) { + $delta = $hashes[$delta]; + } + } + + // This indicates it's a special one. + if (substr($delta, 0, 1) == '-') { + list($nothing, $type, $name, $display_id) = explode('-', $delta); + // Put the - back on. + $type = '-' . $type; + if ($view = views_get_view($name)) { + if ($view->access($display_id)) { + $view->setDisplay($display_id); + if (isset($view->display_handler)) { + $output = $view->display_handler->viewSpecialBlocks($type); + // Before returning the block output, convert it to a renderable + // array with contextual links. + views_add_block_contextual_links($output, $view, $display_id, 'special_block_' . $type); + $view->destroy(); + return $output; + } + } + $view->destroy(); + } + } + + // If the delta doesn't contain valid data return nothing. + $explode = explode('-', $delta); + if (count($explode) != 2) { + return; + } + list($name, $display_id) = $explode; + // Load the view + if ($view = views_get_view($name)) { + if ($view->access($display_id)) { + $output = $view->executeDisplay($display_id); + // Before returning the block output, convert it to a renderable array + // with contextual links. + views_add_block_contextual_links($output, $view, $display_id); + $view->destroy(); + return $output; + } + $view->destroy(); + } +} + +/** + * Converts Views block content to a renderable array with contextual links. + * + * @param $block + * An array representing the block, with the same structure as the return + * value of hook_block_view(). This will be modified so as to force + * $block['content'] to be a renderable array, containing the optional + * '#contextual_links' property (if there are any contextual links associated + * with the block). + * @param $view + * The view that was used to generate the block content. + * @param $display_id + * The ID of the display within the view that was used to generate the block + * content. + * @param $block_type + * The type of the block. If it's block it's a regular views display, + * but 'special_block_-exp' exist as well. + */ +function views_add_block_contextual_links(&$block, ViewExecutable $view, $display_id, $block_type = 'block') { + // Do not add contextual links to an empty block. + if (!empty($block['content'])) { + // Contextual links only work on blocks whose content is a renderable + // array, so if the block contains a string of already-rendered markup, + // convert it to an array. + if (is_string($block['content'])) { + $block['content'] = array('#markup' => $block['content']); + } + // Add the contextual links. + views_add_contextual_links($block['content'], $block_type, $view, $display_id); + } +} + +/** + * Adds contextual links associated with a view display to a renderable array. + * + * This function should be called when a view is being rendered in a particular + * location and you want to attach the appropriate contextual links (e.g., + * links for editing the view) to it. + * + * The function operates by checking the view's display plugin to see if it has + * defined any contextual links that are intended to be displayed in the + * requested location; if so, it attaches them. The contextual links intended + * for a particular location are defined by the 'contextual links' and + * 'contextual_links_locations' properties in hook_views_plugins() and + * hook_views_plugins_alter(); as a result, these hook implementations have + * full control over where and how contextual links are rendered for each + * display. + * + * In addition to attaching the contextual links to the passed-in array (via + * the standard #contextual_links property), this function also attaches + * additional information via the #views_contextual_links_info property. This + * stores an array whose keys are the names of each module that provided + * views-related contextual links (same as the keys of the #contextual_links + * array itself) and whose values are themselves arrays whose keys ('location', + * 'view_name', and 'view_display_id') store the location, name of the view, + * and display ID that were passed in to this function. This allows you to + * access information about the contextual links and how they were generated in + * a variety of contexts where you might be manipulating the renderable array + * later on (for example, alter hooks which run later during the same page + * request). + * + * @param $render_element + * The renderable array to which contextual links will be added. This array + * should be suitable for passing in to drupal_render() and will normally + * contain a representation of the view display whose contextual links are + * being requested. + * @param $location + * The location in which the calling function intends to render the view and + * its contextual links. The core system supports three options for this + * parameter: + * - 'block': Used when rendering a block which contains a view. This + * retrieves any contextual links intended to be attached to the block + * itself. + * - 'page': Used when rendering the main content of a page which contains a + * view. This retrieves any contextual links intended to be attached to the + * page itself (for example, links which are displayed directly next to the + * page title). + * - 'view': Used when rendering the view itself, in any context. This + * retrieves any contextual links intended to be attached directly to the + * view. + * If you are rendering a view and its contextual links in another location, + * you can pass in a different value for this parameter. However, you will + * also need to use hook_views_plugins() or hook_views_plugins_alter() to + * declare, via the 'contextual_links_locations' array key, which view + * displays support having their contextual links rendered in the location + * you have defined. + * @param $view + * The view whose contextual links will be added. + * @param $display_id + * The ID of the display within $view whose contextual links will be added. + * + * @see hook_views_plugins() + * @see views_block_view() + * @see views_page_alter() + * @see template_preprocess_views_view() + */ +function views_add_contextual_links(&$render_element, $location, ViewExecutable $view, $display_id) { + // Do not do anything if the view is configured to hide its administrative + // links. + if (empty($view->hide_admin_links)) { + // Also do not do anything if the display plugin has not defined any + // contextual links that are intended to be displayed in the requested + // location. + $plugin = views_get_plugin_definition('display', $view->displayHandlers[$display_id]->display['display_plugin']); + // If contextual_links_locations are not set, provide a sane default. (To + // avoid displaying any contextual links at all, a display plugin can still + // set 'contextual_links_locations' to, e.g., {""}.) + + if (!isset($plugin['contextual_links_locations'])) { + $plugin['contextual_links_locations'] = array('view'); + } + elseif ($plugin['contextual_links_locations'] == array() || $plugin['contextual_links_locations'] == array('')) { + $plugin['contextual_links_locations'] = array(); + } + else { + $plugin += array('contextual_links_locations' => array('view')); + } + + // On exposed_forms blocks contextual links should always be visible. + $plugin['contextual_links_locations'][] = 'special_block_-exp'; + $has_links = !empty($plugin['contextual links']) && !empty($plugin['contextual_links_locations']); + if ($has_links && in_array($location, $plugin['contextual_links_locations'])) { + foreach ($plugin['contextual links'] as $module => $link) { + $args = array(); + $valid = TRUE; + if (!empty($link['argument properties'])) { + foreach ($link['argument properties'] as $property) { + // If the plugin is trying to create an invalid contextual link + // (for example, "path/to/{$view->storage->property}", where + // $view->storage->{property} does not exist), we cannot construct + // the link, so we skip it. + if (!property_exists($view->storage, $property)) { + $valid = FALSE; + break; + } + else { + $args[] = $view->storage->{$property}; + } + } + } + // If the link was valid, attach information about it to the renderable + // array. + if ($valid) { + $render_element['#contextual_links'][$module] = array($link['parent path'], $args); + $render_element['#views_contextual_links_info'][$module] = array( + 'location' => $location, + 'view' => $view, + 'view_name' => $view->storage->name, + 'view_display_id' => $display_id, + ); + } + } + } + } +} + +/** + * Returns an array of language names. + * + * This is a one to one copy of locale_language_list because we can't rely on enabled locale module. + * + * @param $field + * 'name' => names in current language, localized + * 'native' => native names + * @param $all + * Boolean to return all languages or only enabled ones + * + * @see locale_language_list() + * @todo Figure out whether we need this with language module. + */ +function views_language_list($field = 'name', $all = FALSE) { + if ($all) { + $languages = language_list(); + } + else { + $languages = language_list(); + } + $list = array(); + foreach ($languages as $language) { + $list[$language->langcode] = ($field == 'name') ? t($language->name) : $language->$field; + } + return $list; +} + +/** + * Implements hook_cache_flush(). + */ +function views_cache_flush() { + return array('views_info', 'views_results'); +} + +/** + * Implements hook_field_create_instance. + */ +function views_field_create_instance($instance) { + cache('views_info')->flush(); + cache('views_results')->flush(); +} + +/** + * Implements hook_field_update_instance. + */ +function views_field_update_instance($instance, $prior_instance) { + cache('views_info')->flush(); + cache('views_results')->flush(); +} + +/** + * Implements hook_field_delete_instance. + */ +function views_field_delete_instance($instance) { + cache('views_info')->flush(); + cache('views_results')->flush(); +} + +/** + * Invalidate the views cache, forcing a rebuild on the next grab of table data. + */ +function views_invalidate_cache() { + // Clear the views cache. + cache('views_info')->flush(); + + // Clear the page and block cache. + cache_invalidate(array('content' => TRUE)); + + // Set the menu as needed to be rebuilt. + state()->set('menu_rebuild_needed', TRUE); + + // Allow modules to respond to the Views cache being cleared. + module_invoke_all('views_invalidate_cache'); +} + +/** + * Access callback to determine if the user can import Views. + * + * View imports require an additional access check because they are PHP + * code and PHP is more locked down than administer views. + */ +function views_import_access() { + return user_access('administer views') && user_access('use PHP for settings'); +} + +/** + * Determine if the logged in user has access to a view. + * + * This function should only be called from a menu hook or some other + * embedded source. Each argument is the result of a call to + * views_plugin_access::get_access_callback() which is then used + * to determine if that display is accessible. If *any* argument + * is accessible, then the view is accessible. + */ +function views_access() { + $args = func_get_args(); + foreach ($args as $arg) { + if ($arg === TRUE) { + return TRUE; + } + + if (!is_array($arg)) { + continue; + } + + list($callback, $arguments) = $arg; + $arguments = $arguments ? $arguments : array(); + // Bring dynamic arguments to the access callback. + foreach ($arguments as $key => $value) { + if (is_int($value) && isset($args[$value])) { + $arguments[$key] = $args[$value]; + } + } + if (function_exists($callback) && call_user_func_array($callback, $arguments)) { + return TRUE; + } + } + + return FALSE; +} + +/** + * Access callback for the views_plugin_access_perm access plugin. + * + * Determine if the specified user has access to a view on the basis of + * permissions. If the $account argument is omitted, the current user + * is used. + */ +function views_check_perm($perm, $account = NULL) { + return user_access($perm, $account) || user_access('access all views', $account); +} + +/** + * Access callback for the views_plugin_access_role access plugin. + + * Determine if the specified user has access to a view on the basis of any of + * the requested roles. If the $account argument is omitted, the current user + * is used. + */ +function views_check_roles($rids, $account = NULL) { + global $user; + $account = isset($account) ? $account : $user; + $roles = array_keys($account->roles); + $roles[] = $account->uid ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID; + return user_access('access all views', $account) || array_intersect(array_filter($rids), $roles); +} +// ------------------------------------------------------------------ +// Functions to help identify views that are running or ran + +/** + * Set the current 'page view' that is being displayed so that it is easy + * for other modules or the theme to identify. + */ +function &views_set_page_view($view = NULL) { + static $cache = NULL; + if (isset($view)) { + $cache = $view; + } + + return $cache; +} + +/** + * Find out what, if any, page view is currently in use. Please note that + * this returns a reference, so be careful! You can unintentionally modify the + * $view object. + * + * @return Drupal\views\ViewExecutable + * A fully formed, empty $view object. + */ +function &views_get_page_view() { + return views_set_page_view(); +} + +/** + * Set the current 'current view' that is being built/rendered so that it is + * easy for other modules or items in drupal_eval to identify + * + * @return Drupal\views\ViewExecutable + */ +function &views_set_current_view($view = NULL) { + static $cache = NULL; + if (isset($view)) { + $cache = $view; + } + + return $cache; +} + +/** + * Find out what, if any, current view is currently in use. Please note that + * this returns a reference, so be careful! You can unintentionally modify the + * $view object. + * + * @return Drupal\views\ViewExecutable + */ +function &views_get_current_view() { + return views_set_current_view(); +} + +// ------------------------------------------------------------------ +// Include file helpers + +/** + * Include views .inc files as necessary. + */ +function views_include($file) { + module_load_include('inc', 'views', 'includes/' . $file); +} + +/** + * Implements hook_hook_info(). + */ +function views_hook_info() { + $hooks['views_data'] = array( + 'group' => 'views', + ); + + return $hooks; +} + +/** + * Include views .css files. + */ +function views_add_css($file) { + // We set preprocess to FALSE because we are adding the files conditionally, + // and we don't want to generate duplicate cache files. + // TODO: at some point investigate adding some files unconditionally and + // allowing preprocess. + drupal_add_css(drupal_get_path('module', 'views') . "/css/$file.css", array('preprocess' => FALSE)); +} + +/** + * Include views .js files. + */ +function views_add_js($file) { + // If javascript has been disabled by the user, never add js files. + if (config('views.settings')->get('no_javascript')) { + return; + } + $path = drupal_get_path('module', 'views'); + static $base = TRUE, $ajax = TRUE; + if ($base) { + drupal_add_js($path . "/js/base.js"); + $base = FALSE; + } + if ($ajax && in_array($file, array('ajax', 'ajax_view'))) { + drupal_add_library('system', 'drupal.ajax'); + drupal_add_library('system', 'jquery.form'); + $ajax = FALSE; + } + drupal_add_js($path . "/js/$file.js"); +} + +// ----------------------------------------------------------------------- +// Views handler functions + +/** + * Fetch a handler from the data cache. + * + * @param $table + * The name of the table this handler is from. + * @param $field + * The name of the field this handler is from. + * @param $type + * The type of handler. i.e, sort, field, argument, filter, relationship + * @param $override + * Override the actual handler object with this class. Used for + * aggregation when the handler is redirected to the aggregation + * handler. + * + * @return views_handler + * An instance of a handler object. May be views_handler_broken. + */ +function views_get_handler($table, $field, $type, $override = NULL) { + static $recursion_protection = array(); + + $data = views_fetch_data($table, FALSE); + $handler = NULL; + + // Support conversion on table level. + if (isset($data['moved to'])) { + $moved = array($data['moved to'], $field); + } + // Support conversion on datafield level. + if (isset($data[$field]['moved to'])) { + $moved = $data[$field]['moved to']; + } + // Support conversion on handler level. + if (isset($data[$field][$type]['moved to'])) { + $moved = $data[$field][$type]['moved to']; + } + + if (isset($data[$field][$type]) || !empty($moved)) { + if (!empty($moved)) { + list($moved_table, $moved_field) = $moved; + if (!empty($recursion_protection[$moved_table][$moved_field])) { + // recursion detected! + return NULL; + } + + $recursion_protection[$moved_table][$moved_field] = TRUE; + $handler = views_get_handler($moved_table, $moved_field, $type, $override); + $recursion_protection = array(); + if ($handler) { + // store these values so we know what we were originally called. + $handler->original_table = $table; + $handler->original_field = $field; + if (empty($handler->actualTable)) { + $handler->actualTable = $moved_table; + $handler->actualField = $moved_field; + } + } + return $handler; + } + + // @fixme: temporary. + // Set up a default handler, if both handler and id is not specified. + if (empty($data[$field][$type]['handler']) && empty($data[$field][$type]['id'])) { + $data[$field][$type]['id'] = 'standard'; + } + + if ($override) { + $data[$field][$type]['override handler'] = $override; + } + + $definition = $data[$field][$type]; + foreach (array('group', 'title', 'title short', 'help', 'real field', 'real table') as $key) { + if (!isset($definition[$key])) { + // First check the field level + if (!empty($data[$field][$key])) { + $definition[$key] = $data[$field][$key]; + } + // Then if that doesn't work, check the table level + elseif (!empty($data['table'][$key])) { + $definition[$key] = $data['table'][$key]; + } + } + } + + // @todo This is crazy. Find a way to remove the override functionality. + $manager = drupal_container()->get("plugin.manager.views.$type"); + $plugin_id = !empty($definition['override handler']) ? $definition['override handler'] : $definition['id']; + // Try to use the overridden handler. + try { + return $manager->createInstance($plugin_id, $definition); + } + catch (PluginException $e) { + // If that fails, use the original handler. + try { + return $manager->createInstance($definition['id'], $definition); + } + catch (PluginException $e) { + // Deliberately empty, this case is handled generically below. + } + } + } + + // Finally, use the 'broken' handler. + debug(t("Missing handler: @table @field @type", array('@table' => $table, '@field' => $field, '@type' => $type))); + return views_get_plugin($type, 'broken'); +} + +/** + * Fetch Views' data from the cache + */ +function views_fetch_data($table = NULL, $move = TRUE, $reset = FALSE) { + views_include('cache'); + return _views_fetch_data($table, $move, $reset); +} + +// ----------------------------------------------------------------------- +// Views plugin functions + +/** + * Fetch a list of all base tables available + * + * @param $type + * Either 'display', 'style' or 'row' + * @param $key + * For style plugins, this is an optional type to restrict to. May be 'normal', + * 'summary', 'feed' or others based on the neds of the display. + * @param $base + * An array of possible base tables. + * + * @return + * A keyed array of in the form of 'base_table' => 'Description'. + */ +function views_fetch_plugin_names($type, $key = NULL, $base = array()) { + $definitions = views_get_plugin_definitions($type); + $plugins = array(); + + foreach ($definitions as $id => $plugin) { + // Skip plugins that don't conform to our key. + if ($key && (empty($plugin['type']) || $plugin['type'] != $key)) { + continue; + } + // @todo While Views is providing on behalf of core modules, check to see + // if they are enabled or not. + if (isset($plugin['module']) && !module_exists($plugin['module'])) { + continue; + } + + if (empty($plugin['no_ui']) && (empty($base) || empty($plugin['base']) || array_intersect($base, $plugin['base']))) { + $plugins[$id] = $plugin['title']; + } + } + + if (!empty($plugins)) { + asort($plugins); + return $plugins; + } + + // fall-through + return array(); +} + +/** + * Get an instance of a plugin. + * + * @param string $type + * The plugin type, e.g., 'access' or 'display'. + * @param string $plugin_id + * The name of the plugin, e.g., 'standard'. + * + * @return Drupal\views\Plugin\view\PluginBase + * The created plugin instance. + */ +function views_get_plugin($type, $plugin_id) { + return drupal_container()->get("plugin.manager.views.$type")->createInstance($plugin_id); +} + +/** + * Gets all the views plugin definitions. + * + * @param string|false $type + * Either the plugin type, or FALSE to load all definitions. + * + * @return array + * An array of plugin definitions for a single type, or an associative array + * of plugin definitions keyed by plugin type. + */ +function views_get_plugin_definitions($type = FALSE) { + $plugins = array(); + $plugin_types = $type ? array($type) : ViewExecutable::getPluginTypes(); + $container = drupal_container(); + foreach ($plugin_types as $plugin_type) { + $plugins[$plugin_type] = $container->get("plugin.manager.views.$plugin_type")->getDefinitions(); + } + if ($type) { + return $plugins[$type]; + } + return $plugins; +} + +/** + * Gets the plugin definition from a plugin type with a specific ID. + * + * @param string $type + * The plugin type, e.g., 'access' or 'display'. + * @param string $plugin_id + * The name of the plugin, e.g., 'standard'. + * + * @return array + * A plugin definition. + */ +function views_get_plugin_definition($type, $plugin_id) { + return drupal_container()->get("plugin.manager.views.$type")->getDefinition($plugin_id); +} + +/** + * Get enabled display extenders. + */ +function views_get_enabled_display_extenders() { + $enabled = array_filter((array) config('views.settings')->get('display_extenders')); + + return drupal_map_assoc($enabled); +} + +// ----------------------------------------------------------------------- +// Views database functions + +/** + * Create an empty view to work with. + * + * @return Drupal\views\ViewStorage + * A fully formed, empty $view object. This object must be populated before + * it can be successfully saved. + */ +function views_new_view() { + return entity_create('view', array()); +} + +/** + * Creates a view from an array of values. + * + * @return Drupal\views\ViewStorage + * A fully formed $view object with properties from the values passed in. + */ +function views_create_view(array $values = array()) { + return entity_create('view', $values); +} + +/** + * Return a list of all views and display IDs that have a particular + * setting in their display's plugin settings. + * + * @return + * @code + * array( + * array($view, $display_id), + * array($view, $display_id), + * ); + * @endcode + */ +function views_get_applicable_views($type) { + // @todo: Use a smarter flagging system so that we don't have to + // load every view for this. + $result = array(); + $views = views_get_all_views(); + + foreach ($views as $view) { + // Skip disabled views. + if (!$view->isEnabled()) { + continue; + } + + if (empty($view->display)) { + // Skip this view as it is broken. + continue; + } + + // Loop on array keys because something seems to muck with $view->display + // a bit in PHP4. + foreach (array_keys($view->display) as $id) { + $plugin = views_get_plugin_definition('display', $view->display[$id]['display_plugin']); + if (!empty($plugin[$type])) { + $executable = $view->getExecutable(); + // This view uses_hook_menu. Clone it so that different handlers + // don't trip over each other, and add it to the list. + $v = $executable->cloneView(); + if ($v->setDisplay($id) && $v->display_handler->isEnabled()) { + $result[] = array($v, $id); + } + // In PHP 4.4.7 and presumably earlier, if we do not unset $v + // here, we will find that it actually overwrites references + // possibly due to shallow copying issues. + unset($v); + } + } + } + return $result; +} + +/** + * Returns an array of all views as fully loaded $view objects. + */ +function views_get_all_views() { + return entity_get_controller('view')->load(); +} + +/** + * Returns an array of all enabled views, as fully loaded $view objects. + */ +function views_get_enabled_views() { + $views = views_get_all_views(); + return array_filter($views, 'views_view_is_enabled'); +} + +/** + * Returns an array of all disabled views, as fully loaded $view objects. + */ +function views_get_disabled_views() { + $views = views_get_all_views(); + return array_filter($views, 'views_view_is_disabled'); +} + +/** + * Return an array of view as options array, that can be used by select, + * checkboxes and radios as #options. + * + * @param bool $views_only + * If TRUE, only return views, not displays. + * @param string $filter + * Filters the views on status. Can either be 'all' (default), 'enabled' or + * 'disabled' + * @param mixed $exclude_view + * view or current display to exclude + * either a + * - views object (containing $exclude_view->storage->name and $exclude_view->current_display) + * - views name as string: e.g. my_view + * - views name and display id (separated by ':'): e.g. my_view:default + * @param bool $optgroup + * If TRUE, returns an array with optgroups for each view (will be ignored for + * $views_only = TRUE). Can be used by select + * @param bool $sort + * If TRUE, the list of views is sorted ascending. + * + * @return array + * an associative array for use in select. + * - key: view name and display id separated by ':', or the view name only + */ +function views_get_views_as_options($views_only = FALSE, $filter = 'all', $exclude_view = NULL, $optgroup = FALSE, $sort = FALSE) { + + // Filter the big views array. + switch ($filter) { + case 'all': + case 'disabled': + case 'enabled': + $func = "views_get_{$filter}_views"; + $views = $func(); + break; + default: + return array(); + } + + // Prepare exclude view strings for comparison. + if (empty($exclude_view)) { + $exclude_view_name = ''; + $exclude_view_display = ''; + } + elseif (is_object($exclude_view)) { + $exclude_view_name = $exclude_view->storage->id(); + $exclude_view_display = $exclude_view->current_display; + } + else { + // Append a ':' to the $exclude_view string so we always have more than one + // item to explode. + list($exclude_view_name, $exclude_view_display) = explode(':', "$exclude_view:"); + } + + $options = array(); + foreach ($views as $view) { + $id = $view->id(); + // Return only views. + if ($views_only && $id != $exclude_view_name) { + $options[$id] = $view->getHumanName(); + } + // Return views with display ids. + else { + foreach ($view->display as $display_id => $display) { + if (!($id == $exclude_view_name && $display_id == $exclude_view_display)) { + if ($optgroup) { + $options[$id][$id . ':' . $display['id']] = t('@view : @display', array('@view' => $id, '@display' => $display['id'])); + } + else { + $options[$id . ':' . $display['id']] = t('View: @view - Display: @display', array('@view' => $id, '@display' => $display['id'])); + } + } + } + } + } + if ($sort) { + ksort($options); + } + return $options; +} + +/** + * Returns whether the view is enabled. + * + * @param Drupal\views\ViewExecutable $view + * The view object to check. + * + * @return bool + * Returns TRUE if a view is enabled, FALSE otherwise. + */ +function views_view_is_enabled($view) { + return $view->isEnabled(); +} + +/** + * Returns whether the view is disabled. + * + * @param Drupal\views\ViewExecutable $view + * The view object to check. + * + * @return bool + * Returns TRUE if a view is disabled, FALSE otherwise. + */ +function views_view_is_disabled($view) { + return !$view->isEnabled(); +} + +/** + * Loads a view from configuration. + * + * @param string $name + * The name of the view. + * + * @return Drupal\views\ViewExecutable + * A reference to the $view object. + */ +function views_get_view($name) { + $view = entity_load('view', $name); + if ($view) { + $view->update(); + $view = $view->getExecutable(); + // @figure out whether it makes sense to clone this here. + return $view->cloneView(); + } +} + +/** + * Find the real location of a table. + * + * If a table has moved, find the new name of the table so that we can + * change its name directly in options where necessary. + */ +function views_move_table($table) { + $data = views_fetch_data($table, FALSE); + if (isset($data['moved to'])) { + $table = $data['moved to']; + } + + return $table; +} + +// ------------------------------------------------------------------ +// Views form (View with form elements) + +/** + * Returns TRUE if the passed-in view contains handlers with views form + * implementations, FALSE otherwise. + */ +function views_view_has_form_elements($view) { + foreach ($view->field as $field) { + if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) { + return TRUE; + } + } + $area_handlers = array_merge(array_values($view->header), array_values($view->footer)); + $empty = empty($view->result); + foreach ($area_handlers as $area) { + if (method_exists($area, 'views_form') && !$area->views_form_empty($empty)) { + return TRUE; + } + } + return FALSE; +} + +/** + * This is the entry function. Just gets the form for the current step. + * The form is always assumed to be multistep, even if it has only one + * step (the default 'views_form_views_form' step). That way it is actually + * possible for modules to have a multistep form if they need to. + */ +function views_form($form, &$form_state, ViewExecutable $view, $output) { + $form_state['step'] = isset($form_state['step']) ? $form_state['step'] : 'views_form_views_form'; + // Cache the built form to prevent it from being rebuilt prior to validation + // and submission, which could lead to data being processed incorrectly, + // because the views rows (and thus, the form elements as well) have changed + // in the meantime. + $form_state['cache'] = TRUE; + + $form = array(); + $query = drupal_get_query_parameters(); + $form['#action'] = url($view->getUrl(), array('query' => $query)); + // Tell the preprocessor whether it should hide the header, footer, pager... + $form['show_view_elements'] = array( + '#type' => 'value', + '#value' => ($form_state['step'] == 'views_form_views_form') ? TRUE : FALSE, + ); + + $form = $form_state['step']($form, $form_state, $view, $output); + return $form; +} + +/** + * Callback for the main step of a Views form. + * Invoked by views_form(). + */ +function views_form_views_form($form, &$form_state, ViewExecutable $view, $output) { + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + $form['#theme'] = 'views_form_views_form'; + $form['#validate'][] = 'views_form_views_form_validate'; + $form['#submit'][] = 'views_form_views_form_submit'; + + // Add the output markup to the form array so that it's included when the form + // array is passed to the theme function. + $form['output'] = array( + '#type' => 'markup', + '#markup' => $output, + // This way any additional form elements will go before the view + // (below the exposed widgets). + '#weight' => 50, + ); + + $substitutions = array(); + foreach ($view->field as $field_name => $field) { + $form_element_name = $field_name; + if (method_exists($field, 'form_element_name')) { + $form_element_name = $field->form_element_name(); + } + $method_form_element_row_id_exists = FALSE; + if (method_exists($field, 'form_element_row_id')) { + $method_form_element_row_id_exists = TRUE; + } + + // If the field provides a views form, allow it to modify the $form array. + $has_form = FALSE; + if (property_exists($field, 'views_form_callback')) { + $callback = $field->views_form_callback; + $callback($view, $field, $form, $form_state); + $has_form = TRUE; + } + elseif (method_exists($field, 'views_form')) { + $field->views_form($form, $form_state); + $has_form = TRUE; + } + + // Build the substitutions array for use in the theme function. + if ($has_form) { + foreach ($view->result as $row_id => $row) { + if ($method_form_element_row_id_exists) { + $form_element_row_id = $field->form_element_row_id($row_id); + } + else { + $form_element_row_id = $row_id; + } + + $substitutions[] = array( + 'placeholder' => '', + 'field_name' => $form_element_name, + 'row_id' => $form_element_row_id, + ); + } + } + } + + // Give the area handlers a chance to extend the form. + $area_handlers = array_merge(array_values($view->header), array_values($view->footer)); + $empty = empty($view->result); + foreach ($area_handlers as $area) { + if (method_exists($area, 'views_form') && !$area->views_form_empty($empty)) { + $area->views_form($form, $form_state); + } + } + + $form['#substitutions'] = array( + '#type' => 'value', + '#value' => $substitutions, + ); + $form['actions'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('form-actions')), + '#weight' => 100, + ); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + return $form; +} + +/** + * Validate handler for the first step of the views form. + * Calls any existing views_form_validate functions located + * on the views fields. + */ +function views_form_views_form_validate($form, &$form_state) { + $view = $form_state['build_info']['args'][0]; + + // Call the validation method on every field handler that has it. + foreach ($view->field as $field_name => $field) { + if (method_exists($field, 'views_form_validate')) { + $field->views_form_validate($form, $form_state); + } + } + + // Call the validate method on every area handler that has it. + foreach (array('header', 'footer') as $area) { + foreach ($view->{$area} as $area_name => $area_handler) { + if (method_exists($area_handler, 'views_form_validate')) { + $area_handler->views_form_validate($form, $form_state); + } + } + } +} + +/** + * Submit handler for the first step of the views form. + * Calls any existing views_form_submit functions located + * on the views fields. + */ +function views_form_views_form_submit($form, &$form_state) { + $view = $form_state['build_info']['args'][0]; + + // Call the submit method on every field handler that has it. + foreach ($view->field as $field_name => $field) { + if (method_exists($field, 'views_form_submit')) { + $field->views_form_submit($form, $form_state); + } + } + + // Call the submit method on every area handler that has it. + foreach (array('header', 'footer') as $area) { + foreach ($view->{$area} as $area_name => $area_handler) { + if (method_exists($area_handler, 'views_form_submit')) { + $area_handler->views_form_submit($form, $form_state); + } + } + } +} + +// ------------------------------------------------------------------ +// Exposed widgets form + +/** + * Form builder for the exposed widgets form. + * + * Be sure that $view and $display are references. + */ +function views_exposed_form($form, &$form_state) { + // Don't show the form when batch operations are in progress. + if ($batch = batch_get() && isset($batch['current_set'])) { + return array( + // Set the theme callback to be nothing to avoid errors in template_preprocess_views_exposed_form(). + '#theme' => '', + ); + } + + // Make sure that we validate because this form might be submitted + // multiple times per page. + $form_state['must_validate'] = TRUE; + $view = &$form_state['view']; + $display = &$form_state['display']; + + $form_state['input'] = $view->getExposedInput(); + + // Let form plugins know this is for exposed widgets. + $form_state['exposed'] = TRUE; + // Check if the form was already created + if ($cache = views_exposed_form_cache($view->storage->name, $view->current_display)) { + return $cache; + } + + $form['#info'] = array(); + + // Go through each handler and let it generate its exposed widget. + foreach ($view->display_handler->handlers as $type => $value) { + foreach ($view->$type as $id => $handler) { + if ($handler->canExpose() && $handler->isExposed()) { + // Grouped exposed filters have their own forms. + // Instead of render the standard exposed form, a new Select or + // Radio form field is rendered with the available groups. + // When an user choose an option the selected value is split + // into the operator and value that the item represents. + if ($handler->isAGroup()) { + $handler->group_form($form, $form_state); + $id = $handler->options['group_info']['identifier']; + } + else { + $handler->buildExposedForm($form, $form_state); + } + if ($info = $handler->exposedInfo()) { + $form['#info']["$type-$id"] = $info; + } + } + } + } + + $form['submit'] = array( + // Prevent from showing up in $_GET. + '#name' => '', + '#type' => 'submit', + '#value' => t('Apply'), + '#id' => drupal_html_id('edit-submit-' . $view->storage->name), + ); + + $form['#action'] = url($view->display_handler->getUrl()); + $form['#theme'] = views_theme_functions('views_exposed_form', $view, $display); + $form['#id'] = drupal_clean_css_identifier('views_exposed_form-' . check_plain($view->storage->name) . '-' . check_plain($display['id'])); +// $form['#attributes']['class'] = array('views-exposed-form'); + + // If using AJAX, we need the form plugin. + if ($view->use_ajax) { + drupal_add_library('system', 'jquery.form'); + } + + $exposed_form_plugin = $form_state['exposed_form_plugin']; + $exposed_form_plugin->exposed_form_alter($form, $form_state); + + // Save the form + views_exposed_form_cache($view->storage->name, $view->current_display, $form); + + return $form; +} + +/** + * Implement hook_form_alter for the exposed form. + * + * Since the exposed form is a GET form, we don't want it to send a wide + * variety of information. + */ +function views_form_views_exposed_form_alter(&$form, &$form_state) { + $form['form_build_id']['#access'] = FALSE; + $form['form_token']['#access'] = FALSE; + $form['form_id']['#access'] = FALSE; +} + +/** + * Validate handler for exposed filters + */ +function views_exposed_form_validate(&$form, &$form_state) { + foreach (array('field', 'filter') as $type) { + $handlers = &$form_state['view']->$type; + foreach ($handlers as $key => $handler) { + $handlers[$key]->validateExposed($form, $form_state); + } + } + $exposed_form_plugin = $form_state['exposed_form_plugin']; + $exposed_form_plugin->exposed_form_validate($form, $form_state); +} + +/** + * Submit handler for exposed filters + */ +function views_exposed_form_submit(&$form, &$form_state) { + foreach (array('field', 'filter') as $type) { + $handlers = &$form_state['view']->$type; + foreach ($handlers as $key => $info) { + $handlers[$key]->submitExposed($form, $form_state); + } + } + $form_state['view']->exposed_data = $form_state['values']; + $form_state['view']->exposed_raw_input = array(); + + $exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', '', 'reset'); + $exposed_form_plugin = $form_state['exposed_form_plugin']; + $exposed_form_plugin->exposed_form_submit($form, $form_state, $exclude); + + foreach ($form_state['values'] as $key => $value) { + if (!in_array($key, $exclude)) { + $form_state['view']->exposed_raw_input[$key] = $value; + } + } +} + +/** + * Save the Views exposed form for later use. + * + * @param $views_name + * String. The views name. + * @param $display_name + * String. The current view display name. + * @param $form_output + * Array (optional). The form structure. Only needed when inserting the value. + * @return + * Array. The form structure, if any. Otherwise, return FALSE. + */ +function views_exposed_form_cache($views_name, $display_name, $form_output = NULL) { + // When running tests for exposed filters, this cache should + // be cleared between each test. + $views_exposed = &drupal_static(__FUNCTION__); + + // Save the form output + if (!empty($form_output)) { + $views_exposed[$views_name][$display_name] = $form_output; + return; + } + + // Return the form output, if any + return empty($views_exposed[$views_name][$display_name]) ? FALSE : $views_exposed[$views_name][$display_name]; +} + +// ------------------------------------------------------------------ +// Misc helpers + +/** + * Build a list of theme function names for use most everywhere. + */ +function views_theme_functions($hook, ViewExecutable $view, $display = NULL) { + module_load_include('inc', 'views', 'theme/theme'); + return _views_theme_functions($hook, $view, $display); +} + +/** + * Substitute current time; this works with cached queries. + */ +function views_views_query_substitutions($view) { + return array( + '***CURRENT_VERSION***' => VERSION, + '***CURRENT_TIME***' => REQUEST_TIME, + '***CURRENT_LANGUAGE***' => language(LANGUAGE_TYPE_CONTENT)->langcode, + '***DEFAULT_LANGUAGE***' => language_default()->langcode, + ); +} + +/** + * Implements hook_query_TAG_alter(). + * + * This is the hook_query_alter() for queries tagged by Views and is used to + * add in substitutions from hook_views_query_substitutions(). + */ +function views_query_views_alter(AlterableInterface $query) { + $substitutions = $query->getMetaData('views_substitutions'); + $tables =& $query->getTables(); + $where =& $query->conditions(); + + // Replaces substitions in tables. + foreach ($tables as $table_name => $table_metadata) { + foreach ($table_metadata['arguments'] as $replacement_key => $value) { + if (isset($substitutions[$value])) { + $tables[$table_name]['arguments'][$replacement_key] = $substitutions[$value]; + } + } + } + + // Replaces substitions in filter criterias. + _views_query_tag_alter_condition($query, $where, $substitutions); +} + +/** + * Replaces the substitutions recursive foreach condition. + */ +function _views_query_tag_alter_condition(AlterableInterface $query, &$conditions, $substitutions) { + foreach ($conditions as $condition_id => &$condition) { + if (is_numeric($condition_id)) { + if (is_string($condition['field'])) { + $condition['field'] = str_replace(array_keys($substitutions), array_values($substitutions), $condition['field']); + } + elseif (is_object($condition['field'])) { + $sub_conditions =& $condition['field']->conditions(); + _views_query_tag_alter_condition($query, $sub_conditions, $substitutions); + } + // $condition['value'] is a subquery so alter the subquery recursive. + // Therefore take sure to get the metadata of the main query. + if (is_object($condition['value'])) { + $subquery = $condition['value']; + $subquery->addMetaData('views_substitutions', $query->getMetaData('views_substitutions')); + views_query_views_alter($condition['value']); + } + elseif (isset($condition['value'])) { + $condition['value'] = str_replace(array_keys($substitutions), array_values($substitutions), $condition['value']); + } + } + } +} + +/** + * Embed a view using a PHP snippet. + * + * This function is meant to be called from PHP snippets, should one wish to + * embed a view in a node or something. It's meant to provide the simplest + * solution and doesn't really offer a lot of options, but breaking the function + * apart is pretty easy, and this provides a worthwhile guide to doing so. + * + * Note that this function does NOT display the title of the view. If you want + * to do that, you will need to do what this function does manually, by + * loading the view, getting the preview and then getting $view->getTitle(). + * + * @param $name + * The name of the view to embed. + * @param $display_id + * The display id to embed. If unsure, use 'default', as it will always be + * valid. But things like 'page' or 'block' should work here. + * @param ... + * Any additional parameters will be passed as arguments. + */ +function views_embed_view($name, $display_id = 'default') { + $args = func_get_args(); + array_shift($args); // remove $name + if (count($args)) { + array_shift($args); // remove $display_id + } + + $view = views_get_view($name); + if (!$view || !$view->access($display_id)) { + return; + } + + return $view->preview($display_id, $args); +} + +/** + * Get the result of a view. + * + * @param string $name + * The name of the view to retrieve the data from. + * @param string $display_id + * The display id. On the edit page for the view in question, you'll find + * a list of displays at the left side of the control area. "Master" + * will be at the top of that list. Hover your cursor over the name of the + * display you want to use. An URL will appear in the status bar of your + * browser. This is usually at the bottom of the window, in the chrome. + * Everything after #views-tab- is the display ID, e.g. page_1. + * @param ... + * Any additional parameters will be passed as arguments. + * @return array + * An array containing an object for each view item. + */ +function views_get_view_result($name, $display_id = NULL) { + $args = func_get_args(); + array_shift($args); // remove $name + if (count($args)) { + array_shift($args); // remove $display_id + } + + $view = views_get_view($name); + if (is_object($view)) { + if (is_array($args)) { + $view->setArguments($args); + } + if (is_string($display_id)) { + $view->setDisplay($display_id); + } + else { + $view->initDisplay(); + } + $view->preExecute(); + $view->execute(); + return $view->result; + } + else { + return array(); + } +} + +/** + * #process callback to see if we need to check_plain() the options. + * + * Since FAPI is inconsistent, the #options are sanitized for you in all cases + * _except_ checkboxes. We have form elements that are sometimes 'select' and + * sometimes 'checkboxes', so we need decide late in the form rendering cycle + * if the options need to be sanitized before they're rendered. This callback + * inspects the type, and if it's still 'checkboxes', does the sanitation. + */ +function views_process_check_options($element, &$form_state) { + if ($element['#type'] == 'checkboxes' || $element['#type'] == 'checkbox') { + $element['#options'] = array_map('check_plain', $element['#options']); + } + return $element; +} + +/** + * Validation callback for query tags. + */ +function views_element_validate_tags($element, &$form_state) { + $values = array_map('trim', explode(',', $element['#value'])); + foreach ($values as $value) { + if (preg_match("/[^a-z_]/", $value)) { + form_error($element, t('The query tags may only contain lower-case alphabetical characters and underscores.')); + return; + } + } +} + +/** + * Prerender function to move the textarea to the top. + */ +function views_handler_field_custom_pre_render_move_text($form) { + $form['text'] = $form['alter']['text']; + $form['help'] = $form['alter']['help']; + unset($form['alter']['text']); + unset($form['alter']['help']); + + return $form; +} + +/** + * Helper function: Return an array of formatter options for a field type. + * + * Borrowed from field_ui. + */ +function _field_view_formatter_options($field_type = NULL) { + $options = &drupal_static(__FUNCTION__); + + if (!isset($options)) { + $field_types = field_info_field_types(); + $options = array(); + foreach (field_info_formatter_types() as $name => $formatter) { + foreach ($formatter['field types'] as $formatter_field_type) { + // Check that the field type exists. + if (isset($field_types[$formatter_field_type])) { + $options[$formatter_field_type][$name] = $formatter['label']; + } + } + } + } + + if ($field_type) { + return !empty($options[$field_type]) ? $options[$field_type] : array(); + } + return $options; +} + +/** + * Trim the field down to the specified length. + * + * @param $alter + * - max_length: Maximum lenght of the string, the rest gets truncated. + * - word_boundary: Trim only on a word boundary. + * - ellipsis: Show an ellipsis (...) at the end of the trimmed string. + * - html: Take sure that the html is correct. + * + * @param $value + * The string which should be trimmed. + */ +function views_trim_text($alter, $value) { + if (drupal_strlen($value) > $alter['max_length']) { + $value = drupal_substr($value, 0, $alter['max_length']); + if (!empty($alter['word_boundary'])) { + $regex = "(.*)\b.+"; + if (function_exists('mb_ereg')) { + mb_regex_encoding('UTF-8'); + $found = mb_ereg($regex, $value, $matches); + } + else { + $found = preg_match("/$regex/us", $value, $matches); + } + if ($found) { + $value = $matches[1]; + } + } + // Remove scraps of HTML entities from the end of a strings + $value = rtrim(preg_replace('/(?:<(?!.+>)|&(?!.+;)).*$/us', '', $value)); + + if (!empty($alter['ellipsis'])) { + // @todo: What about changing this to a real ellipsis? + $value .= t('...'); + } + } + if (!empty($alter['html'])) { + $value = _filter_htmlcorrector($value); + } + + return $value; +} + +/** + * Filter by no empty values, though allow to use "0". + * @param $var + * @return bool + */ +function _views_array_filter_zero($var) { + return trim($var) != ""; +} + +/** + * Adds one to each key of the array. + * + * For example array(0 => 'foo') would be array(1 => 'foo'). + */ +function views_array_key_plus($array) { + $keys = array_keys($array); + rsort($keys); + foreach ($keys as $key) { + $array[$key+1] = $array[$key]; + unset($array[$key]); + } + asort($array); + return $array; +} diff --git a/core/modules/views/views.tokens.inc b/core/modules/views/views.tokens.inc new file mode 100644 index 0000000..7d0bf93 --- /dev/null +++ b/core/modules/views/views.tokens.inc @@ -0,0 +1,94 @@ + t('View'), + 'description' => t('Tokens related to views.'), + 'needs-data' => 'view', + ); + $info['tokens']['view']['name'] = array( + 'name' => t('Name'), + 'description' => t('The human-readable name of the view.'), + ); + $info['tokens']['view']['description'] = array( + 'name' => t('Description'), + 'description' => t('The description of the view.'), + ); + $info['tokens']['view']['machine-name'] = array( + 'name' => t('Machine name'), + 'description' => t('The machine-readable name of the view.'), + ); + $info['tokens']['view']['title'] = array( + 'name' => t('Title'), + 'description' => t('The title of current display of the view.'), + ); + $info['tokens']['view']['url'] = array( + 'name' => t('URL'), + 'description' => t('The URL of the view.'), + 'type' => 'url', + ); + + return $info; +} + +/** + * Implements hook_tokens(). + */ +function views_tokens($type, $tokens, array $data = array(), array $options = array()) { + $url_options = array('absolute' => TRUE); + if (isset($options['language'])) { + $url_options['language'] = $options['language']; + } + $sanitize = !empty($options['sanitize']); + $langcode = isset($options['language']) ? $options['language']->langcode : NULL; + + $replacements = array(); + + if ($type == 'view' && !empty($data['view'])) { + $view = $data['view']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'name': + $replacements[$original] = $sanitize ? check_plain($view->storage->getHumanName()) : $view->storage->getHumanName(); + break; + + case 'description': + $replacements[$original] = $sanitize ? check_plain($view->storage->description) : $view->storage->description; + break; + + case 'machine-name': + $replacements[$original] = $view->storage->name; + break; + + case 'title': + $title = $view->getTitle(); + $replacements[$original] = $sanitize ? check_plain($title) : $title; + break; + + case 'url': + if ($path = $view->getUrl()) { + $replacements[$original] = url($path, $url_options); + } + break; + } + } + + // [view:url:*] nested tokens. This only works if Token module is installed. + if ($url_tokens = token_find_with_prefix($tokens, 'url')) { + if ($path = $view->getUrl()) { + $replacements += token_generate('url', $url_tokens, array('path' => $path), $options); + } + } + } + + return $replacements; +} diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc new file mode 100644 index 0000000..a12edde --- /dev/null +++ b/core/modules/views/views.views.inc @@ -0,0 +1,103 @@ + array(), + ); + + $data['views']['random'] = array( + 'title' => t('Random'), + 'help' => t('Randomize the display order.'), + 'sort' => array( + 'id' => 'random', + ), + ); + + $data['views']['null'] = array( + 'title' => t('Null'), + 'help' => t('Allow a contextual filter value to be ignored. The query will not be altered by this contextual filter value. Can be used when contextual filter values come from the URL, and a part of the URL needs to be ignored.'), + 'argument' => array( + 'id' => 'null', + ), + ); + + $data['views']['nothing'] = array( + 'title' => t('Custom text'), + 'help' => t('Provide custom text or link.'), + 'field' => array( + 'id' => 'custom', + ), + ); + + $data['views']['counter'] = array( + 'title' => t('View result counter'), + 'help' => t('Displays the actual position of the view result'), + 'field' => array( + 'id' => 'counter', + ), + ); + + $data['views']['area'] = array( + 'title' => t('Text area'), + 'help' => t('Provide markup text for the area.'), + 'area' => array( + 'id' => 'text', + ), + ); + + $data['views']['area_text_custom'] = array( + 'title' => t('Unfiltered text'), + 'help' => t('Add unrestricted, custom text or markup. This is similar to the custom text field.'), + 'area' => array( + 'id' => 'text_custom', + ), + ); + + $data['views']['view'] = array( + 'title' => t('View area'), + 'help' => t('Insert a view inside an area.'), + 'area' => array( + 'id' => 'view', + ), + ); + + $data['views']['result'] = array( + 'title' => t('Result summary'), + 'help' => t('Shows result summary, for example the items per page.'), + 'area' => array( + 'id' => 'result', + ), + ); + + if (module_exists('contextual')) { + $data['views']['contextual_links'] = array( + 'title' => t('Contextual Links'), + 'help' => t('Display fields in a contextual links menu.'), + 'field' => array( + 'id' => 'contextual_links', + ), + ); + } + + $data['views']['combine'] = array( + 'title' => t('Combine fields filter'), + 'help' => t('Combine two fields together and search by them.'), + 'filter' => array( + 'id' => 'combine', + ), + ); + + return $data; +} diff --git a/core/modules/views/views_ui/admin.inc b/core/modules/views/views_ui/admin.inc new file mode 100644 index 0000000..378e04a --- /dev/null +++ b/core/modules/views/views_ui/admin.inc @@ -0,0 +1,2764 @@ +renderPreview($display_id, $args); +} + +/** + * Page callback to add a new view. + */ +function views_ui_add_page() { + drupal_set_title(t('Add new view')); + $form_state['build_info']['args'] = array(); + $form_state['build_info']['callback'] = array('Drupal\views_ui\ViewUI', 'buildAddForm'); + return drupal_build_form('views_ui_add_form', $form_state); +} + +/** + * Gets the current value of a #select element, from within a form constructor function. + * + * This function is intended for use in highly dynamic forms (in particular the + * add view wizard) which are rebuilt in different ways depending on which + * triggering element (AJAX or otherwise) was most recently fired. For example, + * sometimes it is necessary to decide how to build one dynamic form element + * based on the value of a different dynamic form element that may not have + * even been present on the form the last time it was submitted. This function + * takes care of resolving those conflicts and gives you the proper current + * value of the requested #select element. + * + * By necessity, this function sometimes uses non-validated user input from + * $form_state['input'] in making its determination. Although it performs some + * minor validation of its own, it is not complete. The intention is that the + * return value of this function should only be used to help decide how to + * build the current form the next time it is reloaded, not to be saved as if + * it had gone through the normal, final form validation process. Do NOT use + * the results of this function for any other purpose besides deciding how to + * build the next version of the form. + * + * @param $form_state + * The standard associative array containing the current state of the form. + * @param $parents + * An array of parent keys that point to the part of the submitted form + * values that are expected to contain the element's value (in the case where + * this form element was actually submitted). In a simple case (assuming + * #tree is TRUE throughout the form), if the select element is located in + * $form['wrapper']['select'], so that the submitted form values would + * normally be found in $form_state['values']['wrapper']['select'], you would + * pass array('wrapper', 'select') for this parameter. + * @param $default_value + * The default value to return if the #select element does not currently have + * a proper value set based on the submitted input. + * @param $element + * An array representing the current version of the #select element within + * the form. + * + * @return + * The current value of the #select element. A common use for this is to feed + * it back into $element['#default_value'] so that the form will be rendered + * with the correct value selected. + */ +function views_ui_get_selected($form_state, $parents, $default_value, $element) { + // For now, don't trust this to work on anything but a #select element. + if (!isset($element['#type']) || $element['#type'] != 'select' || !isset($element['#options'])) { + return $default_value; + } + + // If there is a user-submitted value for this element that matches one of + // the currently available options attached to it, use that. We need to check + // $form_state['input'] rather than $form_state['values'] here because the + // triggering element often has the #limit_validation_errors property set to + // prevent unwanted errors elsewhere on the form. This means that the + // $form_state['values'] array won't be complete. We could make it complete + // by adding each required part of the form to the #limit_validation_errors + // property individually as the form is being built, but this is difficult to + // do for a highly dynamic and extensible form. This method is much simpler. + if (!empty($form_state['input'])) { + $key_exists = NULL; + $submitted = drupal_array_get_nested_value($form_state['input'], $parents, $key_exists); + // Check that the user-submitted value is one of the allowed options before + // returning it. This is not a substitute for actual form validation; + // rather it is necessary because, for example, the same select element + // might have #options A, B, and C under one set of conditions but #options + // D, E, F under a different set of conditions. So the form submission + // might have occurred with option A selected, but when the form is rebuilt + // option A is no longer one of the choices. In that case, we don't want to + // use the value that was submitted anymore but rather fall back to the + // default value. + if ($key_exists && in_array($submitted, array_keys($element['#options']))) { + return $submitted; + } + } + + // Fall back on returning the default value if nothing was returned above. + return $default_value; +} + +/** + * Converts a form element in the add view wizard to be AJAX-enabled. + * + * This function takes a form element and adds AJAX behaviors to it such that + * changing it triggers another part of the form to update automatically. It + * also adds a submit button to the form that appears next to the triggering + * element and that duplicates its functionality for users who do not have + * JavaScript enabled (the button is automatically hidden for users who do have + * JavaScript). + * + * To use this function, call it directly from your form builder function + * immediately after you have defined the form element that will serve as the + * JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may + * mean that the non-JavaScript fallback button does not appear in the correct + * place in the form. + * + * @param $wrapping_element + * The element whose child will server as the AJAX trigger. For example, if + * $form['some_wrapper']['triggering_element'] represents the element which + * will trigger the AJAX behavior, you would pass $form['some_wrapper'] for + * this parameter. + * @param $trigger_key + * The key within the wrapping element that identifies which of its children + * serves as the AJAX trigger. In the above example, you would pass + * 'triggering_element' for this parameter. + * @param $refresh_parents + * An array of parent keys that point to the part of the form that will be + * refreshed by AJAX. For example, if triggering the AJAX behavior should + * cause $form['dynamic_content']['section'] to be refreshed, you would pass + * array('dynamic_content', 'section') for this parameter. + */ +function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_parents) { + $seen_ids = &drupal_static(__FUNCTION__ . ':seen_ids', array()); + $seen_buttons = &drupal_static(__FUNCTION__ . ':seen_buttons', array()); + + // Add the AJAX behavior to the triggering element. + $triggering_element = &$wrapping_element[$trigger_key]; + $triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form'; + // We do not use drupal_html_id() to get an ID for the AJAX wrapper, because + // it remembers IDs across AJAX requests (and won't reuse them), but in our + // case we need to use the same ID from request to request so that the + // wrapper can be recognized by the AJAX system and its content can be + // dynamically updated. So instead, we will keep track of duplicate IDs + // (within a single request) on our own, later in this function. + $triggering_element['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refresh_parents) . '-wrapper'; + + // Add a submit button for users who do not have JavaScript enabled. It + // should be displayed next to the triggering element on the form. + $button_key = $trigger_key . '_trigger_update'; + $wrapping_element[$button_key] = array( + '#type' => 'submit', + // Hide this button when JavaScript is enabled. + '#attributes' => array('class' => array('js-hide')), + '#submit' => array('views_ui_nojs_submit'), + // Add a process function to limit this button's validation errors to the + // triggering element only. We have to do this in #process since until the + // form API has added the #parents property to the triggering element for + // us, we don't have any (easy) way to find out where its submitted values + // will eventually appear in $form_state['values']. + '#process' => array_merge(array('views_ui_add_limited_validation'), element_info_property('submit', '#process', array())), + // Add an after-build function that inserts a wrapper around the region of + // the form that needs to be refreshed by AJAX (so that the AJAX system can + // detect and dynamically update it). This is done in #after_build because + // it's a convenient place where we have automatic access to the complete + // form array, but also to minimize the chance that the HTML we add will + // get clobbered by code that runs after we have added it. + '#after_build' => array_merge(element_info_property('submit', '#after_build', array()), array('views_ui_add_ajax_wrapper')), + ); + // Copy #weight and #access from the triggering element to the button, so + // that the two elements will be displayed together. + foreach (array('#weight', '#access') as $property) { + if (isset($triggering_element[$property])) { + $wrapping_element[$button_key][$property] = $triggering_element[$property]; + } + } + // For easiest integration with the form API and the testing framework, we + // always give the button a unique #value, rather than playing around with + // #name. + $button_title = !empty($triggering_element['#title']) ? $triggering_element['#title'] : $trigger_key; + if (empty($seen_buttons[$button_title])) { + $wrapping_element[$button_key]['#value'] = t('Update "@title" choice', array( + '@title' => $button_title, + )); + $seen_buttons[$button_title] = 1; + } + else { + $wrapping_element[$button_key]['#value'] = t('Update "@title" choice (@number)', array( + '@title' => $button_title, + '@number' => ++$seen_buttons[$button_title], + )); + } + + // Attach custom data to the triggering element and submit button, so we can + // use it in both the process function and AJAX callback. + $ajax_data = array( + 'wrapper' => $triggering_element['#ajax']['wrapper'], + 'trigger_key' => $trigger_key, + 'refresh_parents' => $refresh_parents, + // Keep track of duplicate wrappers so we don't add the same wrapper to the + // page more than once. + 'duplicate_wrapper' => !empty($seen_ids[$triggering_element['#ajax']['wrapper']]), + ); + $seen_ids[$triggering_element['#ajax']['wrapper']] = TRUE; + $triggering_element['#views_ui_ajax_data'] = $ajax_data; + $wrapping_element[$button_key]['#views_ui_ajax_data'] = $ajax_data; +} + +/** + * Processes a non-JavaScript fallback submit button to limit its validation errors. + */ +function views_ui_add_limited_validation($element, &$form_state) { + // Retrieve the AJAX triggering element so we can determine its parents. (We + // know it's at the same level of the complete form array as the submit + // button, so all we have to do to find it is swap out the submit button's + // last array parent.) + $array_parents = $element['#array_parents']; + array_pop($array_parents); + $array_parents[] = $element['#views_ui_ajax_data']['trigger_key']; + $ajax_triggering_element = drupal_array_get_nested_value($form_state['complete_form'], $array_parents); + + // Limit this button's validation to the AJAX triggering element, so it can + // update the form for that change without requiring that the rest of the + // form be filled out properly yet. + $element['#limit_validation_errors'] = array($ajax_triggering_element['#parents']); + + // If we are in the process of a form submission and this is the button that + // was clicked, the form API workflow in form_builder() will have already + // copied it to $form_state['triggering_element'] before our #process + // function is run. So we need to make the same modifications in $form_state + // as we did to the element itself, to ensure that #limit_validation_errors + // will actually be set in the correct place. + if (!empty($form_state['triggering_element'])) { + $clicked_button = &$form_state['triggering_element']; + if ($clicked_button['#name'] == $element['#name'] && $clicked_button['#value'] == $element['#value']) { + $clicked_button['#limit_validation_errors'] = $element['#limit_validation_errors']; + } + } + + return $element; +} + +/** + * After-build function that adds a wrapper to a form region (for AJAX refreshes). + * + * This function inserts a wrapper around the region of the form that needs to + * be refreshed by AJAX, based on information stored in the corresponding + * submit button form element. + */ +function views_ui_add_ajax_wrapper($element, &$form_state) { + // Don't add the wrapper
if the same one was already inserted on this + // form. + if (empty($element['#views_ui_ajax_data']['duplicate_wrapper'])) { + // Find the region of the complete form that needs to be refreshed by AJAX. + // This was earlier stored in a property on the element. + $complete_form = &$form_state['complete_form']; + $refresh_parents = $element['#views_ui_ajax_data']['refresh_parents']; + $refresh_element = drupal_array_get_nested_value($complete_form, $refresh_parents); + + // The HTML ID that AJAX expects was also stored in a property on the + // element, so use that information to insert the wrapper
here. + $id = $element['#views_ui_ajax_data']['wrapper']; + $refresh_element += array( + '#prefix' => '', + '#suffix' => '', + ); + $refresh_element['#prefix'] = '
' . $refresh_element['#prefix']; + $refresh_element['#suffix'] .= '
'; + + // Copy the element that needs to be refreshed back into the form, with our + // modifications to it. + drupal_array_set_nested_value($complete_form, $refresh_parents, $refresh_element); + } + + return $element; +} + +/** + * Updates a part of the add view form via AJAX. + * + * @return + * The part of the form that has changed. + */ +function views_ui_ajax_update_form($form, $form_state) { + // The region that needs to be updated was stored in a property of the + // triggering element by views_ui_add_ajax_trigger(), so all we have to do is + // retrieve that here. + return drupal_array_get_nested_value($form, $form_state['triggering_element']['#views_ui_ajax_data']['refresh_parents']); +} + +/** + * Non-Javascript fallback for updating the add view form. + */ +function views_ui_nojs_submit($form, &$form_state) { + $form_state['rebuild'] = TRUE; +} + +/** + * Validate the add view form. + */ +function views_ui_wizard_form_validate($form, &$form_state) { + $wizard = views_ui_get_wizard($form_state['values']['show']['wizard_key']); + $form_state['wizard'] = $wizard; + $form_state['wizard_instance'] = views_get_plugin('wizard', $wizard['id']); + $errors = $form_state['wizard_instance']->validateView($form, $form_state); + foreach ($errors as $name => $message) { + form_set_error($name, $message); + } +} + +/** + * Process the add view form, 'save'. + */ +function views_ui_add_form_save_submit($form, &$form_state) { + try { + $view = $form_state['wizard_instance']->create_view($form, $form_state); + } + catch (WizardException $e) { + drupal_set_message($e->getMessage(), 'error'); + $form_state['redirect'] = 'admin/structure/views'; + } + $view->save(); + + $form_state['redirect'] = 'admin/structure/views'; + if (!empty($view->displayHandlers['page'])) { + $display = $view->displayHandlers['page']; + if ($display->hasPath()) { + $one_path = $display->getOption('path'); + if (strpos($one_path, '%') === FALSE) { + $form_state['redirect'] = $one_path; // PATH TO THE VIEW IF IT HAS ONE + return; + } + } + } + drupal_set_message(t('Your view was saved. You may edit it from the list below.')); +} + +/** + * Process the add view form, 'continue'. + */ +function views_ui_add_form_store_edit_submit($form, &$form_state) { + try { + $view = $form_state['wizard_instance']->create_view($form, $form_state); + } + catch (WizardException $e) { + drupal_set_message($e->getMessage(), 'error'); + $form_state['redirect'] = 'admin/structure/views'; + } + // Just cache it temporarily to edit it. + views_ui_cache_set($view); + + // If there is a destination query, ensure we still redirect the user to the + // edit view page, and then redirect the user to the destination. + // @todo: Revisit this when http://drupal.org/node/1668866 is in. + $destination = array(); + $query = drupal_container()->get('request')->query; + if ($query->has('destination')) { + $destination = drupal_get_destination(); + $query->remove('destination'); + } + $form_state['redirect'] = array('admin/structure/views/view/' . $view->storage->name, array('query' => $destination)); +} + +/** + * Cancel the add view form. + */ +function views_ui_add_form_cancel_submit($form, &$form_state) { + $form_state['redirect'] = 'admin/structure/views'; +} + +/** + * Form element validation handler for a taxonomy autocomplete field. + * + * This allows a taxonomy autocomplete field to be validated outside the + * standard Field API workflow, without passing in a complete field widget. + * Instead, all that is required is that $element['#field_name'] contain the + * name of the taxonomy autocomplete field that is being validated. + * + * This function is currently not used for validation directly, although it + * could be. Instead, it is only used to store the term IDs and vocabulary name + * in the element value, based on the tags that the user typed in. + * + * @see taxonomy_autocomplete_validate() + */ +function views_ui_taxonomy_autocomplete_validate($element, &$form_state) { + $value = array(); + if ($tags = $element['#value']) { + // Get the machine names of the vocabularies we will search, keyed by the + // vocabulary IDs. + $field = field_info_field($element['#field_name']); + $vocabularies = array(); + if (!empty($field['settings']['allowed_values'])) { + foreach ($field['settings']['allowed_values'] as $tree) { + if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) { + $vocabularies[$vocabulary->vid] = $tree['vocabulary']; + } + } + } + // Store the term ID of each (valid) tag that the user typed. + $typed_terms = drupal_explode_tags($tags); + foreach ($typed_terms as $typed_term) { + if ($terms = entity_load_multiple_by_properties('taxonomy_term', array('name' => trim($typed_term), 'vid' => array_keys($vocabularies)))) { + $term = array_pop($terms); + $value['tids'][] = $term->tid; + } + } + // Store the term IDs along with the name of the vocabulary. Currently + // Views (as well as the Field UI) assumes that there will only be one + // vocabulary, although technically the API allows there to be more than + // one. + if (!empty($value['tids'])) { + $value['tids'] = array_unique($value['tids']); + $value['vocabulary'] = array_pop($vocabularies); + } + } + form_set_value($element, $value, $form_state); +} + +/** + * Page to delete a view. + */ +function views_ui_break_lock_confirm($form, &$form_state, ViewUI $view) { + $form_state['view'] = &$view; + $form = array(); + + if (empty($view->locked)) { + $form['message']['#markup'] = t('There is no lock on view %name to break.', array('%name' => $view->storage->name)); + return $form; + } + + $cancel = drupal_container()->get('request')->query->get('cancel'); + if (empty($cancel)) { + $cancel = 'admin/structure/views/view/' . $view->storage->name . '/edit'; + } + + $account = user_load($view->locked->owner); + $form = confirm_form($form, + t('Are you sure you want to break the lock on view %name?', + array('%name' => $view->storage->name)), + $cancel, + t('By breaking this lock, any unsaved changes made by !user will be lost!', array('!user' => theme('username', array('account' => $account)))), + t('Break lock'), + t('Cancel')); + $form['actions']['submit']['#submit'][] = array($view, 'submitBreakLock'); + return $form; +} + +/** + * Page callback for the Edit View page. + */ +function views_ui_edit_page(ViewUI $view, $display_id = NULL) { + $display_id = $view->getDisplayEditPage($display_id); + if (!in_array($display_id, array(MENU_ACCESS_DENIED, MENU_NOT_FOUND))) { + $build = array(); + $form_state['build_info']['args'] = array($display_id); + $form_state['build_info']['callback'] = array($view, 'buildEditForm'); + $build['edit_form'] = drupal_build_form('views_ui_edit_form', $form_state); + $build['preview'] = views_ui_build_preview($view, $display_id, FALSE); + } + else { + $build = $display_id; + } + + return $build; +} + +function views_ui_build_preview(ViewUI $view, $display_id, $render = TRUE) { + $build = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('id' => 'views-preview-wrapper', 'class' => 'views-admin clearfix'), + ); + + $form_state['build_info']['args'] = array($display_id); + $form_state['build_info']['callback'] = array($view, 'buildPreviewForm'); + $build['controls'] = drupal_build_form('views_ui_preview_form', $form_state); + + $args = array(); + if (!empty($form_state['values']['view_args'])) { + $args = explode('/', $form_state['values']['view_args']); + } + + if ($render) { + $clone = $view->cloneView(); + $build['preview'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('id' => 'views-live-preview'), + '#markup' => $clone->renderPreview($display_id, $args), + ); + } + + return $build; +} + +/** + * Move form elements into fieldsets for presentation purposes. + * + * Many views forms use #tree = TRUE to keep their values in a hierarchy for + * easier storage. Moving the form elements into fieldsets during form building + * would break up that hierarchy. Therefore, we wait until the pre_render stage, + * where any changes we make affect presentation only and aren't reflected in + * $form_state['values']. + */ +function views_ui_pre_render_add_fieldset_markup($form) { + foreach (element_children($form) as $key) { + $element = $form[$key]; + // In our form builder functions, we added an arbitrary #fieldset property + // to any element that belongs in a fieldset. If this form element has that + // property, move it into its fieldset. + if (isset($element['#fieldset']) && isset($form[$element['#fieldset']])) { + $form[$element['#fieldset']][$key] = $element; + // Remove the original element this duplicates. + unset($form[$key]); + } + } + + return $form; +} + +/** + * Flattens the structure of an element containing the #flatten property. + * + * If a form element has #flatten = TRUE, then all of it's children + * get moved to the same level as the element itself. + * So $form['to_be_flattened'][$key] becomes $form[$key], and + * $form['to_be_flattened'] gets unset. + */ +function views_ui_pre_render_flatten_data($form) { + foreach (element_children($form) as $key) { + $element = $form[$key]; + if (!empty($element['#flatten'])) { + foreach (element_children($element) as $child_key) { + $form[$child_key] = $form[$key][$child_key]; + } + // All done, remove the now-empty parent. + unset($form[$key]); + } + } + + return $form; +} + +/** + * Moves argument options into their place. + * + * When configuring the default argument behavior, almost each of the radio + * buttons has its own fieldset shown bellow it when the radio button is + * clicked. That fieldset is created through a custom form process callback. + * Each element that has #argument_option defined and pointing to a default + * behavior gets moved to the appropriate fieldset. + * So if #argument_option is specified as 'default', the element is moved + * to the 'default_options' fieldset. + */ +function views_ui_pre_render_move_argument_options($form) { + foreach (element_children($form) as $key) { + $element = $form[$key]; + if (!empty($element['#argument_option'])) { + $container_name = $element['#argument_option'] . '_options'; + if (isset($form['no_argument']['default_action'][$container_name])) { + $form['no_argument']['default_action'][$container_name][$key] = $element; + } + // Remove the original element this duplicates. + unset($form[$key]); + } + } + return $form; +} + +/** + * Validate that a view is complete and whole. + */ +function views_ui_edit_view_form_validate($form, &$form_state) { + // Do not validate cancel or delete or revert. + if (empty($form_state['clicked_button']['#value']) || $form_state['clicked_button']['#value'] != t('Save')) { + return; + } + + $errors = $form_state['view']->validate(); + if ($errors !== TRUE) { + foreach ($errors as $error) { + form_set_error('', $error); + } + } +} + +/** + * Submit handler for the edit view form. + */ +function views_ui_edit_view_form_submit($form, &$form_state) { + // Go through and remove displayed scheduled for removal. + foreach ($form_state['view']->storage->display as $id => $display) { + if (!empty($display['deleted'])) { + unset($form_state['view']->displayHandlers[$id]); + unset($form_state['view']->storage->display[$id]); + } + } + // Rename display ids if needed. + foreach ($form_state['view']->displayHandlers as $id => $display) { + if (!empty($display->display['new_id'])) { + $new_id = $display->display['new_id']; + $form_state['view']->displayHandlers[$new_id] = $form_state['view']->displayHandlers[$id]; + $form_state['view']->displayHandlers[$new_id]->display['id'] = $new_id; + + $form_state['view']->storage->display[$new_id] = $form_state['view']->storage->display[$id]; + unset($form_state['view']->storage->display[$id]); + // Redirect the user to the renamed display to be sure that the page itself exists and doesn't throw errors. + $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->storage->name . '/edit/' . $new_id; + } + } + + // Direct the user to the right url, if the path of the display has changed. + $query = drupal_container()->get('request')->query; + // @todo: Revisit this when http://drupal.org/node/1668866 is in. + $destination = $query->get('destination'); + if (!empty($destination)) { + // Find out the first display which has a changed path and redirect to this url. + $old_view = views_get_view($form_state['view']->storage->name); + foreach ($old_view->displayHandlers as $id => $display) { + // Only check for displays with a path. + if (!isset($display->display['display_options']['path'])) { + continue; + } + $old_path = $display->display['display_options']['path']; + if (($display->display['display_plugin'] == 'page') && ($old_path == $destination) && ($old_path != $form_state['view']->display[$id]->display['display_options']['path'])) { + $destination = $form_state['view']->displayHandlers[$id]->display['display_options']['path']; + $query->remove('destination'); + } + } + $form_state['redirect'] = $destination; + } + + $form_state['view']->save(); + drupal_set_message(t('The view %name has been saved.', array('%name' => $form_state['view']->storage->getHumanName()))); + + // Remove this view from cache so we can edit it properly. + drupal_container()->get('user.tempstore')->get('views')->delete($form_state['view']->storage->name); +} + +/** + * Submit handler for the edit view form. + */ +function views_ui_edit_view_form_cancel($form, &$form_state) { + // Remove this view from cache so edits will be lost. + drupal_container()->get('user.tempstore')->get('views')->delete($form_state['view']->storage->name); + if (empty($form['view']->vid)) { + // I seem to have to drupal_goto here because I can't get fapi to + // honor the redirect target. Not sure what I screwed up here. + drupal_goto('admin/structure/views'); + } +} + +/** + * Add a elements from the DOM, wraps them + * in an unordered list, then appends them to the list of tabs. + */ +Drupal.behaviors.viewsUiRenderAddViewButton = {}; + +Drupal.behaviors.viewsUiRenderAddViewButton.attach = function (context, settings) { + + "use strict"; + + var $ = jQuery; + // Build the add display menu and pull the display input buttons into it. + var $menu = $('#views-display-menu-tabs', context).once('views-ui-render-add-view-button-processed'); + + if (!$menu.length) { + return; + } + var $addDisplayDropdown = $('
  • ' + Drupal.t('Add') + '
  • '); + var $displayButtons = $menu.nextAll('input.add-display').detach(); + $displayButtons.appendTo($addDisplayDropdown.find('.action-list')).wrap('
  • ') + .parent().first().addClass('first').end().last().addClass('last'); + // Remove the 'Add ' prefix from the button labels since they're being palced + // in an 'Add' dropdown. + // @todo This assumes English, but so does $addDisplayDropdown above. Add + // support for translation. + $displayButtons.each(function () { + var label = $(this).val(); + if (label.substr(0, 4) === 'Add ') { + $(this).val(label.substr(4)); + } + }); + $addDisplayDropdown.appendTo($menu); + + // Add the click handler for the add display button + $('li.add > a', $menu).bind('click', function (event) { + event.preventDefault(); + var $trigger = $(this); + Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu($trigger); + }); + // Add a mouseleave handler to close the dropdown when the user mouses + // away from the item. We use mouseleave instead of mouseout because + // the user is going to trigger mouseout when she moves from the trigger + // link to the sub menu items. + // We use the live binder because the open class on this item will be + // toggled on and off and we want the handler to take effect in the cases + // that the class is present, but not when it isn't. + $('li.add', $menu).on('mouseleave', function (event) { + var $this = $(this); + var $trigger = $this.children('a[href="#"]'); + if ($this.children('.action-list').is(':visible')) { + Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu($trigger); + } + }); +}; + +/** + * @note [@jessebeach] I feel like the following should be a more generic function and + * not written specifically for this UI, but I'm not sure where to put it. + */ +Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu = function ($trigger) { + + "use strict"; + + $trigger.parent().toggleClass('open'); + $trigger.next().slideToggle('fast'); +}; + +Drupal.behaviors.viewsUiSearchOptions = {}; + +Drupal.behaviors.viewsUiSearchOptions.attach = function (context) { + + "use strict"; + + var $ = jQuery; + // The add item form may have an id of views-ui-add-item-form--n. + var $form = $(context).find('form[id^="views-ui-add-item-form"]').first(); + // Make sure we don't add more than one event handler to the same form. + $form = $form.once('views-ui-filter-options'); + if ($form.length) { + new Drupal.viewsUi.OptionsSearch($form); + } +}; + +/** + * Constructor for the viewsUi.OptionsSearch object. + * + * The OptionsSearch object filters the available options on a form according + * to the user's search term. Typing in "taxonomy" will show only those options + * containing "taxonomy" in their label. + */ +Drupal.viewsUi.OptionsSearch = function ($form) { + + "use strict"; + + this.$form = $form; + // Add a keyup handler to the search box. + this.$searchBox = this.$form.find('#edit-options-search'); + this.$searchBox.keyup(jQuery.proxy(this.handleKeyup, this)); + // Get a list of option labels and their corresponding divs and maintain it + // in memory, so we have as little overhead as possible at keyup time. + this.options = this.getOptions(this.$form.find('.filterable-option')); + // Restripe on initial loading. + this.handleKeyup(); + // Trap the ENTER key in the search box so that it doesn't submit the form. + this.$searchBox.keypress(function(event) { + if (event.which === 13) { + event.preventDefault(); + } + }); +}; + +/** + * Assemble a list of all the filterable options on the form. + * + * @param $allOptions + * A jQuery object representing the rows of filterable options to be + * shown and hidden depending on the user's search terms. + */ +Drupal.viewsUi.OptionsSearch.prototype.getOptions = function ($allOptions) { + + "use strict"; + + var $ = jQuery; + var i, $label, $description, $option; + var options = []; + var length = $allOptions.length; + for (i = 0; i < length; i++) { + $option = $($allOptions[i]); + $label = $option.find('label'); + $description = $option.find('div.description'); + options[i] = { + // Search on the lowercase version of the label text + description. + 'searchText': $label.text().toLowerCase() + " " + $description.text().toLowerCase(), + // Maintain a reference to the jQuery object for each row, so we don't + // have to create a new object inside the performance-sensitive keyup + // handler. + '$div': $option + }; + } + return options; +}; + +/** + * Keyup handler for the search box that hides or shows the relevant options. + */ +Drupal.viewsUi.OptionsSearch.prototype.handleKeyup = function (event) { + + "use strict"; + + var found, i, j, option, search, words, wordsLength, zebraClass, zebraCounter; + + // Determine the user's search query. The search text has been converted to + // lowercase. + search = this.$searchBox.val().toLowerCase(); + words = search.split(' '); + wordsLength = words.length; + + // Start the counter for restriping rows. + zebraCounter = 0; + + // Search through the search texts in the form for matching text. + var length = this.options.length; + for (i = 0; i < length; i++) { + // Use a local variable for the option being searched, for performance. + option = this.options[i]; + found = true; + // Each word in the search string has to match the item in order for the + // item to be shown. + for (j = 0; j < wordsLength; j++) { + if (option.searchText.indexOf(words[j]) === -1) { + found = false; + } + } + if (found) { + // Show the checkbox row, and restripe it. + zebraClass = (zebraCounter % 2) ? 'odd' : 'even'; + option.$div.show(); + option.$div.removeClass('even odd'); + option.$div.addClass(zebraClass); + zebraCounter++; + } + else { + // The search string wasn't found; hide this item. + option.$div.hide(); + } + } +}; + +Drupal.behaviors.viewsUiPreview = {}; +Drupal.behaviors.viewsUiPreview.attach = function (context, settings) { + + "use strict"; + + var $ = jQuery; + + // Only act on the edit view form. + var contextualFiltersBucket = $('.views-display-column .views-ui-display-tab-bucket.contextual-filters', context); + if (contextualFiltersBucket.length === 0) { + return; + } + + // If the display has no contextual filters, hide the form where you enter + // the contextual filters for the live preview. If it has contextual filters, + // show the form. + var contextualFilters = $('.views-display-setting a', contextualFiltersBucket); + if (contextualFilters.length) { + $('#preview-args').parent().show(); + } + else { + $('#preview-args').parent().hide(); + } + + // Executes an initial preview. + if ($('#edit-displays-live-preview').once('edit-displays-live-preview').is(':checked')) { + $('#preview-submit').once('edit-displays-live-preview').click(); + } +}; + +Drupal.behaviors.viewsUiRearrangeFilter = {}; +Drupal.behaviors.viewsUiRearrangeFilter.attach = function (context, settings) { + + "use strict"; + + var $ = jQuery; + // Only act on the rearrange filter form. + if (typeof Drupal.tableDrag === 'undefined' || typeof Drupal.tableDrag['views-rearrange-filters'] === 'undefined') { + return; + } + + var table = $('#views-rearrange-filters', context).once('views-rearrange-filters'); + var operator = $('.form-item-filter-groups-operator', context).once('views-rearrange-filters'); + if (table.length) { + new Drupal.viewsUi.rearrangeFilterHandler(table, operator); + } +}; + +/** + * Improve the UI of the rearrange filters dialog box. + */ +Drupal.viewsUi.rearrangeFilterHandler = function (table, operator) { + + "use strict"; + + var $ = jQuery; + // Keep a reference to the being altered and to the div containing + // the filter groups operator dropdown (if it exists). + this.table = table; + this.operator = operator; + this.hasGroupOperator = this.operator.length > 0; + + // Keep a reference to all draggable rows within the table. + this.draggableRows = $('.draggable', table); + + // Keep a reference to the buttons for adding and removing filter groups. + this.addGroupButton = $('input#views-add-group'); + this.removeGroupButtons = $('input.views-remove-group', table); + + // Add links that duplicate the functionality of the (hidden) add and remove + // buttons. + this.insertAddRemoveFilterGroupLinks(); + + // When there is a filter groups operator dropdown on the page, create + // duplicates of the dropdown between each pair of filter groups. + if (this.hasGroupOperator) { + this.dropdowns = this.duplicateGroupsOperator(); + this.syncGroupsOperators(); + } + + // Add methods to the tableDrag instance to account for operator cells (which + // span multiple rows), the operator labels next to each filter (e.g., "And" + // or "Or"), the filter groups, and other special aspects of this tableDrag + // instance. + this.modifyTableDrag(); + + // Initialize the operator labels (e.g., "And" or "Or") that are displayed + // next to the filters in each group, and bind a handler so that they change + // based on the values of the operator dropdown within that group. + this.redrawOperatorLabels(); + $('.views-group-title select', table) + .once('views-rearrange-filter-handler') + .bind('change.views-rearrange-filter-handler', $.proxy(this, 'redrawOperatorLabels')); + + // Bind handlers so that when a "Remove" link is clicked, we: + // - Update the rowspans of cells containing an operator dropdown (since they + // need to change to reflect the number of rows in each group). + // - Redraw the operator labels next to the filters in the group (since the + // filter that is currently displayed last in each group is not supposed to + // have a label display next to it). + $('a.views-groups-remove-link', this.table) + .once('views-rearrange-filter-handler') + .bind('click.views-rearrange-filter-handler', $.proxy(this, 'updateRowspans')) + .bind('click.views-rearrange-filter-handler', $.proxy(this, 'redrawOperatorLabels')); +}; + +/** + * Insert links that allow filter groups to be added and removed. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.insertAddRemoveFilterGroupLinks = function () { + + "use strict"; + + var $ = jQuery; + + // Insert a link for adding a new group at the top of the page, and make it + // match the action links styling used in a typical page.tpl.php. Note that + // Drupal does not provide a theme function for this markup, so this is the + // best we can do. + $('') + .prependTo(this.table.parent()) + // When the link is clicked, dynamically click the hidden form button for + // adding a new filter group. + .once('views-rearrange-filter-handler') + .bind('click.views-rearrange-filter-handler', $.proxy(this, 'clickAddGroupButton')); + + // Find each (visually hidden) button for removing a filter group and insert + // a link next to it. + var length = this.removeGroupButtons.length; + for (i = 0; i < length; i++) { + var $removeGroupButton = $(this.removeGroupButtons[i]); + var buttonId = $removeGroupButton.attr('id'); + $('' + Drupal.t('Remove group') + '') + .insertBefore($removeGroupButton) + // When the link is clicked, dynamically click the corresponding form + // button. + .once('views-rearrange-filter-handler') + .bind('click.views-rearrange-filter-handler', {buttonId: buttonId}, $.proxy(this, 'clickRemoveGroupButton')); + } +}; + +/** + * Dynamically click the button that adds a new filter group. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.clickAddGroupButton = function () { + + "use strict"; + + // Due to conflicts between Drupal core's AJAX system and the Views AJAX + // system, the only way to get this to work seems to be to trigger both the + // .mousedown() and .submit() events. + this.addGroupButton.mousedown(); + this.addGroupButton.submit(); + event.preventDefault(); +}; + +/** + * Dynamically click a button for removing a filter group. + * + * @param event + * Event being triggered, with event.data.buttonId set to the ID of the + * form button that should be clicked. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.clickRemoveGroupButton = function (event) { + + "use strict"; + + // For some reason, here we only need to trigger .submit(), unlike for + // Drupal.viewsUi.rearrangeFilterHandler.prototype.clickAddGroupButton() + // where we had to trigger .mousedown() also. + jQuery('input#' + event.data.buttonId, this.table).submit(); + event.preventDefault(); +}; + +/** + * Move the groups operator so that it's between the first two groups, and + * duplicate it between any subsequent groups. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.duplicateGroupsOperator = function () { + + "use strict"; + + var $ = jQuery; + var dropdowns, newRow; + + var titleRows = $('tr.views-group-title'), titleRow; + + // Get rid of the explanatory text around the operator; its placement is + // explanatory enough. + this.operator.find('label').add('div.description').addClass('element-invisible'); + this.operator.find('select').addClass('form-select'); + + // Keep a list of the operator dropdowns, so we can sync their behavior later. + dropdowns = this.operator; + + // Move the operator to a new row just above the second group. + titleRow = $('tr#views-group-title-2'); + newRow = $(''); + newRow.find('td').append(this.operator); + newRow.insertBefore(titleRow); + var i, length = titleRows.length; + // Starting with the third group, copy the operator to a new row above the + // group title. + for (i = 2; i < length; i++) { + titleRow = $(titleRows[i]); + // Make a copy of the operator dropdown and put it in a new table row. + var fakeOperator = this.operator.clone(); + fakeOperator.attr('id', ''); + newRow = $(''); + newRow.find('td').append(fakeOperator); + newRow.insertBefore(titleRow); + dropdowns = dropdowns.add(fakeOperator); + } + + return dropdowns; +}; + +/** + * Make the duplicated groups operators change in sync with each other. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.syncGroupsOperators = function () { + + "use strict"; + + if (this.dropdowns.length < 2) { + // We only have one dropdown (or none at all), so there's nothing to sync. + return; + } + + this.dropdowns.change(jQuery.proxy(this, 'operatorChangeHandler')); +}; + +/** + * Click handler for the operators that appear between filter groups. + * + * Forces all operator dropdowns to have the same value. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.operatorChangeHandler = function (event) { + + "use strict"; + + var $ = jQuery; + var $target = $(event.target); + var operators = this.dropdowns.find('select').not($target); + + // Change the other operators to match this new value. + operators.val($target.val()); +}; + +Drupal.viewsUi.rearrangeFilterHandler.prototype.modifyTableDrag = function () { + + "use strict"; + + var tableDrag = Drupal.tableDrag['views-rearrange-filters']; + var filterHandler = this; + + /** + * Override the row.onSwap method from tabledrag.js. + * + * When a row is dragged to another place in the table, several things need + * to occur. + * - The row needs to be moved so that it's within one of the filter groups. + * - The operator cells that span multiple rows need their rowspan attributes + * updated to reflect the number of rows in each group. + * - The operator labels that are displayed next to each filter need to be + * redrawn, to account for the row's new location. + */ + tableDrag.row.prototype.onSwap = function () { + if (filterHandler.hasGroupOperator) { + // Make sure the row that just got moved (this.group) is inside one of + // the filter groups (i.e. below an empty marker row or a draggable). If + // it isn't, move it down one. + var thisRow = jQuery(this.group); + var previousRow = thisRow.prev('tr'); + if (previousRow.length && !previousRow.hasClass('group-message') && !previousRow.hasClass('draggable')) { + // Move the dragged row down one. + var next = thisRow.next(); + if (next.is('tr')) { + this.swap('after', next); + } + } + filterHandler.updateRowspans(); + } + // Redraw the operator labels that are displayed next to each filter, to + // account for the row's new location. + filterHandler.redrawOperatorLabels(); + }; + + /** + * Override the onDrop method from tabledrag.js. + */ + tableDrag.onDrop = function () { + var $ = jQuery; + + // If the tabledrag change marker (i.e., the "*") has been inserted inside + // a row after the operator label (i.e., "And" or "Or") rearrange the items + // so the operator label continues to appear last. + var changeMarker = $(this.oldRowElement).find('.tabledrag-changed'); + if (changeMarker.length) { + // Search for occurrences of the operator label before the change marker, + // and reverse them. + var operatorLabel = changeMarker.prevAll('.views-operator-label'); + if (operatorLabel.length) { + operatorLabel.insertAfter(changeMarker); + } + } + + // Make sure the "group" dropdown is properly updated when rows are dragged + // into an empty filter group. This is borrowed heavily from the block.js + // implementation of tableDrag.onDrop(). + var groupRow = $(this.rowObject.element).prevAll('tr.group-message').get(0); + var groupName = groupRow.className.replace(/([^ ]+[ ]+)*group-([^ ]+)-message([ ]+[^ ]+)*/, '$2'); + var groupField = $('select.views-group-select', this.rowObject.element); + if ($(this.rowObject.element).prev('tr').is('.group-message') && !groupField.is('.views-group-select-' + groupName)) { + var oldGroupName = groupField.attr('class').replace(/([^ ]+[ ]+)*views-group-select-([^ ]+)([ ]+[^ ]+)*/, '$2'); + groupField.removeClass('views-group-select-' + oldGroupName).addClass('views-group-select-' + groupName); + groupField.val(groupName); + } + }; +}; + +/** + * Redraw the operator labels that are displayed next to each filter. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.redrawOperatorLabels = function () { + + "use strict"; + + var $ = jQuery; + for (i = 0; i < this.draggableRows.length; i++) { + // Within the row, the operator labels are displayed inside the first table + // cell (next to the filter name). + var $draggableRow = $(this.draggableRows[i]); + var $firstCell = $('td:first', $draggableRow); + if ($firstCell.length) { + // The value of the operator label ("And" or "Or") is taken from the + // first operator dropdown we encounter, going backwards from the current + // row. This dropdown is the one associated with the current row's filter + // group. + var operatorValue = $draggableRow.prevAll('.views-group-title').find('option:selected').html(); + var operatorLabel = '' + operatorValue + ''; + // If the next visible row after this one is a draggable filter row, + // display the operator label next to the current row. (Checking for + // visibility is necessary here since the "Remove" links hide the removed + // row but don't actually remove it from the document). + var $nextRow = $draggableRow.nextAll(':visible').eq(0); + var $existingOperatorLabel = $firstCell.find('.views-operator-label'); + if ($nextRow.hasClass('draggable')) { + // If an operator label was already there, replace it with the new one. + if ($existingOperatorLabel.length) { + $existingOperatorLabel.replaceWith(operatorLabel); + } + // Otherwise, append the operator label to the end of the table cell. + else { + $firstCell.append(operatorLabel); + } + } + // If the next row doesn't contain a filter, then this is the last row + // in the group. We don't want to display the operator there (since + // operators should only display between two related filters, e.g. + // "filter1 AND filter2 AND filter3"). So we remove any existing label + // that this row has. + else { + $existingOperatorLabel.remove(); + } + } + } +}; + +/** + * Update the rowspan attribute of each cell containing an operator dropdown. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.updateRowspans = function () { + + "use strict"; + + var $ = jQuery; + var i, $row, $currentEmptyRow, draggableCount, $operatorCell; + var rows = $(this.table).find('tr'); + var length = rows.length; + for (i = 0; i < length; i++) { + $row = $(rows[i]); + if ($row.hasClass('views-group-title')) { + // This row is a title row. + // Keep a reference to the cell containing the dropdown operator. + $operatorCell = $($row.find('td.group-operator')); + // Assume this filter group is empty, until we find otherwise. + draggableCount = 0; + $currentEmptyRow = $row.next('tr'); + $currentEmptyRow.removeClass('group-populated').addClass('group-empty'); + // The cell with the dropdown operator should span the title row and + // the "this group is empty" row. + $operatorCell.attr('rowspan', 2); + } + else if (($row).hasClass('draggable') && $row.is(':visible')) { + // We've found a visible filter row, so we now know the group isn't empty. + draggableCount++; + $currentEmptyRow.removeClass('group-empty').addClass('group-populated'); + // The operator cell should span all draggable rows, plus the title. + $operatorCell.attr('rowspan', draggableCount + 1); + } + } +}; + +Drupal.behaviors.viewsFilterConfigSelectAll = {}; + +/** + * Add a select all checkbox, which checks each checkbox at once. + */ +Drupal.behaviors.viewsFilterConfigSelectAll.attach = function(context) { + + "use strict"; + + var $ = jQuery; + // Show the select all checkbox. + $('#views-ui-config-item-form div.form-item-options-value-all', context).once(function() { + $(this).show(); + }) + .find('input[type=checkbox]') + .click(function() { + var checked = $(this).is(':checked'); + // Update all checkbox beside the select all checkbox. + $(this).parents('.form-checkboxes').find('input[type=checkbox]').each(function() { + $(this).attr('checked', checked); + }); + }); + // Uncheck the select all checkbox if any of the others are unchecked. + $('#views-ui-config-item-form div.form-type-checkbox').not($('.form-item-options-value-all')).find('input[type=checkbox]').each(function() { + $(this).click(function() { + if ($(this).is('checked') === 0) { + $('#edit-options-value-all').removeAttr('checked'); + } + }); + }); +}; + +/** + * Ensure the desired default button is used when a form is implicitly submitted via an ENTER press on textfields, radios, and checkboxes. + * + * @see http://www.w3.org/TR/html5/association-of-controls-and-forms.html#implicit-submission + */ +Drupal.behaviors.viewsImplicitFormSubmission = {}; +Drupal.behaviors.viewsImplicitFormSubmission.attach = function (context, settings) { + + "use strict"; + + var $ = jQuery; + $(':text, :password, :radio, :checkbox', context).once('viewsImplicitFormSubmission', function() { + $(this).keypress(function(event) { + if (event.which === 13) { + var formId = this.form.id; + if (formId && settings.viewsImplicitFormSubmission && settings.viewsImplicitFormSubmission[formId] && settings.viewsImplicitFormSubmission[formId].defaultButton) { + event.preventDefault(); + var buttonId = settings.viewsImplicitFormSubmission[formId].defaultButton; + var $button = $('#' + buttonId, this.form); + if ($button.length === 1 && $button.is(':enabled')) { + if (Drupal.ajax && Drupal.ajax[buttonId]) { + $button.trigger(Drupal.ajax[buttonId].element_settings.event); + } + else { + $button.click(); + } + } + } + } + }); + }); +}; + +/** + * Remove icon class from elements that are themed as buttons or dropbuttons. + */ +Drupal.behaviors.viewsRemoveIconClass = {}; +Drupal.behaviors.viewsRemoveIconClass.attach = function (context, settings) { + + "use strict"; + + jQuery(context).find('.dropbutton').once('dropbutton-icon', function () { + jQuery(this).find('.icon').removeClass('icon'); + }); +}; + +/** + * Change "Expose filter" buttons into checkboxes. + */ +Drupal.behaviors.viewsUiCheckboxify = {}; +Drupal.behaviors.viewsUiCheckboxify.attach = function (context, settings) { + + "use strict"; + + var $ = jQuery; + var $buttons = $('#edit-options-expose-button-button, #edit-options-group-button-button').once('views-ui-checkboxify'); + var length = $buttons.length; + var i; + for (i = 0; i < length; i++) { + new Drupal.viewsUi.Checkboxifier($buttons[i]); + } +}; + +/** + * Change the default widget to select the default group according to the + * selected widget for the exposed group. + */ +Drupal.behaviors.viewsUiChangeDefaultWidget = {}; +Drupal.behaviors.viewsUiChangeDefaultWidget.attach = function (context, settings) { + + "use strict"; + + var $ = jQuery; + function change_default_widget(multiple) { + if (multiple) { + $('input.default-radios').hide(); + $('td.any-default-radios-row').parent().hide(); + $('input.default-checkboxes').show(); + } + else { + $('input.default-checkboxes').hide(); + $('td.any-default-radios-row').parent().show(); + $('input.default-radios').show(); + } + } + // Update on widget change. + $('input[name="options[group_info][multiple]"]').change(function() { + change_default_widget($(this).attr("checked")); + }); + // Update the first time the form is rendered. + $('input[name="options[group_info][multiple]"]').trigger('change'); +}; + +/** + * Attaches an expose filter button to a checkbox that triggers its click event. + * + * @param button + * The DOM object representing the button to be checkboxified. + */ +Drupal.viewsUi.Checkboxifier = function (button) { + + "use strict"; + + var $ = jQuery; + this.$button = $(button); + this.$parent = this.$button.parent('div.views-expose, div.views-grouped'); + this.$input = this.$parent.find('input:checkbox, input:radio'); + // Hide the button and its description. + this.$button.hide(); + this.$parent.find('.exposed-description, .grouped-description').hide(); + + this.$input.click($.proxy(this, 'clickHandler')); + +}; + +/** + * When the checkbox is checked or unchecked, simulate a button press. + */ +Drupal.viewsUi.Checkboxifier.prototype.clickHandler = function (e) { + + "use strict"; + + this.$button.mousedown(); + this.$button.submit(); +}; + +/** + * Change the Apply button text based upon the override select state. + */ +Drupal.behaviors.viewsUiOverrideSelect = {}; +Drupal.behaviors.viewsUiOverrideSelect.attach = function (context, settings) { + + "use strict"; + + var $ = jQuery; + $('#edit-override-dropdown', context).once('views-ui-override-button-text', function() { + // Closures! :( + var $submit = $('#edit-submit', context); + var old_value = $submit.val(); + + $submit.once('views-ui-override-button-text') + .bind('mouseup', function() { + $(this).val(old_value); + return true; + }); + + $(this).bind('change', function() { + if ($(this).val() === 'default') { + $submit.val(Drupal.t('Apply (all displays)')); + } + else if ($(this).val() === 'default_revert') { + $submit.val(Drupal.t('Revert to default')); + } + else { + $submit.val(Drupal.t('Apply (this display)')); + } + }) + .trigger('change'); + }); + +}; + +Drupal.viewsUi.resizeModal = function (e, no_shrink) { + + "use strict"; + + var $ = jQuery; + var $modal = $('.views-ui-dialog'); + var $scroll = $('.scroll', $modal); + if ($modal.size() === 0 || $modal.css('display') === 'none') { + return; + } + + var maxWidth = parseInt($(window).width() * .85); // 70% of window + var minWidth = parseInt($(window).width() * .6); // 70% of window + + // Set the modal to the minwidth so that our width calculation of + // children works. + $modal.css('width', minWidth); + var width = minWidth; + + // Don't let the window get more than 80% of the display high. + var maxHeight = parseInt($(window).height() * .8); + var minHeight = 200; + if (no_shrink) { + minHeight = $modal.height(); + } + + if (minHeight > maxHeight) { + minHeight = maxHeight; + } + + var height = 0; + + // Calculate the height of the 'scroll' region. + var scrollHeight = 0; + + scrollHeight += parseInt($scroll.css('padding-top')); + scrollHeight += parseInt($scroll.css('padding-bottom')); + + $scroll.children().each(function() { + var w = $(this).innerWidth(); + if (w > width) { + width = w; + } + scrollHeight += $(this).outerHeight(true); + }); + + // Now, calculate what the difference between the scroll and the modal + // will be. + + var difference = 0; + difference += parseInt($scroll.css('padding-top')); + difference += parseInt($scroll.css('padding-bottom')); + difference += $('.views-override').outerHeight(true); + difference += $('.views-messages').outerHeight(true); + difference += $('#views-ajax-title').outerHeight(true); + difference += $('.views-add-form-selected').outerHeight(true); + difference += $('.form-buttons', $modal).outerHeight(true); + + height = scrollHeight + difference; + + if (height > maxHeight) { + height = maxHeight; + scrollHeight = maxHeight - difference; + } + else if (height < minHeight) { + height = minHeight; + scrollHeight = minHeight - difference; + } + + if (width > maxWidth) { + width = maxWidth; + } + + // Get where we should move content to + var top = ($(window).height() / 2) - (height / 2); + var left = ($(window).width() / 2) - (width / 2); + + $modal.css({ + 'top': top + 'px', + 'left': left + 'px', + 'width': width + 'px', + 'height': height + 'px' + }); + + // Ensure inner popup height matches. + $(Drupal.settings.views.ajax.popup).css('height', height + 'px'); + + $scroll.css({ + 'height': scrollHeight + 'px', + 'max-height': scrollHeight + 'px' + }); + +}; + +jQuery(function() { + + "use strict" + + jQuery(window).bind('resize', Drupal.viewsUi.resizeModal); + jQuery(window).bind('scroll', Drupal.viewsUi.resizeModal); +}); diff --git a/core/modules/views/views_ui/lib/Drupal/views_ui/ViewListController.php b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewListController.php new file mode 100644 index 0000000..b4780cc --- /dev/null +++ b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewListController.php @@ -0,0 +1,160 @@ +isEnabled(); + $b_enabled = $b->isEnabled(); + if ($a_enabled != $b_enabled) { + return $a_enabled < $b_enabled; + } + return $a->id() > $b->id(); + }); + return $entities; + } + + /** + * Overrides Drupal\Core\Entity\EntityListController::buildRow(); + */ + public function buildRow(EntityInterface $view) { + return array( + 'data' => array( + 'view_name' => theme('views_ui_view_info', array('view' => $view)), + 'description' => $view->description, + 'tag' => $view->tag, + 'path' => implode(', ', $view->getPaths()), + 'operations' => array( + 'data' => $this->buildOperations($view), + ), + ), + 'title' => t('Machine name: ') . $view->id(), + 'class' => array($view->isEnabled() ? 'views-ui-list-enabled' : 'views-ui-list-disabled'), + ); + } + + /** + * Overrides Drupal\Core\Entity\EntityListController::buildHeader(); + */ + public function buildHeader() { + return array( + 'view_name' => array( + 'data' => t('View name'), + 'class' => array('views-ui-name'), + ), + 'description' => array( + 'data' => t('Description'), + 'class' => array('views-ui-description'), + ), + 'tag' => array( + 'data' => t('Tag'), + 'class' => array('views-ui-tag'), + ), + 'path' => array( + 'data' => t('Path'), + 'class' => array('views-ui-path'), + ), + 'operations' => array( + 'data' => t('Operations'), + 'class' => array('views-ui-operations'), + ), + ); + } + + /** + * Implements Drupal\Core\Entity\EntityListController::getOperations(); + */ + public function getOperations(EntityInterface $view) { + $uri = $view->uri(); + $path = $uri['path'] . '/view/' . $view->id(); + + $definition['edit'] = array( + 'title' => t('Edit'), + 'href' => "$path/edit", + 'weight' => -5, + ); + if (!$view->isEnabled()) { + $definition['enable'] = array( + 'title' => t('Enable'), + 'ajax' => TRUE, + 'token' => TRUE, + 'href' => "$path/enable", + 'weight' => -10, + ); + } + else { + $definition['disable'] = array( + 'title' => t('Disable'), + 'ajax' => TRUE, + 'token' => TRUE, + 'href' => "$path/disable", + 'weight' => 0, + ); + } + // This property doesn't exist yet. + if (!empty($view->overridden)) { + $definition['revert'] = array( + 'title' => t('Revert'), + 'href' => "$path/revert", + 'weight' => 5, + ); + } + else { + $definition['delete'] = array( + 'title' => t('Delete'), + 'href' => "$path/delete", + 'weight' => 10, + ); + } + return $definition; + } + + /** + * Overrides Drupal\Core\Entity\EntityListController::buildOperations(); + */ + public function buildOperations(EntityInterface $entity) { + $build = parent::buildOperations($entity); + + // Allow operations to specify that they use AJAX. + foreach ($build['#links'] as &$operation) { + if (!empty($operation['ajax'])) { + $operation['attributes']['class'][] = 'use-ajax'; + } + } + + // Use the dropbutton #type. + unset($build['#theme']); + $build['#type'] = 'dropbutton'; + + return $build; + } + + /** + * Overrides Drupal\Core\Entity\EntityListController::render(); + */ + public function render() { + $list = parent::render(); + $list['#attached']['css'] = ViewUI::getAdminCSS(); + $list['#attached']['library'][] = array('system', 'drupal.ajax'); + $list['#attributes']['id'] = 'views-entity-list'; + return $list; + } + +} diff --git a/core/modules/views/views_ui/lib/Drupal/views_ui/ViewUI.php b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewUI.php new file mode 100644 index 0000000..7ac85c9 --- /dev/null +++ b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewUI.php @@ -0,0 +1,1996 @@ +storage; + return $storage->getExecutable(TRUE, TRUE); + } + + /** + * Placeholder function for overriding $display['display_title']. + * + * @todo Remove this function once editing the display title is possible. + */ + public function getDisplayLabel($display_id, $check_changed = TRUE) { + $title = $display_id == 'default' ? t('Master') : $this->storage->display[$display_id]['display_title']; + $title = views_ui_truncate($title, 25); + + if ($check_changed && !empty($this->changed_display[$display_id])) { + $changed = '*'; + $title = $title . $changed; + } + + return $title; + } + + /** + * Helper function to return the used display_id for the edit page + * + * This function handles access to the display. + */ + public function getDisplayEditPage($display_id) { + // Determine the displays available for editing. + if ($tabs = $this->getDisplayTabs($display_id)) { + // If a display isn't specified, use the first one. + if (empty($display_id)) { + foreach ($tabs as $id => $tab) { + if (!isset($tab['#access']) || $tab['#access']) { + $display_id = $id; + break; + } + } + } + // If a display is specified, but we don't have access to it, return + // an access denied page. + if ($display_id && (!isset($tabs[$display_id]) || (isset($tabs[$display_id]['#access']) && !$tabs[$display_id]['#access']))) { + return MENU_ACCESS_DENIED; + } + + return $display_id; + } + elseif ($display_id) { + return MENU_ACCESS_DENIED; + } + else { + $display_id = NULL; + } + + return $display_id; + } + + /** + * Helper function to get the display details section of the edit UI. + * + * @param $display + * + * @return array + * A renderable page build array. + */ + public function getDisplayDetails($display) { + $display_title = $this->getDisplayLabel($display['id'], FALSE); + $build = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('id' => 'edit-display-settings-details'), + ); + + $is_display_deleted = !empty($display['deleted']); + // The master display cannot be cloned. + $is_default = $display['id'] == 'default'; + // @todo: Figure out why getOption doesn't work here. + $is_enabled = $this->displayHandlers[$display['id']]->isEnabled(); + + if ($display['id'] != 'default') { + $build['top']['#theme_wrappers'] = array('container'); + $build['top']['#attributes']['id'] = 'edit-display-settings-top'; + $build['top']['#attributes']['class'] = array('views-ui-display-tab-actions', 'views-ui-display-tab-bucket', 'clearfix'); + + // The Delete, Duplicate and Undo Delete buttons. + $build['top']['actions'] = array( + '#theme_wrappers' => array('dropbutton_wrapper'), + ); + + // Because some of the 'links' are actually submit buttons, we have to + // manually wrap each item in
  • and the whole list in
      . + $build['top']['actions']['prefix']['#markup'] = '
        '; + + if (!$is_display_deleted) { + if (!$is_enabled) { + $build['top']['actions']['enable'] = array( + '#type' => 'submit', + '#value' => t('enable @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array(array($this, 'submitDisplayEnable'), array($this, 'submitDelayDestination')), + '#prefix' => '
      • ', + "#suffix" => '
      • ', + ); + } + // Add a link to view the page. + elseif ($this->displayHandlers[$display['id']]->hasPath()) { + $path = $this->displayHandlers[$display['id']]->getPath(); + if (strpos($path, '%') === FALSE) { + $build['top']['actions']['path'] = array( + '#type' => 'link', + '#title' => t('view @display', array('@display' => $display['display_title'])), + '#options' => array('alt' => array(t("Go to the real page for this display"))), + '#href' => $path, + '#prefix' => '
      • ', + "#suffix" => '
      • ', + ); + } + } + if (!$is_default) { + $build['top']['actions']['duplicate'] = array( + '#type' => 'submit', + '#value' => t('clone @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array(array($this, 'submitDisplayDuplicate'), array($this, 'submitDelayDestination')), + '#prefix' => '
      • ', + "#suffix" => '
      • ', + ); + } + // Always allow a display to be deleted. + $build['top']['actions']['delete'] = array( + '#type' => 'submit', + '#value' => t('delete @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array(array($this, 'submitDisplayDelete'), array($this, 'submitDelayDestination')), + '#prefix' => '
      • ', + "#suffix" => '
      • ', + ); + if ($is_enabled) { + $build['top']['actions']['disable'] = array( + '#type' => 'submit', + '#value' => t('disable @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array(array($this, 'submitDisplayDisable'), array($this, 'submitDelayDestination')), + '#prefix' => '
      • ', + "#suffix" => '
      • ', + ); + } + } + else { + $build['top']['actions']['undo_delete'] = array( + '#type' => 'submit', + '#value' => t('undo delete of @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array(array($this, 'submitDisplayUndoDelete'), array($this, 'submitDelayDestination')), + '#prefix' => '
      • ', + "#suffix" => '
      • ', + ); + } + $build['top']['actions']['suffix']['#markup'] = '
      '; + + // The area above the three columns. + $build['top']['display_title'] = array( + '#theme' => 'views_ui_display_tab_setting', + '#description' => t('Display name'), + '#link' => $this->displayHandlers[$display['id']]->optionLink(check_plain($display_title), 'display_title'), + ); + } + + $build['columns'] = array(); + $build['columns']['#theme_wrappers'] = array('container'); + $build['columns']['#attributes'] = array('id' => 'edit-display-settings-main', 'class' => array('clearfix', 'views-display-columns')); + + $build['columns']['first']['#theme_wrappers'] = array('container'); + $build['columns']['first']['#attributes'] = array('class' => array('views-display-column', 'first')); + + $build['columns']['second']['#theme_wrappers'] = array('container'); + $build['columns']['second']['#attributes'] = array('class' => array('views-display-column', 'second')); + + $build['columns']['second']['settings'] = array(); + $build['columns']['second']['header'] = array(); + $build['columns']['second']['footer'] = array(); + $build['columns']['second']['pager'] = array(); + + // The third column buckets are wrapped in a fieldset. + $build['columns']['third'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#theme_wrappers' => array('fieldset', 'container'), + '#attributes' => array( + 'class' => array( + 'views-display-column', + 'third', + ), + ), + ); + + // Collapse the fieldset by default. + if (config('views.settings')->get('ui.show.advanced_column')) { + $build['columns']['third']['#collapsed'] = FALSE; + } + + // Each option (e.g. title, access, display as grid/table/list) fits into one + // of several "buckets," or boxes (Format, Fields, Sort, and so on). + $buckets = array(); + + // Fetch options from the display plugin, with a list of buckets they go into. + $options = array(); + $this->displayHandlers[$display['id']]->optionsSummary($buckets, $options); + + // Place each option into its bucket. + foreach ($options as $id => $option) { + // Each option self-identifies as belonging in a particular bucket. + $buckets[$option['category']]['build'][$id] = $this->buildOptionForm($id, $option, $display); + } + + // Place each bucket into the proper column. + foreach ($buckets as $id => $bucket) { + // Let buckets identify themselves as belonging in a column. + if (isset($bucket['column']) && isset($build['columns'][$bucket['column']])) { + $column = $bucket['column']; + } + // If a bucket doesn't pick one of our predefined columns to belong to, put + // it in the last one. + else { + $column = 'third'; + } + if (isset($bucket['build']) && is_array($bucket['build'])) { + $build['columns'][$column][$id] = $bucket['build']; + $build['columns'][$column][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket'; + $build['columns'][$column][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : ''; + $build['columns'][$column][$id]['#name'] = !empty($bucket['title']) ? $bucket['title'] : $id; + } + } + + $build['columns']['first']['fields'] = $this->getFormBucket('field', $display); + $build['columns']['first']['filters'] = $this->getFormBucket('filter', $display); + $build['columns']['first']['sorts'] = $this->getFormBucket('sort', $display); + $build['columns']['second']['header'] = $this->getFormBucket('header', $display); + $build['columns']['second']['footer'] = $this->getFormBucket('footer', $display); + $build['columns']['third']['arguments'] = $this->getFormBucket('argument', $display); + $build['columns']['third']['relationships'] = $this->getFormBucket('relationship', $display); + $build['columns']['third']['empty'] = $this->getFormBucket('empty', $display); + + return $build; + } + + /** + * Build a renderable array representing one option on the edit form. + * + * This function might be more logical as a method on an object, if a suitable + * object emerges out of refactoring. + */ + public function buildOptionForm($id, $option, $display) { + $option_build = array(); + $option_build['#theme'] = 'views_ui_display_tab_setting'; + + $option_build['#description'] = $option['title']; + + $option_build['#link'] = $this->displayHandlers[$display['id']]->optionLink($option['value'], $id, '', empty($option['desc']) ? '' : $option['desc']); + + $option_build['#links'] = array(); + if (!empty($option['links']) && is_array($option['links'])) { + foreach ($option['links'] as $link_id => $link_value) { + $option_build['#settings_links'][] = $this->displayHandlers[$display['id']]->optionLink($option['setting'], $link_id, 'views-button-configure', $link_value); + } + } + + if (!empty($this->displayHandlers[$display['id']]->options['defaults'][$id])) { + $display_id = 'default'; + $option_build['#defaulted'] = TRUE; + } + else { + $display_id = $display['id']; + if (!$this->displayHandlers[$display['id']]->isDefaultDisplay()) { + if ($this->displayHandlers[$display['id']]->defaultableSections($id)) { + $option_build['#overridden'] = TRUE; + } + } + } + $option_build['#attributes']['class'][] = drupal_clean_css_identifier($display_id . '-' . $id); + return $option_build; + } + + /** + * Render the top of the display so it can be updated during ajax operations. + */ + public function renderDisplayTop($display_id) { + $element['#theme_wrappers'] = array('views_ui_container'); + $element['#attributes']['class'] = array('views-display-top', 'clearfix'); + $element['#attributes']['id'] = array('views-display-top'); + + // Extra actions for the display + $element['extra_actions'] = array( + '#type' => 'dropbutton', + '#attributes' => array( + 'id' => 'views-display-extra-actions', + ), + '#links' => array( + 'edit-details' => array( + 'title' => t('edit view name/description'), + 'href' => "admin/structure/views/nojs/edit-details/{$this->storage->name}", + 'attributes' => array('class' => array('views-ajax-link')), + ), + 'analyze' => array( + 'title' => t('analyze view'), + 'href' => "admin/structure/views/nojs/analyze/{$this->storage->name}/$display_id", + 'attributes' => array('class' => array('views-ajax-link')), + ), + 'clone' => array( + 'title' => t('clone view'), + 'href' => "admin/structure/views/view/{$this->storage->name}/clone", + ), + 'reorder' => array( + 'title' => t('reorder displays'), + 'href' => "admin/structure/views/nojs/reorder-displays/{$this->storage->name}/$display_id", + 'attributes' => array('class' => array('views-ajax-link')), + ), + ), + ); + + // Let other modules add additional links here. + drupal_alter('views_ui_display_top_links', $element['extra_actions']['#links'], $this, $display_id); + + if (isset($this->type) && $this->type != t('Default')) { + if ($this->type == t('Overridden')) { + $element['extra_actions']['#links']['revert'] = array( + 'title' => t('revert view'), + 'href' => "admin/structure/views/view/{$this->storage->name}/revert", + 'query' => array('destination' => "admin/structure/views/view/{$this->storage->name}"), + ); + } + else { + $element['extra_actions']['#links']['delete'] = array( + 'title' => t('delete view'), + 'href' => "admin/structure/views/view/{$this->storage->name}/delete", + ); + } + } + + // Determine the displays available for editing. + if ($tabs = $this->getDisplayTabs($display_id)) { + if ($display_id) { + $tabs[$display_id]['#active'] = TRUE; + } + $tabs['#prefix'] = '

      ' . t('Secondary tabs') . '

        '; + $tabs['#suffix'] = '
      '; + $element['tabs'] = $tabs; + } + + // Buttons for adding a new display. + foreach (views_fetch_plugin_names('display', NULL, array($this->storage->base_table)) as $type => $label) { + $element['add_display'][$type] = array( + '#type' => 'submit', + '#value' => t('Add !display', array('!display' => $label)), + '#limit_validation_errors' => array(), + '#submit' => array(array($this, 'submitDisplayAdd'), array($this, 'submitDelayDestination')), + '#attributes' => array('class' => array('add-display')), + // Allow JavaScript to remove the 'Add ' prefix from the button label when + // placing the button in a "Add" dropdown menu. + '#process' => array_merge(array('views_ui_form_button_was_clicked'), element_info_property('submit', '#process', array())), + '#values' => array(t('Add !display', array('!display' => $label)), $label), + ); + } + + return $element; + } + + public static function getDefaultAJAXMessage() { + return '
      ' . t("Click on an item to edit that item's details.") . '
      '; + } + + /** + * Adds tabs for navigating across Displays when editing a View. + * + * This function can be called from hook_menu_local_tasks_alter() to implement + * these tabs as secondary local tasks, or it can be called from elsewhere if + * having them as secondary local tasks isn't desired. The caller is responsible + * for setting the active tab's #active property to TRUE. + * + * @param $display_id + * The display_id which is edited on the current request. + */ + public function getDisplayTabs($display_id = NULL) { + $tabs = array(); + + // Create a tab for each display. + uasort($this->storage->display, array('static', 'sortPosition')); + foreach ($this->storage->display as $id => $display) { + $tabs[$id] = array( + '#theme' => 'menu_local_task', + '#link' => array( + 'title' => $this->getDisplayLabel($id), + 'href' => 'admin/structure/views/view/' . $this->storage->name . '/edit/' . $id, + 'localized_options' => array(), + ), + ); + if (!empty($display['deleted'])) { + $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'views-display-deleted-link'; + } + if (isset($display['display_options']['enabled']) && !$display['display_options']['enabled']) { + $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'views-display-disabled-link'; + } + } + + // If the default display isn't supposed to be shown, don't display its tab, unless it's the only display. + if ((!$this->isDefaultDisplayShown() && $display_id != 'default') && count($tabs) > 1) { + $tabs['default']['#access'] = FALSE; + } + + // Mark the display tab as red to show validation errors. + $this->validate(); + foreach ($this->storage->display as $id => $display) { + if (!empty($this->display_errors[$id])) { + // Always show the tab. + $tabs[$id]['#access'] = TRUE; + // Add a class to mark the error and a title to make a hover tip. + $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'error'; + $tabs[$id]['#link']['localized_options']['attributes']['title'] = t('This display has one or more validation errors; please review it.'); + } + } + + return $tabs; + } + + /** + * Returns a renderable array representing the edit page for one display. + */ + public function getDisplayTab($display_id) { + $build = array(); + $display = $this->displayHandlers[$display_id]; + // If the plugin doesn't exist, display an error message instead of an edit + // page. + if (empty($display)) { + $title = isset($display['display_title']) ? $display['display_title'] : t('Invalid'); + // @TODO: Improved UX for the case where a plugin is missing. + $build['#markup'] = t("Error: Display @display refers to a plugin named '@plugin', but that plugin is not available.", array('@display' => $display['id'], '@plugin' => $display['display_plugin'])); + } + // Build the content of the edit page. + else { + $build['details'] = $this->getDisplayDetails($display->display); + } + // In AJAX context, ViewUI::rebuildCurrentTab() returns this outside of form + // context, so hook_form_views_ui_edit_form_alter() is insufficient. + drupal_alter('views_ui_display_tab', $build, $this, $display_id); + return $build; + } + + /** + * Controls whether or not the default display should have its own tab on edit. + */ + public function isDefaultDisplayShown() { + // Always show the default display for advanced users who prefer that mode. + $advanced_mode = config('views.settings')->get('ui.show.master_display'); + // For other users, show the default display only if there are no others, and + // hide it if there's at least one "real" display. + $additional_displays = (count($this->displayHandlers) == 1); + + return $advanced_mode || $additional_displays; + } + + /** + * Basic submit handler applicable to all 'standard' forms. + * + * This submit handler determines whether the user wants the submitted changes + * to apply to the default display or to the current display, and dispatches + * control appropriately. + */ + public function standardSubmit($form, &$form_state) { + // Determine whether the values the user entered are intended to apply to + // the current display or the default display. + + list($was_defaulted, $is_defaulted, $revert) = $this->getOverrideValues($form, $form_state); + + // Based on the user's choice in the display dropdown, determine which display + // these changes apply to. + if ($revert) { + // If it's revert just change the override and return. + $display = &$this->displayHandlers[$form_state['display_id']]; + $display->optionsOverride($form, $form_state); + + // Don't execute the normal submit handling but still store the changed view into cache. + views_ui_cache_set($this); + return; + } + elseif ($was_defaulted === $is_defaulted) { + // We're not changing which display these form values apply to. + // Run the regular submit handler for this form. + } + elseif ($was_defaulted && !$is_defaulted) { + // We were using the default display's values, but we're now overriding + // the default display and saving values specific to this display. + $display = &$this->displayHandlers[$form_state['display_id']]; + // optionsOverride toggles the override of this section. + $display->optionsOverride($form, $form_state); + $display->submitOptionsForm($form, $form_state); + } + elseif (!$was_defaulted && $is_defaulted) { + // We used to have an override for this display, but the user now wants + // to go back to the default display. + // Overwrite the default display with the current form values, and make + // the current display use the new default values. + $display = &$this->displayHandlers[$form_state['display_id']]; + // optionsOverride toggles the override of this section. + $display->optionsOverride($form, $form_state); + $display->submitOptionsForm($form, $form_state); + } + + $submit_handler = $form['#form_id'] . '_submit'; + if (function_exists($submit_handler)) { + $submit_handler($form, $form_state); + } + } + + /** + * Submit handler for cancel button + */ + public function standardCancel($form, &$form_state) { + if (!empty($this->changed) && isset($this->form_cache)) { + unset($this->form_cache); + views_ui_cache_set($this); + } + + $form_state['redirect'] = 'admin/structure/views/view/' . $this->storage->name . '/edit'; + } + + /** + * Provide a standard set of Apply/Cancel/OK buttons for the forms. Also provide + * a hidden op operator because the forms plugin doesn't seem to properly + * provide which button was clicked. + * + * TODO: Is the hidden op operator still here somewhere, or is that part of the + * docblock outdated? + */ + public function getStandardButtons(&$form, &$form_state, $form_id, $name = NULL, $third = NULL, $submit = NULL) { + $form['buttons'] = array( + '#prefix' => '
      ', + '#suffix' => '
      ', + ); + + if (empty($name)) { + $name = t('Apply'); + if (!empty($this->stack) && count($this->stack) > 1) { + $name = t('Apply and continue'); + } + $names = array(t('Apply'), t('Apply and continue')); + } + + // Forms that are purely informational set an ok_button flag, so we know not + // to create an "Apply" button for them. + if (empty($form_state['ok_button'])) { + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => $name, + // The regular submit handler ($form_id . '_submit') does not apply if + // we're updating the default display. It does apply if we're updating + // the current display. Since we have no way of knowing at this point + // which display the user wants to update, views_ui_standard_submit will + // take care of running the regular submit handler as appropriate. + '#submit' => array(array($this, 'standardSubmit')), + ); + // Form API button click detection requires the button's #value to be the + // same between the form build of the initial page request, and the initial + // form build of the request processing the form submission. Ideally, the + // button's #value shouldn't change until the form rebuild step. However, + // views_ui_ajax_form() implements a different multistep form workflow than + // the Form API does, and adjusts $view->stack prior to form processing, so + // we compensate by extending button click detection code to support any of + // the possible button labels. + if (isset($names)) { + $form['buttons']['submit']['#values'] = $names; + $form['buttons']['submit']['#process'] = array_merge(array('views_ui_form_button_was_clicked'), element_info_property($form['buttons']['submit']['#type'], '#process', array())); + } + // If a validation handler exists for the form, assign it to this button. + if (function_exists($form_id . '_validate')) { + $form['buttons']['submit']['#validate'][] = $form_id . '_validate'; + } + } + + // Create a "Cancel" button. For purely informational forms, label it "OK". + $cancel_submit = function_exists($form_id . '_cancel') ? $form_id . '_cancel' : array($this, 'standardCancel'); + $form['buttons']['cancel'] = array( + '#type' => 'submit', + '#value' => empty($form_state['ok_button']) ? t('Cancel') : t('Ok'), + '#submit' => array($cancel_submit), + '#validate' => array(), + ); + + // Some forms specify a third button, with a name and submit handler. + if ($third) { + if (empty($submit)) { + $submit = 'third'; + } + $third_submit = function_exists($form_id . '_' . $submit) ? $form_id . '_' . $submit : array($this, 'standardCancel'); + + $form['buttons'][$submit] = array( + '#type' => 'submit', + '#value' => $third, + '#validate' => array(), + '#submit' => array($third_submit), + ); + } + + // Compatibility, to be removed later: // TODO: When is "later"? + // We used to set these items on the form, but now we want them on the $form_state: + if (isset($form['#title'])) { + $form_state['title'] = $form['#title']; + } + if (isset($form['#url'])) { + $form_state['url'] = $form['#url']; + } + if (isset($form['#section'])) { + $form_state['#section'] = $form['#section']; + } + // Finally, we never want these cached -- our object cache does that for us. + $form['#no_cache'] = TRUE; + + // If this isn't an ajaxy form, then we want to set the title. + if (!empty($form['#title'])) { + drupal_set_title($form['#title']); + } + } + + /** + * Creates an array of Views admin CSS for adding or attaching. + * + * This returns an array of arrays. Each array represents a single + * file. The array format is: + * - file: The fully qualified name of the file to send to drupal_add_css + * - options: An array of options to pass to drupal_add_css. + */ + public static function getAdminCSS() { + $module_path = drupal_get_path('module', 'views_ui'); + $list = array(); + $list[$module_path . '/css/views-admin.css'] = array(); + $list[$module_path . '/css/views-admin.theme.css'] = array(); + + // Add in any theme specific CSS files we have + $themes = list_themes(); + $theme_key = $GLOBALS['theme']; + while ($theme_key) { + // Try to find the admin css file for non-core themes. + if (!in_array($theme_key, array('seven', 'bartik'))) { + $theme_path = drupal_get_path('theme', $theme_key); + // First search in the css directory, then in the root folder of the theme. + if (file_exists($theme_path . "/css/views-admin.$theme_key.css")) { + $list[$theme_path . "/css/views-admin.$theme_key.css"] = array( + 'group' => CSS_THEME, + ); + } + elseif (file_exists($theme_path . "/views-admin.$theme_key.css")) { + $list[$theme_path . "/views-admin.$theme_key.css"] = array( + 'group' => CSS_THEME, + ); + } + } + else { + $list[$module_path . "/css/views-admin.$theme_key.css"] = array( + 'group' => CSS_THEME, + ); + } + $theme_key = isset($themes[$theme_key]->base_theme) ? $themes[$theme_key]->base_theme : ''; + } + if (module_exists('contextual')) { + $list[$module_path . '/css/views-admin.contextual.css'] = array(); + } + + return $list; + } + + /** + * Submit handler to add a display to a view. + */ + public function submitDisplayAdd($form, &$form_state) { + // Create the new display. + $parents = $form_state['triggering_element']['#parents']; + $display_type = array_pop($parents); + $display_id = $this->storage->addDisplay($display_type); + // A new display got added so the asterisks symbol should appear on the new + // display. + $this->current_display = $display_id; + views_ui_cache_set($this); + + // Redirect to the new display's edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $this->storage->name . '/edit/' . $display_id; + } + + /** + * Submit handler to duplicate a display for a view. + */ + public function submitDisplayDuplicate($form, &$form_state) { + $display_id = $form_state['display_id']; + + // Create the new display. + $display = $this->storage->display[$display_id]; + $new_display_id = $this->storage->addDisplay($display['display_plugin']); + $this->storage->display[$new_display_id] = $display; + $this->storage->display[$new_display_id]['id'] = $new_display_id; + + // By setting the current display the changed marker will appear on the new + // display. + $this->current_display = $new_display_id; + views_ui_cache_set($this); + + // Redirect to the new display's edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $this->storage->name . '/edit/' . $new_display_id; + } + + /** + * Submit handler to delete a display from a view. + */ + public function submitDisplayDelete($form, &$form_state) { + $display_id = $form_state['display_id']; + + // Mark the display for deletion. + $this->storage->display[$display_id]['deleted'] = TRUE; + views_ui_cache_set($this); + + // Redirect to the top-level edit page. The first remaining display will + // become the active display. + $form_state['redirect'] = 'admin/structure/views/view/' . $this->storage->name; + } + + /** + * Submit handler for form buttons that do not complete a form workflow. + * + * The Edit View form is a multistep form workflow, but with state managed by + * the TempStore rather than $form_state['rebuild']. Without this + * submit handler, buttons that add or remove displays would redirect to the + * destination parameter (e.g., when the Edit View form is linked to from a + * contextual link). This handler can be added to buttons whose form submission + * should not yet redirect to the destination. + */ + public function submitDelayDestination($form, &$form_state) { + $query = drupal_container()->get('request')->query; + // @todo: Revisit this when http://drupal.org/node/1668866 is in. + $destination = $query->get('destination'); + if (isset($destination) && $form_state['redirect'] !== FALSE) { + if (!isset($form_state['redirect'])) { + $form_state['redirect'] = current_path(); + } + if (is_string($form_state['redirect'])) { + $form_state['redirect'] = array($form_state['redirect']); + } + $options = isset($form_state['redirect'][1]) ? $form_state['redirect'][1] : array(); + if (!isset($options['query']['destination'])) { + $options['query']['destination'] = $destination; + } + $form_state['redirect'][1] = $options; + $query->remove('destination'); + } + } + + /** + * Submit handler to enable a disabled display. + */ + public function submitDisplayEnable($form, &$form_state) { + $id = $form_state['display_id']; + // setOption doesn't work because this would might affect upper displays + $this->displayHandlers[$id]->setOption('enabled', TRUE); + + // Store in cache + views_ui_cache_set($this); + + // Redirect to the top-level edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $this->storage->name . '/edit/' . $id; + } + + /** + * Submit handler to disable display. + */ + public function submitDisplayDisable($form, &$form_state) { + $id = $form_state['display_id']; + $this->displayHandlers[$id]->setOption('enabled', FALSE); + + // Store in cache + views_ui_cache_set($this); + + // Redirect to the top-level edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $this->storage->name . '/edit/' . $id; + } + + /** + * Submit handler to add a restore a removed display to a view. + */ + public function submitDisplayUndoDelete($form, &$form_state) { + // Create the new display + $id = $form_state['display_id']; + $this->storage->display[$id]['deleted'] = FALSE; + + // Store in cache + views_ui_cache_set($this); + + // Redirect to the top-level edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $this->storage->name . '/edit/' . $id; + } + + /** + * Add information about a section to a display. + */ + public function getFormBucket($type, $display) { + $build = array( + '#theme_wrappers' => array('views_ui_display_tab_bucket'), + ); + $types = static::viewsHandlerTypes(); + + $build['#overridden'] = FALSE; + $build['#defaulted'] = FALSE; + + $build['#name'] = $build['#title'] = $types[$type]['title']; + + // Different types now have different rearrange forms, so we use this switch + // to get the right one. + switch ($type) { + case 'filter': + $rearrange_url = "admin/structure/views/nojs/rearrange-$type/{$this->storage->name}/{$display['id']}/$type"; + $rearrange_text = t('And/Or, Rearrange'); + // TODO: Add another class to have another symbol for filter rearrange. + $class = 'icon compact rearrange'; + break; + case 'field': + // Fetch the style plugin info so we know whether to list fields or not. + $style_plugin = $this->displayHandlers[$display['id']]->getPlugin('style'); + $uses_fields = $style_plugin && $style_plugin->usesFields(); + if (!$uses_fields) { + $build['fields'][] = array( + '#markup' => t('The selected style or row format does not utilize fields.'), + '#theme_wrappers' => array('views_ui_container'), + '#attributes' => array('class' => array('views-display-setting')), + ); + return $build; + } + + default: + $rearrange_url = "admin/structure/views/nojs/rearrange/{$this->storage->name}/{$display['id']}/$type"; + $rearrange_text = t('Rearrange'); + $class = 'icon compact rearrange'; + } + + // Create an array of actions to pass to theme_links + $actions = array(); + $count_handlers = count($this->displayHandlers[$display['id']]->getHandlers($type)); + $actions['add'] = array( + 'title' => t('Add'), + 'href' => "admin/structure/views/nojs/add-item/{$this->storage->name}/{$display['id']}/$type", + 'attributes' => array('class' => array('icon compact add', 'views-ajax-link'), 'title' => t('Add'), 'id' => 'views-add-' . $type), + 'html' => TRUE, + ); + if ($count_handlers > 0) { + $actions['rearrange'] = array( + 'title' => $rearrange_text, + 'href' => $rearrange_url, + 'attributes' => array('class' => array($class, 'views-ajax-link'), 'title' => t('Rearrange'), 'id' => 'views-rearrange-' . $type), + 'html' => TRUE, + ); + } + + // Render the array of links + $build['#actions'] = array( + '#type' => 'dropbutton', + '#links' => $actions, + '#attributes' => array( + 'class' => array('views-ui-settings-bucket-operations'), + ), + ); + + if (!$this->displayHandlers[$display['id']]->isDefaultDisplay()) { + if (!$this->displayHandlers[$display['id']]->isDefaulted($types[$type]['plural'])) { + $build['#overridden'] = TRUE; + } + else { + $build['#defaulted'] = TRUE; + } + } + + // If there's an options form for the bucket, link to it. + if (!empty($types[$type]['options'])) { + $build['#title'] = l($build['#title'], "admin/structure/views/nojs/config-type/{$this->storage->name}/{$display['id']}/$type", array('attributes' => array('class' => array('views-ajax-link'), 'id' => 'views-title-' . $type))); + } + + static $relationships = NULL; + if (!isset($relationships)) { + // Get relationship labels + $relationships = array(); + foreach ($this->displayHandlers[$display['id']]->getHandlers('relationship') as $id => $handler) { + $relationships[$id] = $handler->label(); + } + } + + // Filters can now be grouped so we do a little bit extra: + $groups = array(); + $grouping = FALSE; + if ($type == 'filter') { + $group_info = $this->display_handler->getOption('filter_groups'); + // If there is only one group but it is using the "OR" filter, we still + // treat it as a group for display purposes, since we want to display the + // "OR" label next to items within the group. + if (!empty($group_info['groups']) && (count($group_info['groups']) > 1 || current($group_info['groups']) == 'OR')) { + $grouping = TRUE; + $groups = array(0 => array()); + } + } + + $build['fields'] = array(); + + foreach ($this->displayHandlers[$display['id']]->getOption($types[$type]['plural']) as $id => $field) { + // Build the option link for this handler ("Node: ID = article"). + $build['fields'][$id] = array(); + $build['fields'][$id]['#theme'] = 'views_ui_display_tab_setting'; + + $handler = $this->displayHandlers[$display['id']]->getHandler($type, $id); + if (empty($handler)) { + $build['fields'][$id]['#class'][] = 'broken'; + $field_name = t('Broken/missing handler: @table > @field', array('@table' => $field['table'], '@field' => $field['field'])); + $build['fields'][$id]['#link'] = l($field_name, "admin/structure/views/nojs/config-item/{$this->storage->name}/{$display['id']}/$type/$id", array('attributes' => array('class' => array('views-ajax-link')), 'html' => TRUE)); + continue; + } + + $field_name = check_plain($handler->adminLabel(TRUE)); + if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) { + $field_name = '(' . $relationships[$field['relationship']] . ') ' . $field_name; + } + + $description = filter_xss_admin($handler->adminSummary()); + $link_text = $field_name . (empty($description) ? '' : " ($description)"); + $link_attributes = array('class' => array('views-ajax-link')); + if (!empty($field['exclude'])) { + $link_attributes['class'][] = 'views-field-excluded'; + } + $build['fields'][$id]['#link'] = l($link_text, "admin/structure/views/nojs/config-item/{$this->storage->name}/{$display['id']}/$type/$id", array('attributes' => $link_attributes, 'html' => TRUE)); + $build['fields'][$id]['#class'][] = drupal_clean_css_identifier($display['id']. '-' . $type . '-' . $id); + + if ($this->displayHandlers[$display['id']]->useGroupBy() && $handler->usesGroupBy()) { + $build['fields'][$id]['#settings_links'][] = l('' . t('Aggregation settings') . '', "admin/structure/views/nojs/config-item-group/{$this->storage->name}/{$display['id']}/$type/$id", array('attributes' => array('class' => 'views-button-configure views-ajax-link', 'title' => t('Aggregation settings')), 'html' => TRUE)); + } + + if ($handler->hasExtraOptions()) { + $build['fields'][$id]['#settings_links'][] = l('' . t('Settings') . '', "admin/structure/views/nojs/config-item-extra/{$this->storage->name}/{$display['id']}/$type/$id", array('attributes' => array('class' => array('views-button-configure', 'views-ajax-link'), 'title' => t('Settings')), 'html' => TRUE)); + } + + if ($grouping) { + $gid = $handler->options['group']; + + // Show in default group if the group does not exist. + if (empty($group_info['groups'][$gid])) { + $gid = 0; + } + $groups[$gid][] = $id; + } + } + + // If using grouping, re-order fields so that they show up properly in the list. + if ($type == 'filter' && $grouping) { + $store = $build['fields']; + $build['fields'] = array(); + foreach ($groups as $gid => $contents) { + // Display an operator between each group. + if (!empty($build['fields'])) { + $build['fields'][] = array( + '#theme' => 'views_ui_display_tab_setting', + '#class' => array('views-group-text'), + '#link' => ($group_info['operator'] == 'OR' ? t('OR') : t('AND')), + ); + } + // Display an operator between each pair of filters within the group. + $keys = array_keys($contents); + $last = end($keys); + foreach ($contents as $key => $pid) { + if ($key != $last) { + $store[$pid]['#link'] .= '  ' . ($group_info['groups'][$gid] == 'OR' ? t('OR') : t('AND')); + } + $build['fields'][$pid] = $store[$pid]; + } + } + } + + return $build; + } + + /** + * Return the was_defaulted, is_defaulted and revert state of a form. + */ + public function getOverrideValues($form, $form_state) { + // Make sure the dropdown exists in the first place. + if (isset($form_state['values']['override']['dropdown'])) { + // #default_value is used to determine whether it was the default value or not. + // So the available options are: $display, 'default' and 'default_revert', not 'defaults'. + $was_defaulted = ($form['override']['dropdown']['#default_value'] === 'defaults'); + $is_defaulted = ($form_state['values']['override']['dropdown'] === 'default'); + $revert = ($form_state['values']['override']['dropdown'] === 'default_revert'); + + if ($was_defaulted !== $is_defaulted && isset($form['#section'])) { + // We're changing which display these values apply to. + // Update the #section so it knows what to mark changed. + $form['#section'] = str_replace('default-', $form_state['display_id'] . '-', $form['#section']); + } + } + else { + // The user didn't get the dropdown for overriding the default display. + $was_defaulted = FALSE; + $is_defaulted = FALSE; + $revert = FALSE; + } + + return array($was_defaulted, $is_defaulted, $revert); + } + + /** + * Regenerate the current tab for AJAX updates. + */ + public function rebuildCurrentTab(&$output, $display_id) { + if (!$this->setDisplay('default')) { + return; + } + + // Regenerate the main display area. + $build = $this->getDisplayTab($display_id); + static::addMicroweights($build); + $output[] = ajax_command_html('#views-tab-' . $display_id, drupal_render($build)); + + // Regenerate the top area so changes to display names and order will appear. + $build = $this->renderDisplayTop($display_id); + static::addMicroweights($build); + $output[] = ajax_command_replace('#views-display-top', drupal_render($build)); + } + + /** + * Submit handler to break_lock a view. + */ + public function submitBreakLock(&$form, &$form_state) { + drupal_container()->get('user.tempstore')->get('views')->delete($this->storage->name); + $form_state['redirect'] = 'admin/structure/views/view/' . $this->storage->name . '/edit'; + drupal_set_message(t('The lock has been broken and you may now edit this view.')); + } + + public static function buildAddForm($form, &$form_state) { + $form['#attached']['css'] = static::getAdminCSS(); + $form['#attached']['js'][] = drupal_get_path('module', 'views_ui') . '/js/views-admin.js'; + $form['#attributes']['class'] = array('views-admin'); + + $form['human_name'] = array( + '#type' => 'textfield', + '#title' => t('View name'), + '#required' => TRUE, + '#size' => 32, + '#default_value' => '', + '#maxlength' => 255, + ); + $form['name'] = array( + '#type' => 'machine_name', + '#maxlength' => 128, + '#machine_name' => array( + 'exists' => 'views_get_view', + 'source' => array('human_name'), + ), + '#description' => t('A unique machine-readable name for this View. It must only contain lowercase letters, numbers, and underscores.'), + ); + + $form['description_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Description'), + ); + $form['description'] = array( + '#type' => 'textfield', + '#title' => t('Provide description'), + '#title_display' => 'invisible', + '#size' => 64, + '#default_value' => '', + '#states' => array( + 'visible' => array( + ':input[name="description_enable"]' => array('checked' => TRUE), + ), + ), + ); + + // Create a wrapper for the entire dynamic portion of the form. Everything + // that can be updated by AJAX goes somewhere inside here. For example, this + // is needed by "Show" dropdown (below); it changes the base table of the + // view and therefore potentially requires all options on the form to be + // dynamically updated. + $form['displays'] = array(); + + // Create the part of the form that allows the user to select the basic + // properties of what the view will display. + $form['displays']['show'] = array( + '#type' => 'fieldset', + '#tree' => TRUE, + '#attributes' => array('class' => array('container-inline')), + ); + + // Create the "Show" dropdown, which allows the base table of the view to be + // selected. + $wizard_plugins = views_ui_get_wizards(); + $options = array(); + foreach ($wizard_plugins as $key => $wizard) { + $options[$key] = $wizard['title']; + } + $form['displays']['show']['wizard_key'] = array( + '#type' => 'select', + '#title' => t('Show'), + '#options' => $options, + ); + $show_form = &$form['displays']['show']; + $default_value = module_exists('node') ? 'node' : 'users'; + $show_form['wizard_key']['#default_value'] = views_ui_get_selected($form_state, array('show', 'wizard_key'), $default_value, $show_form['wizard_key']); + // Changing this dropdown updates the entire content of $form['displays'] via + // AJAX. + views_ui_add_ajax_trigger($show_form, 'wizard_key', array('displays')); + + // Build the rest of the form based on the currently selected wizard plugin. + $wizard_key = $show_form['wizard_key']['#default_value']; + $wizard_instance = views_get_plugin('wizard', $wizard_key); + $form = $wizard_instance->build_form($form, $form_state); + + $form['save'] = array( + '#type' => 'submit', + '#value' => t('Save & exit'), + '#validate' => array('views_ui_wizard_form_validate'), + '#submit' => array('views_ui_add_form_save_submit'), + ); + $form['continue'] = array( + '#type' => 'submit', + '#value' => t('Continue & edit'), + '#validate' => array('views_ui_wizard_form_validate'), + '#submit' => array('views_ui_add_form_store_edit_submit'), + '#process' => array_merge(array(array(get_called_class(), 'processDefaultButton')), element_info_property('submit', '#process', array())), + ); + $form['cancel'] = array( + '#type' => 'submit', + '#value' => t('Cancel'), + '#submit' => array('views_ui_add_form_cancel_submit'), + '#limit_validation_errors' => array(), + ); + + return $form; + } + + /** + * Form builder callback for editing a View. + * + * @todo Remove as many #prefix/#suffix lines as possible. Use #theme_wrappers + * instead. + * + * @todo Rename to views_ui_edit_view_form(). See that function for the "old" + * version. + * + * @see views_ui_ajax_get_form() + */ + public function buildEditForm($form, &$form_state, $display_id = NULL) { + // Do not allow the form to be cached, because $form_state['view'] can become + // stale between page requests. + // See views_ui_ajax_get_form() for how this affects #ajax. + // @todo To remove this and allow the form to be cacheable: + // - Change $form_state['view'] to $form_state['temporary']['view']. + // - Add a #process function to initialize $form_state['temporary']['view'] + // on cached form submissions. + // - Use form_load_include(). + $form_state['no_cache'] = TRUE; + + if ($display_id) { + if (!$this->setDisplay($display_id)) { + $form['#markup'] = t('Invalid display id @display', array('@display' => $display_id)); + return $form; + } + } + + $form['#tree'] = TRUE; + // @todo When more functionality is added to this form, cloning here may be + // too soon. But some of what we do with $view later in this function + // results in making it unserializable due to PDO limitations. + $form_state['view'] = clone($this); + + $form['#attached']['library'][] = array('system', 'jquery.ui.tabs'); + $form['#attached']['library'][] = array('system', 'jquery.ui.dialog'); + $form['#attached']['library'][] = array('system', 'drupal.ajax'); + $form['#attached']['library'][] = array('system', 'jquery.form'); + $form['#attached']['library'][] = array('system', 'drupal.states'); + $form['#attached']['library'][] = array('system', 'drupal.tabledrag'); + + $form['#attached']['css'] = static::getAdminCSS(); + + $form['#attached']['js'][] = drupal_get_path('module', 'views_ui') . '/js/views-admin.js'; + $form['#attached']['js'][] = array( + 'data' => array('views' => array('ajax' => array( + 'id' => '#views-ajax-body', + 'title' => '#views-ajax-title', + 'popup' => '#views-ajax-popup', + 'defaultForm' => static::getDefaultAJAXMessage(), + ))), + 'type' => 'setting', + ); + + $form += array( + '#prefix' => '', + '#suffix' => '', + ); + $form['#prefix'] .= '
      '; + $form['#suffix'] = '
      ' . $form['#suffix']; + + $form['#attributes']['class'] = array('form-edit'); + + if (isset($this->locked) && is_object($this->locked) && $this->locked->owner != $GLOBALS['user']->uid) { + $form['locked'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('view-locked', 'messages', 'warning')), + '#markup' => t('This view is being edited by user !user, and is therefore locked from editing by others. This lock is !age old. Click here to break this lock.', array('!user' => theme('username', array('account' => user_load($this->locked->owner))), '!age' => format_interval(REQUEST_TIME - $this->locked->updated), '!break' => url('admin/structure/views/view/' . $this->storage->name . '/break-lock'))), + ); + } + else { + if (isset($this->vid) && $this->vid == 'new') { + $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard the view.'); + } + else { + $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard your changes.'); + } + + $form['changed'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('view-changed', 'messages', 'warning')), + '#markup' => $message, + ); + if (empty($this->changed)) { + $form['changed']['#attributes']['class'][] = 'js-hide'; + } + } + + $form['help_text'] = array( + '#prefix' => '
      ', + '#suffix' => '
      ', + '#markup' => t('Modify the display(s) of your view below or add new displays.'), + ); + + $form['actions'] = array( + '#type' => 'actions', + '#weight' => 0, + ); + + if (empty($this->changed)) { + $form['actions']['#attributes'] = array( + 'class' => array( + 'js-hide', + ), + ); + } + + $form['actions']['save'] = array( + '#type' => 'submit', + '#value' => t('Save'), + // Taken from the "old" UI. @TODO: Review and rename. + '#validate' => array('views_ui_edit_view_form_validate'), + '#submit' => array('views_ui_edit_view_form_submit'), + ); + $form['actions']['cancel'] = array( + '#type' => 'submit', + '#value' => t('Cancel'), + '#submit' => array('views_ui_edit_view_form_cancel'), + ); + + $form['displays'] = array( + '#prefix' => '

      ' . t('Displays') . '

      ' . "\n" . '
      ', + '#suffix' => '
      ', + ); + + $form['displays']['top'] = $this->renderDisplayTop($display_id); + + // The rest requires a display to be selected. + if ($display_id) { + $form_state['display_id'] = $display_id; + + // The part of the page where editing will take place. + $form['displays']['settings'] = array( + '#type' => 'container', + '#id' => 'edit-display-settings', + ); + $display_title = $this->getDisplayLabel($display_id, FALSE); + + $form['displays']['settings']['#title'] = '

      ' . t('@display_title details', array('@display_title' => ucwords($display_title))) . '

      '; + + // Add a text that the display is disabled. + if (!empty($this->displayHandlers[$display_id])) { + if (!$this->displayHandlers[$display_id]->isEnabled()) { + $form['displays']['settings']['disabled']['#markup'] = t('This display is disabled.'); + } + } + + $form['displays']['settings']['settings_content']= array( + '#theme_wrappers' => array('container'), + ); + // Add the edit display content + $form['displays']['settings']['settings_content']['tab_content'] = $this->getDisplayTab($display_id); + $form['displays']['settings']['settings_content']['tab_content']['#theme_wrappers'] = array('container'); + $form['displays']['settings']['settings_content']['tab_content']['#attributes'] = array('class' => array('views-display-tab')); + $form['displays']['settings']['settings_content']['tab_content']['#id'] = 'views-tab-' . $display_id; + // Mark deleted displays as such. + if (!empty($this->storage->display[$display_id]['deleted'])) { + $form['displays']['settings']['settings_content']['tab_content']['#attributes']['class'][] = 'views-display-deleted'; + } + // Mark disabled displays as such. + if (!$this->displayHandlers[$display_id]->isEnabled()) { + $form['displays']['settings']['settings_content']['tab_content']['#attributes']['class'][] = 'views-display-disabled'; + } + + // The content of the popup dialog. + $form['ajax-area'] = array( + '#theme_wrappers' => array('container'), + '#id' => 'views-ajax-popup', + ); + $form['ajax-area']['ajax-title'] = array( + '#markup' => '

      ', + ); + $form['ajax-area']['ajax-body'] = array( + '#theme_wrappers' => array('container'), + '#id' => 'views-ajax-body', + '#markup' => static::getDefaultAJAXMessage(), + ); + } + + // If relationships had to be fixed, we want to get that into the cache + // so that edits work properly, and to try to get the user to save it + // so that it's not using weird fixed up relationships. + if (!empty($this->relationships_changed) && drupal_container()->get('request')->request->count()) { + drupal_set_message(t('This view has been automatically updated to fix missing relationships. While this View should continue to work, you should verify that the automatic updates are correct and save this view.')); + views_ui_cache_set($this); + } + return $form; + } + + /** + * Provide the preview formulas and the preview output, too. + */ + public function buildPreviewForm($form, &$form_state, $display_id = 'default') { + // Reset the cache of IDs. Drupal rather aggressively prevents ID + // duplication but this causes it to remember IDs that are no longer even + // being used. + $seen_ids_init = &drupal_static('drupal_html_id:init'); + $seen_ids_init = array(); + + $form_state['no_cache'] = TRUE; + $form_state['view'] = $this; + + $form['#attributes'] = array('class' => array('clearfix')); + + // Add a checkbox controlling whether or not this display auto-previews. + $form['live_preview'] = array( + '#type' => 'checkbox', + '#id' => 'edit-displays-live-preview', + '#title' => t('Auto preview'), + '#default_value' => config('views.settings')->get('ui.always_live_preview'), + ); + + // Add the arguments textfield + $form['view_args'] = array( + '#type' => 'textfield', + '#title' => t('Preview with contextual filters:'), + '#description' => t('Separate contextual filter values with a "/". For example, %example.', array('%example' => '40/12/10')), + '#id' => 'preview-args', + ); + + // Add the preview button + $form['button'] = array( + '#type' => 'submit', + '#value' => t('Update preview'), + '#attributes' => array('class' => array('arguments-preview')), + '#prefix' => '
      ', + '#suffix' => '
      ', + '#id' => 'preview-submit', + '#ajax' => array( + 'path' => 'admin/structure/views/view/' . $this->storage->name . '/preview/' . $display_id . '/ajax', + 'wrapper' => 'views-preview-wrapper', + 'event' => 'click', + 'progress' => array('type' => 'throbber'), + 'method' => 'replace', + ), + // Make ENTER in arguments textfield (and other controls) submit the form + // as this button, not the Save button. + // @todo This only works for JS users. To make this work for nojs users, + // we may need to split Preview into a separate form. + '#process' => array_merge(array(array($this, 'processDefaultButton')), element_info_property('submit', '#process', array())), + ); + $form['#action'] = url('admin/structure/views/view/' . $this->storage->name .'/preview/' . $display_id); + + return $form; + } + + /** + * Form constructor callback to reorder displays on a view + */ + public function buildDisplaysReorderForm($form, &$form_state) { + $display_id = $form_state['display_id']; + + $form['view'] = array('#type' => 'value', '#value' => $this); + + $form['#tree'] = TRUE; + + $count = count($this->storage->display); + + uasort($this->storage->display, array('static', 'sortPosition')); + foreach ($this->storage->display as $display) { + $form[$display['id']] = array( + 'title' => array('#markup' => $display['display_title']), + 'weight' => array( + '#type' => 'weight', + '#value' => $display['position'], + '#delta' => $count, + '#title' => t('Weight for @display', array('@display' => $display['display_title'])), + '#title_display' => 'invisible', + ), + '#tree' => TRUE, + '#display' => $display, + 'removed' => array( + '#type' => 'checkbox', + '#id' => 'display-removed-' . $display['id'], + '#attributes' => array('class' => array('views-remove-checkbox')), + '#default_value' => isset($display['deleted']), + ), + ); + + if (isset($display['deleted']) && $display['deleted']) { + $form[$display['id']]['deleted'] = array('#type' => 'value', '#value' => TRUE); + } + if ($display['id'] === 'default') { + unset($form[$display['id']]['weight']); + unset($form[$display['id']]['removed']); + } + + } + + $form['#title'] = t('Displays Reorder'); + $form['#section'] = 'reorder'; + + // Add javascript settings that will be added via $.extend for tabledragging + $form['#js']['tableDrag']['reorder-displays']['weight'][0] = array( + 'target' => 'weight', + 'source' => NULL, + 'relationship' => 'sibling', + 'action' => 'order', + 'hidden' => TRUE, + 'limit' => 0, + ); + + $form['#action'] = url('admin/structure/views/nojs/reorder-displays/' . $this->storage->name . '/' . $display_id); + + $this->getStandardButtons($form, $form_state, 'views_ui_reorder_displays_form'); + $form['buttons']['submit']['#submit'] = array(array($this, 'submitDisplaysReorderForm')); + + return $form; + } + + /** + * Submit handler for rearranging display form + */ + public function submitDisplaysReorderForm($form, &$form_state) { + foreach ($form_state['input'] as $display => $info) { + // add each value that is a field with a weight to our list, but only if + // it has had its 'removed' checkbox checked. + if (is_array($info) && isset($info['weight']) && empty($info['removed'])) { + $order[$display] = $info['weight']; + } + } + + // Sort the order array + asort($order); + + // Fixing up positions + $position = 1; + + foreach (array_keys($order) as $display) { + $order[$display] = $position++; + } + + // Setting up position and removing deleted displays + $displays = $this->storage->display; + foreach ($displays as $display_id => $display) { + // Don't touch the default !!! + if ($display_id === 'default') { + $this->storage->display[$display_id]['position'] = 0; + continue; + } + if (isset($order[$display_id])) { + $this->storage->display[$display_id]['position'] = $order[$display_id]; + } + else { + $this->storage->display[$display_id]['deleted'] = TRUE; + } + } + + // Sorting back the display array as the position is not enough + uasort($this->storage->display, array('static', 'sortPosition')); + + // Store in cache + views_ui_cache_set($this); + $form_state['redirect'] = array('admin/structure/views/view/' . $this->storage->name . '/edit', array('fragment' => 'views-tab-default')); + } + + /** + * Add another form to the stack; clicking 'apply' will go to this form + * rather than closing the ajax popup. + */ + public function addFormToStack($key, $display_id, $args, $top = FALSE, $rebuild_keys = FALSE) { + // Reset the cache of IDs. Drupal rather aggressively prevents ID + // duplication but this causes it to remember IDs that are no longer even + // being used. + $seen_ids_init = &drupal_static('drupal_html_id:init'); + $seen_ids_init = array(); + + if (empty($this->stack)) { + $this->stack = array(); + } + + $stack = array($this->buildIdentifier($key, $display_id, $args), $key, $display_id, $args); + // If we're being asked to add this form to the bottom of the stack, no + // special logic is required. Our work is equally easy if we were asked to add + // to the top of the stack, but there's nothing in it yet. + if (!$top || empty($this->stack)) { + $this->stack[] = $stack; + } + // If we're adding to the top of an existing stack, we have to maintain the + // existing integer keys, so they can be used for the "2 of 3" progress + // indicator (which will now read "2 of 4"). + else { + $keys = array_keys($this->stack); + $first = current($keys); + $last = end($keys); + for ($i = $last; $i >= $first; $i--) { + if (!isset($this->stack[$i])) { + continue; + } + // Move form number $i to the next position in the stack. + $this->stack[$i + 1] = $this->stack[$i]; + unset($this->stack[$i]); + } + // Now that the previously $first slot is free, move the new form into it. + $this->stack[$first] = $stack; + ksort($this->stack); + + // Start the keys from 0 again, if requested. + if ($rebuild_keys) { + $this->stack = array_values($this->stack); + } + } + } + + /** + * Submit handler for adding new item(s) to a view. + */ + public function submitItemAdd($form, &$form_state) { + $type = $form_state['type']; + $types = static::viewsHandlerTypes(); + $section = $types[$type]['plural']; + + // Handle the override select. + list($was_defaulted, $is_defaulted) = $this->getOverrideValues($form, $form_state); + if ($was_defaulted && !$is_defaulted) { + // We were using the default display's values, but we're now overriding + // the default display and saving values specific to this display. + $display = &$this->displayHandlers[$form_state['display_id']]; + // setOverride toggles the override of this section. + $display->setOverride($section); + } + elseif (!$was_defaulted && $is_defaulted) { + // We used to have an override for this display, but the user now wants + // to go back to the default display. + // Overwrite the default display with the current form values, and make + // the current display use the new default values. + $display = &$this->displayHandlers[$form_state['display_id']]; + // optionsOverride toggles the override of this section. + $display->setOverride($section); + } + + if (!empty($form_state['values']['name']) && is_array($form_state['values']['name'])) { + // Loop through each of the items that were checked and add them to the view. + foreach (array_keys(array_filter($form_state['values']['name'])) as $field) { + list($table, $field) = explode('.', $field, 2); + + if ($cut = strpos($field, '$')) { + $field = substr($field, 0, $cut); + } + $id = $this->addItem($form_state['display_id'], $type, $table, $field); + + // check to see if we have group by settings + $key = $type; + // Footer,header and empty text have a different internal handler type(area). + if (isset($types[$type]['type'])) { + $key = $types[$type]['type']; + } + $handler = views_get_handler($table, $field, $key); + if ($this->display_handler->useGroupBy() && $handler->usesGroupBy()) { + $this->addFormToStack('config-item-group', $form_state['display_id'], array($type, $id)); + } + + // check to see if this type has settings, if so add the settings form first + if ($handler && $handler->hasExtraOptions()) { + $this->addFormToStack('config-item-extra', $form_state['display_id'], array($type, $id)); + } + // Then add the form to the stack + $this->addFormToStack('config-item', $form_state['display_id'], array($type, $id)); + } + } + + if (isset($this->form_cache)) { + unset($this->form_cache); + } + + // Store in cache + views_ui_cache_set($this); + } + + public function renderPreview($display_id, $args = array()) { + // Save the current path so it can be restored before returning from this function. + $old_q = current_path(); + + // Determine where the query and performance statistics should be output. + $config = config('views.settings'); + $show_query = $config->get('ui.show.sql_query.enabled'); + $show_info = $config->get('ui.show.preview_information'); + $show_location = $config->get('ui.show.sql_query.where'); + + $show_stats = $config->get('ui.show.performance_statistics'); + if ($show_stats) { + $show_stats = $config->get('ui.show.sql_query.where'); + } + + $combined = $show_query && $show_stats; + + $rows = array('query' => array(), 'statistics' => array()); + $output = ''; + + $errors = $this->validate(); + if ($errors === TRUE) { + $this->ajax = TRUE; + $this->live_preview = TRUE; + $this->views_ui_context = TRUE; + + // AJAX happens via $_POST but everything expects exposed data to + // be in GET. Copy stuff but remove ajax-framework specific keys. + // If we're clicking on links in a preview, though, we could actually + // still have some in $_GET, so we use $_REQUEST to ensure we get it all. + $exposed_input = drupal_container()->get('request')->request->all(); + foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { + if (isset($exposed_input[$key])) { + unset($exposed_input[$key]); + } + } + + $this->setExposedInput($exposed_input); + + if (!$this->setDisplay($display_id)) { + return t('Invalid display id @display', array('@display' => $display_id)); + } + + $this->setArguments($args); + + // Store the current view URL for later use: + if ($this->display_handler->getOption('path')) { + $path = $this->getUrl(); + } + + // Make view links come back to preview. + $this->override_path = 'admin/structure/views/nojs/preview/' . $this->storage->name . '/' . $display_id; + + // Also override the current path so we get the pager. + $original_path = current_path(); + $q = _current_path($this->override_path); + if ($args) { + $q .= '/' . implode('/', $args); + _current_path($q); + } + + // Suppress contextual links of entities within the result set during a + // Preview. + // @todo We'll want to add contextual links specific to editing the View, so + // the suppression may need to be moved deeper into the Preview pipeline. + views_ui_contextual_links_suppress_push(); + $preview = $this->preview($display_id, $args); + views_ui_contextual_links_suppress_pop(); + + // Reset variables. + unset($this->override_path); + _current_path($original_path); + + // Prepare the query information and statistics to show either above or + // below the view preview. + if ($show_info || $show_query || $show_stats) { + // Get information from the preview for display. + if (!empty($this->build_info['query'])) { + if ($show_query) { + $query = $this->build_info['query']; + // Only the sql default class has a method getArguments. + $quoted = array(); + + if (get_class($this->query) == 'views_plugin_query_default') { + $quoted = $query->getArguments(); + $connection = Database::getConnection(); + foreach ($quoted as $key => $val) { + if (is_array($val)) { + $quoted[$key] = implode(', ', array_map(array($connection, 'quote'), $val)); + } + else { + $quoted[$key] = $connection->quote($val); + } + } + } + $rows['query'][] = array('' . t('Query') . '', '
      ' . check_plain(strtr($query, $quoted)) . '
      '); + if (!empty($this->additional_queries)) { + $queries = '' . t('These queries were run during view rendering:') . ''; + foreach ($this->additional_queries as $query) { + if ($queries) { + $queries .= "\n"; + } + $queries .= t('[@time ms]', array('@time' => intval($query[1] * 100000) / 100)) . ' ' . $query[0]; + } + + $rows['query'][] = array('' . t('Other queries') . '', '
      ' . $queries . '
      '); + } + } + if ($show_info) { + $rows['query'][] = array('' . t('Title') . '', filter_xss_admin($this->getTitle())); + if (isset($path)) { + $path = l($path, $path); + } + else { + $path = t('This display has no path.'); + } + $rows['query'][] = array('' . t('Path') . '', $path); + } + + if ($show_stats) { + $rows['statistics'][] = array('' . t('Query build time') . '', t('@time ms', array('@time' => intval($this->build_time * 100000) / 100))); + $rows['statistics'][] = array('' . t('Query execute time') . '', t('@time ms', array('@time' => intval($this->execute_time * 100000) / 100))); + $rows['statistics'][] = array('' . t('View render time') . '', t('@time ms', array('@time' => intval($this->render_time * 100000) / 100))); + + } + drupal_alter('views_preview_info', $rows, $this); + } + else { + // No query was run. Display that information in place of either the + // query or the performance statistics, whichever comes first. + if ($combined || ($show_location === 'above')) { + $rows['query'] = array(array('' . t('Query') . '', t('No query was run'))); + } + else { + $rows['statistics'] = array(array('' . t('Query') . '', t('No query was run'))); + } + } + } + } + else { + foreach ($errors as $error) { + drupal_set_message($error, 'error'); + } + $preview = t('Unable to preview due to validation errors.'); + } + + // Assemble the preview, the query info, and the query statistics in the + // requested order. + if ($show_location === 'above') { + if ($combined) { + $output .= '
      ' . theme('table', array('rows' => array_merge($rows['query'], $rows['statistics']))) . '
      '; + } + else { + $output .= '
      ' . theme('table', array('rows' => $rows['query'])) . '
      '; + } + } + elseif ($show_stats === 'above') { + $output .= '
      ' . theme('table', array('rows' => $rows['statistics'])) . '
      '; + } + + $output .= $preview; + + if ($show_location === 'below') { + if ($combined) { + $output .= '
      ' . theme('table', array('rows' => array_merge($rows['query'], $rows['statistics']))) . '
      '; + } + else { + $output .= '
      ' . theme('table', array('rows' => $rows['query'])) . '
      '; + } + } + elseif ($show_stats === 'below') { + $output .= '
      ' . theme('table', array('rows' => $rows['statistics'])) . '
      '; + } + + _current_path($old_q); + return $output; + } + + /** + * Recursively adds microweights to a render array, similar to what form_builder() does for forms. + * + * @todo Submit a core patch to fix drupal_render() to do this, so that all + * render arrays automatically preserve array insertion order, as forms do. + */ + public static function addMicroweights(&$build) { + $count = 0; + foreach (element_children($build) as $key) { + if (!isset($build[$key]['#weight'])) { + $build[$key]['#weight'] = $count/1000; + } + static::addMicroweights($build[$key]); + $count++; + } + } + + /** + * Get the user's current progress through the form stack. + * + * @return + * FALSE if the user is not currently in a multiple-form stack. Otherwise, + * an associative array with the following keys: + * - current: The number of the current form on the stack. + * - total: The total number of forms originally on the stack. + */ + public function getFormProgress() { + $progress = FALSE; + if (!empty($this->stack)) { + $stack = $this->stack; + // The forms on the stack have integer keys that don't change as the forms + // are completed, so we can see which ones are still left. + $keys = array_keys($this->stack); + // Add 1 to the array keys for the benefit of humans, who start counting + // from 1 and not 0. + $current = reset($keys) + 1; + $total = end($keys) + 1; + if ($total > 1) { + $progress = array(); + $progress['current'] = $current; + $progress['total'] = $total; + } + } + return $progress; + } + + /** + * Build a form identifier that we can use to see if one form + * is the same as another. Since the arguments differ slightly + * we do a lot of spiffy concatenation here. + */ + public function buildIdentifier($key, $display_id, $args) { + $form = views_ui_ajax_forms($key); + // Automatically remove the single-form cache if it exists and + // does not match the key. + $identifier = implode('-', array($key, $this->storage->name, $display_id)); + + foreach ($form['args'] as $id) { + $arg = (!empty($args)) ? array_shift($args) : NULL; + $identifier .= '-' . $arg; + } + return $identifier; + } + + /** + * Display position sorting function + */ + public static function sortPosition($display1, $display2) { + if ($display1['position'] != $display2['position']) { + return $display1['position'] < $display2['position'] ? -1 : 1; + } + + return 0; + } + + /** + * Build up a $form_state object suitable for use with drupal_build_form + * based on known information about a form. + */ + public function buildFormState($js, $key, $display_id, $args) { + $form = views_ui_ajax_forms($key); + // Build up form state + $form_state = array( + 'form_key' => $key, + 'form_id' => $form['form_id'], + 'view' => &$this, + 'ajax' => $js, + 'display_id' => $display_id, + 'no_redirect' => TRUE, + ); + // If an method was specified, use that for the callback. + if (isset($form['callback'])) { + $form_state['build_info']['args'] = array(); + $form_state['build_info']['callback'] = array($this, $form['callback']); + } + + foreach ($form['args'] as $id) { + $form_state[$id] = (!empty($args)) ? array_shift($args) : NULL; + } + + return $form_state; + } + + /** + * #process callback for a button; makes implicit form submissions trigger as this button. + * + * @see Drupal.behaviors.viewsImplicitFormSubmission + */ + public static function processDefaultButton($element, &$form_state, $form) { + $setting['viewsImplicitFormSubmission'][$form['#id']]['defaultButton'] = $element['#id']; + $element['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); + return $element; + } + +} diff --git a/core/modules/views/views_ui/theme/theme.inc b/core/modules/views/views_ui/theme/theme.inc new file mode 100644 index 0000000..530aec5 --- /dev/null +++ b/core/modules/views/views_ui/theme/theme.inc @@ -0,0 +1,521 @@ + container function. + */ +function theme_views_ui_container($variables) { + $element = $variables['element']; + return '' . $element['#children'] . ''; +} + +function template_preprocess_views_ui_display_tab_setting(&$variables) { + static $zebra = 0; + $variables['zebra'] = ($zebra % 2 === 0 ? 'odd' : 'even'); + $zebra++; + + // Put the main link to the left side + array_unshift($variables['settings_links'], $variables['link']); + $variables['settings_links'] = implode(' | ', $variables['settings_links']); + + if (!empty($variables['defaulted'])) { + $variables['attributes']['class'][] = 'defaulted'; + } + if (!empty($variables['overridden'])) { + $variables['attributes']['class'][] = 'overridden'; + $variables['attributes_array']['title'][] = t('Overridden'); + } + + // Append a colon to the description, if requested. + if ($variables['description'] && $variables['description_separator']) { + $variables['description'] .= t(':'); + } +} + +function template_preprocess_views_ui_display_tab_bucket(&$variables) { + $element = $variables['element']; + + if (!empty($element['#name'])) { + $variables['attributes']['class'][] = drupal_html_class($element['#name']); + } + if (!empty($element['#overridden'])) { + $variables['attributes']['class'][] = 'overridden'; + $variables['attributes_array']['title'][] = t('Overridden'); + } + + $variables['content'] = $element['#children']; + $variables['title'] = $element['#title']; + $variables['actions'] = !empty($element['#actions']) ? render($element['#actions']) : ''; +} + +/** + * Implements hook_preprocess_HOOK() for theme_views_ui_view_info(). + */ +function template_preprocess_views_ui_view_info(&$variables) { + $variables['title'] = $variables['view']->getHumanName(); + + $displays = $variables['view']->getDisplaysList(); + $variables['displays'] = empty($displays) ? t('None') : format_plural(count($displays), 'Display', 'Displays') . ': ' . '' . implode(', ', $displays) . ''; +} + +/** + * Returns basic administrative information about a view. + */ +function theme_views_ui_view_info($variables) { + $output = ''; + $output .= '
      ' . $variables['title'] . "
      \n"; + $output .= '
      ' . $variables['displays'] . "
      \n"; + return $output; +} + +/** + * Theme the expose filter form. + */ +function theme_views_ui_expose_filter_form($variables) { + $form = $variables['form']; + $more = drupal_render($form['more']); + + $output = drupal_render($form['form_description']); + $output .= drupal_render($form['expose_button']); + $output .= drupal_render($form['group_button']); + if (isset($form['required'])) { + $output .= drupal_render($form['required']); + } + $output .= drupal_render($form['label']); + $output .= drupal_render($form['description']); + + $output .= drupal_render($form['operator']); + $output .= drupal_render($form['value']); + + if (isset($form['use_operator'])) { + $output .= '
      '; + $output .= drupal_render($form['use_operator']); + $output .= '
      '; + } + + // Only output the right column markup if there's a left column to begin with + if (!empty($form['operator']['#type'])) { + $output .= '
      '; + $output .= drupal_render_children($form); + $output .= '
      '; + } + else { + $output .= drupal_render_children($form); + } + + $output .= $more; + + return $output; +} + +/** + * Theme the build group filter form. + */ +function theme_views_ui_build_group_filter_form($variables) { + $form = $variables['form']; + $more = drupal_render($form['more']); + + $output = drupal_render($form['form_description']); + $output .= drupal_render($form['expose_button']); + $output .= drupal_render($form['group_button']); + if (isset($form['required'])) { + $output .= drupal_render($form['required']); + } + + $output .= drupal_render($form['operator']); + $output .= drupal_render($form['value']); + + $output .= '
      '; + $output .= drupal_render($form['optional']); + $output .= drupal_render($form['remember']); + $output .= '
      '; + + $output .= '
      '; + $output .= drupal_render($form['widget']); + $output .= drupal_render($form['label']); + $output .= drupal_render($form['description']); + $output .= '
      '; + + $header = array( + t('Default'), + t('Weight'), + t('Label'), + t('Operator'), + t('Value'), + t('Operations'), + ); + + $form['default_group'] = form_process_radios($form['default_group']); + $form['default_group_multiple'] = form_process_checkboxes($form['default_group_multiple']); + $form['default_group']['All']['#title'] = ''; + + hide($form['default_group_multiple']['All']); + $rows[] = array( + drupal_render($form['default_group']['All']), + '', + array( + 'data' => config('views.settings')->get('ui.exposed_filter_any_label') == 'old_any' ? t('<Any>') : t('- Any -'), + 'colspan' => 4, + 'class' => array('class' => 'any-default-radios-row'), + ), + ); + + foreach (element_children($form['group_items']) as $group_id) { + $form['group_items'][$group_id]['value']['#title'] = ''; + $data = array( + 'default' => drupal_render($form['default_group'][$group_id]) . drupal_render($form['default_group_multiple'][$group_id]), + 'weight' => drupal_render($form['group_items'][$group_id]['weight']), + 'title' => drupal_render($form['group_items'][$group_id]['title']), + 'operator' => drupal_render($form['group_items'][$group_id]['operator']), + 'value' => drupal_render($form['group_items'][$group_id]['value']), + 'remove' => drupal_render($form['group_items'][$group_id]['remove']) . l('' . t('Remove') . '', 'javascript:void()', array('attributes' => array('id' => 'views-remove-link-' . $group_id, 'class' => array('views-hidden', 'views-button-remove', 'views-groups-remove-link', 'views-remove-link'), 'alt' => t('Remove this item'), 'title' => t('Remove this item')), 'html' => true)), + ); + $rows[] = array('data' => $data, 'id' => 'views-row-' . $group_id, 'class' => array('draggable')); + } + $table = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('views-filter-groups'), 'id' => 'views-filter-groups'))) . drupal_render($form['add_group']); + drupal_add_tabledrag('views-filter-groups', 'order', 'sibling', 'weight'); + $render_form = drupal_render_children($form); + return $output . $render_form . $table . $more; +} + +/** + * Turn the reorder form into a proper table + */ +function theme_views_ui_reorder_displays_form($vars) { + $form = $vars['form']; + $rows = array(); + foreach (element_children($form) as $key) { + if (isset($form[$key]['#display'])) { + $display = &$form[$key]; + + $row = array(); + $row[] = drupal_render($display['title']); + $form[$key]['weight']['#attributes']['class'] = array('weight'); + $row[] = drupal_render($form[$key]['weight']); + if (isset($display['removed'])) { + $row[] = drupal_render($form[$key]['removed']) . + l('' . t('Remove') . '', + 'javascript:void()', + array( + 'attributes' => array( + 'id' => 'display-remove-link-' . $key, + 'class' => array('views-button-remove display-remove-link'), + 'alt' => t('Remove this display'), + 'title' => t('Remove this display')), + 'html' => TRUE)); + } + else { + $row[] = ''; + } + $class = array(); + $styles = array(); + if (isset($form[$key]['weight']['#type'])) { + $class[] = 'draggable'; + } + if (isset($form[$key]['deleted']['#value']) && $form[$key]['deleted']['#value']) { + $styles[] = 'display: none;'; + } + $rows[] = array('data' => $row, 'class' => $class, 'id' => 'display-row-' . $key, 'style' => $styles); + } + } + + $header = array(t('Display'), t('Weight'), t('Remove')); + $output = ''; + drupal_add_tabledrag('reorder-displays', 'order', 'sibling', 'weight'); + + $output = drupal_render($form['override']); + $output .= '
      '; + $output .= theme('table', + array('header' => $header, + 'rows' => $rows, + 'attributes' => array('id' => 'reorder-displays'), + )); + $output .= '
      '; + $output .= drupal_render_children($form); + + return $output; +} + +/** + * Turn the rearrange form into a proper table + */ +function theme_views_ui_rearrange_form($variables) { + $form = $variables['form']; + + $rows = array(); + foreach (element_children($form['fields']) as $id) { + if (isset($form['fields'][$id]['name'])) { + $row = array(); + $row[] = drupal_render($form['fields'][$id]['name']); + $form['fields'][$id]['weight']['#attributes']['class'] = array('weight'); + $row[] = drupal_render($form['fields'][$id]['weight']); + $row[] = drupal_render($form['fields'][$id]['removed']) . l('' . t('Remove') . '', 'javascript:void()', array('attributes' => array('id' => 'views-remove-link-' . $id, 'class' => array('views-hidden', 'views-button-remove', 'views-remove-link'), 'alt' => t('Remove this item'), 'title' => t('Remove this item')), 'html' => TRUE)); + $rows[] = array('data' => $row, 'class' => array('draggable'), 'id' => 'views-row-' . $id); + } + } + if (empty($rows)) { + $rows[] = array(array('data' => t('No fields available.'), 'colspan' => '2')); + } + + $header = array('', t('Weight'), t('Remove')); + $output = drupal_render($form['override']); + $output .= '
      '; + $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'arrange'))); + $output .= '
      '; + $output .= drupal_render_children($form); + drupal_add_tabledrag('arrange', 'order', 'sibling', 'weight'); + + return $output; +} + +/** + * Turn the rearrange form into a proper table + */ +function theme_views_ui_rearrange_filter_form(&$vars) { + $form = $vars['form']; + $rows = $ungroupable_rows = array(); + // Enable grouping only if > 1 group. + $grouping = count(array_keys($form['#group_options'])) > 1; + + foreach ($form['#group_renders'] as $group_id => $contents) { + // Header row for the group. + if ($group_id !== 'ungroupable') { + // Set up tabledrag so that it changes the group dropdown when rows are + // dragged between groups. + drupal_add_tabledrag('views-rearrange-filters', 'match', 'sibling', 'views-group-select', 'views-group-select-' . $group_id); + + // Title row, spanning all columns. + $row = array(); + // Add a cell to the first row, containing the group operator. + $row[] = array('class' => array('group', 'group-operator', 'container-inline'), 'data' => drupal_render($form['filter_groups']['groups'][$group_id]), 'rowspan' => max(array(2, count($contents) + 1))); + // Title. + $row[] = array('class' => array('group', 'group-title'), 'data' => '' . $form['#group_options'][$group_id] . '', 'colspan' => 4); + $rows[] = array('class' => array('views-group-title'), 'data' => $row, 'id' => 'views-group-title-' . $group_id); + + // Row which will only appear if the group has nothing in it. + $row = array(); + $class = 'group-' . (count($contents) ? 'populated' : 'empty'); + $instructions = '' . t('No filters have been added.') . ' ' . t('Drag to add filters.') . ''; + // When JavaScript is enabled, the button for removing the group (if it's + // present) should be hidden, since it will be replaced by a link on the + // client side. + if (!empty($form['remove_groups'][$group_id]['#type']) && $form['remove_groups'][$group_id]['#type'] == 'submit') { + $form['remove_groups'][$group_id]['#attributes']['class'][] = 'js-hide'; + } + $row[] = array('colspan' => 5, 'data' => $instructions . drupal_render($form['remove_groups'][$group_id])); + $rows[] = array('class' => array("group-message", "group-$group_id-message", $class), 'data' => $row, 'id' => 'views-group-' . $group_id); + } + + foreach ($contents as $id) { + if (isset($form['filters'][$id]['name'])) { + $row = array(); + $row[] = drupal_render($form['filters'][$id]['name']); + $form['filters'][$id]['weight']['#attributes']['class'] = array('weight'); + $row[] = drupal_render($form['filters'][$id]['weight']); + $form['filters'][$id]['group']['#attributes']['class'] = array('views-group-select views-group-select-' . $group_id); + $row[] = drupal_render($form['filters'][$id]['group']); + $form['filters'][$id]['removed']['#attributes']['class'][] = 'js-hide'; + $row[] = drupal_render($form['filters'][$id]['removed']) . l('' . t('Remove') . '', 'javascript:void()', array('attributes' => array('id' => 'views-remove-link-' . $id, 'class' => array('views-hidden', 'views-button-remove', 'views-groups-remove-link', 'views-remove-link'), 'alt' => t('Remove this item'), 'title' => t('Remove this item')), 'html' => TRUE)); + + $row = array('data' => $row, 'class' => array('draggable'), 'id' => 'views-row-' . $id); + if ($group_id !== 'ungroupable') { + $rows[] = $row; + } + else { + $ungroupable_rows[] = $row; + } + } + } + } + if (empty($rows)) { + $rows[] = array(array('data' => t('No fields available.'), 'colspan' => '2')); + } + + $output = drupal_render($form['override']); + $output .= '
      '; + if ($grouping) { + $output .= drupal_render($form['filter_groups']['operator']); + } + else { + $form['filter_groups']['groups'][0]['#title'] = t('Operator'); + $output .= drupal_render($form['filter_groups']['groups'][0]); + } + + if (!empty($ungroupable_rows)) { + drupal_add_tabledrag('views-rearrange-filters-ungroupable', 'order', 'sibling', 'weight'); + $header = array(t('Ungroupable filters'), t('Weight'), array('class' => array('views-hide-label'), 'data' => t('Group')), array('class' => array('views-hide-label'), 'data' => t('Remove'))); + $output .= theme('table', array('header' => $header, 'rows' => $ungroupable_rows, 'attributes' => array('id' => 'views-rearrange-filters-ungroupable', 'class' => array('arrange')))); + } + + // Set up tabledrag so that the weights are changed when rows are dragged. + drupal_add_tabledrag('views-rearrange-filters', 'order', 'sibling', 'weight'); + $output .= theme('table', array('rows' => $rows, 'attributes' => array('id' => 'views-rearrange-filters', 'class' => array('arrange')))); + $output .= '
      '; + + // When JavaScript is enabled, the button for adding a new group should be + // hidden, since it will be replaced by a link on the client side. + $form['buttons']['add_group']['#attributes']['class'][] = 'js-hide'; + + // Render the rest of the form and return. + $output .= drupal_render_children($form); + return $output; +} + +/** + * Theme the form for the table style plugin + */ +function theme_views_ui_style_plugin_table($variables) { + $form = $variables['form']; + + $output = drupal_render($form['description_markup']); + + $header = array( + t('Field'), + t('Column'), + t('Align'), + t('Separator'), + array( + 'data' => t('Sortable'), + 'align' => 'center', + ), + array( + 'data' => t('Default order'), + 'align' => 'center', + ), + array( + 'data' => t('Default sort'), + 'align' => 'center', + ), + array( + 'data' => t('Hide empty column'), + 'align' => 'center', + ), + array( + 'data' => t('Responsive'), + 'align' => 'center', + ), + ); + $rows = array(); + foreach (element_children($form['columns']) as $id) { + $row = array(); + $row[] = drupal_render($form['info'][$id]['name']); + $row[] = drupal_render($form['columns'][$id]); + $row[] = drupal_render($form['info'][$id]['align']); + $row[] = drupal_render($form['info'][$id]['separator']); + if (!empty($form['info'][$id]['sortable'])) { + $row[] = array( + 'data' => drupal_render($form['info'][$id]['sortable']), + 'align' => 'center', + ); + $row[] = array( + 'data' => drupal_render($form['info'][$id]['default_sort_order']), + 'align' => 'center', + ); + $row[] = array( + 'data' => drupal_render($form['default'][$id]), + 'align' => 'center', + ); + } + else { + $row[] = ''; + $row[] = ''; + $row[] = ''; + } + $row[] = array( + 'data' => drupal_render($form['info'][$id]['empty_column']), + 'align' => 'center', + ); + $row[] = array( + 'data' => drupal_render($form['info'][$id]['responsive']), + 'align' => 'center', + ); + $rows[] = $row; + } + + // Add the special 'None' row. + $rows[] = array(t('None'), '', '', '', '', '', array('align' => 'center', 'data' => drupal_render($form['default'][-1])), '', ''); + + $output .= theme('table', array('header' => $header, 'rows' => $rows)); + $output .= drupal_render_children($form); + return $output; +} + +/** + * Theme preprocess for theme_views_ui_view_preview_section(). + */ +function template_preprocess_views_ui_view_preview_section(&$vars) { + switch ($vars['section']) { + case 'title': + $vars['title'] = t('Title'); + $links = views_ui_view_preview_section_display_category_links($vars['view'], 'title', $vars['title']); + break; + case 'header': + $vars['title'] = t('Header'); + $links = views_ui_view_preview_section_handler_links($vars['view'], $vars['section']); + break; + case 'empty': + $vars['title'] = t('No results behavior'); + $links = views_ui_view_preview_section_handler_links($vars['view'], $vars['section']); + break; + case 'exposed': + // @todo Sorts can be exposed too, so we may need a better title. + $vars['title'] = t('Exposed Filters'); + $links = views_ui_view_preview_section_display_category_links($vars['view'], 'exposed_form_options', $vars['title']); + break; + case 'rows': + // @todo The title needs to depend on what is being viewed. + $vars['title'] = t('Content'); + $links = views_ui_view_preview_section_rows_links($vars['view']); + break; + case 'pager': + $vars['title'] = t('Pager'); + $links = views_ui_view_preview_section_display_category_links($vars['view'], 'pager_options', $vars['title']); + break; + case 'more': + $vars['title'] = t('More'); + $links = views_ui_view_preview_section_display_category_links($vars['view'], 'use_more', $vars['title']); + break; + case 'footer': + $vars['title'] = t('Footer'); + $links = views_ui_view_preview_section_handler_links($vars['view'], $vars['section']); + break; + case 'attachment_before': + // @todo: Add links to the attachment configuration page. + $vars['title'] = t('Attachment before'); + break; + case 'attachment_after': + // @todo: Add links to the attachment configuration page. + $vars['title'] = t('Attachment after'); + break; + } + + if (isset($links)) { + $build = array( + '#prefix' => '
      ', + '#suffix' => '
      ', + '#theme' => 'links__contextual', + '#links' => $links, + '#attributes' => array('class' => array('contextual-links')), + '#attached' => array( + 'library' => array(array('contextual', 'contextual-links')), + ), + ); + $vars['links'] = drupal_render($build); + } + $vars['theme_hook_suggestions'][] = 'views_ui_view_preview_section__' . $vars['section']; +} + +/** + * Returns the HTML for a section of a View being previewed within the Views UI. + */ +function theme_views_ui_view_preview_section($vars) { + return '

      ' . $vars['title'] . '

      ' + . $vars['links'] + . '
      '. $vars['content'] . '
      '; +} diff --git a/core/modules/views/views_ui/theme/views-ui-display-tab-bucket.tpl.php b/core/modules/views/views_ui/theme/views-ui-display-tab-bucket.tpl.php new file mode 100644 index 0000000..2e74bc3 --- /dev/null +++ b/core/modules/views/views_ui/theme/views-ui-display-tab-bucket.tpl.php @@ -0,0 +1,16 @@ + +
      > + + + + +

      + + +
      diff --git a/core/modules/views/views_ui/theme/views-ui-display-tab-setting.tpl.php b/core/modules/views/views_ui/theme/views-ui-display-tab-setting.tpl.php new file mode 100644 index 0000000..be294b6 --- /dev/null +++ b/core/modules/views/views_ui/theme/views-ui-display-tab-setting.tpl.php @@ -0,0 +1,15 @@ + +
      > + + + + + + +
      diff --git a/core/modules/views/views_ui/theme/views-ui-edit-item.tpl.php b/core/modules/views/views_ui/theme/views-ui-edit-item.tpl.php new file mode 100644 index 0000000..5963736 --- /dev/null +++ b/core/modules/views/views_ui/theme/views-ui-edit-item.tpl.php @@ -0,0 +1,44 @@ + + + +
      + +
      + +
      + +
      + +
      + + $field): ?> + + + +
      + + +
      + + +
      diff --git a/core/modules/views/views_ui/theme/views-ui-edit-view.tpl.php b/core/modules/views/views_ui/theme/views-ui-edit-view.tpl.php new file mode 100644 index 0000000..566b8a4 --- /dev/null +++ b/core/modules/views/views_ui/theme/views-ui-edit-view.tpl.php @@ -0,0 +1,46 @@ + +
      + +
      + break this lock.', array('!user' => $locked, '!age' => $lock_age, '!break' => $break)); ?> +
      + +
      "> + vid)): ?> +
      + +
      + + + @base.', + array('%name' => $view->storage->name, '@base' => $base_table)); ?> +
      + + + +
      +
      + +
      +
      + + +
      +
      + + + +

      +
      + +
      +
      diff --git a/core/modules/views/views_ui/views_ui.info b/core/modules/views/views_ui/views_ui.info new file mode 100644 index 0000000..82c8ab3 --- /dev/null +++ b/core/modules/views/views_ui/views_ui.info @@ -0,0 +1,7 @@ +name = Views UI +description = Administrative interface for Views. +package = Core +version = VERSION +core = 8.x +configure = admin/structure/views +dependencies[] = views diff --git a/core/modules/views/views_ui/views_ui.module b/core/modules/views/views_ui/views_ui.module new file mode 100644 index 0000000..34b05fe --- /dev/null +++ b/core/modules/views/views_ui/views_ui.module @@ -0,0 +1,764 @@ + 'user_access', + 'access arguments' => array('administer views'), + 'file' => 'admin.inc', + ); + // Set up the base for AJAX callbacks. + $ajax_base = array( + 'page callback' => 'views_ui_ajax_callback', + 'page arguments' => array(4, 5), + 'type' => MENU_CALLBACK, + ) + $base; + + // Top-level Views module pages (not tied to a particular View). + $items['admin/structure/views/add'] = array( + 'title' => 'Add new view', + 'page callback' => 'views_ui_add_page', + 'type' => MENU_LOCAL_ACTION, + ) + $base; + + $items['admin/structure/views'] = array( + 'title' => 'Views', + 'description' => 'Manage customized lists of content.', + 'page callback' => 'views_ui_list_page', + ) + $base; + + $items['admin/structure/views/list'] = array( + 'title' => 'List', + 'weight' => -10, + 'type' => MENU_DEFAULT_LOCAL_TASK, + ) + $base; + + $items['admin/structure/views/view/%views_ui/enable'] = array( + 'title' => 'Enable a view', + ) + $ajax_base; + + $items['admin/structure/views/view/%views_ui/disable'] = array( + 'title' => 'Disable a view', + ) + $ajax_base; + + $items['admin/structure/views/settings'] = array( + 'title' => 'Settings', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('views_ui_admin_settings_basic'), + 'type' => MENU_LOCAL_TASK, + ) + $base; + $items['admin/structure/views/settings/basic'] = array( + 'title' => 'Basic', + 'page arguments' => array('views_ui_admin_settings_basic'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + ) + $base; + $items['admin/structure/views/settings/advanced'] = array( + 'title' => 'Advanced', + 'page arguments' => array('views_ui_admin_settings_advanced'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + ) + $base; + + // The primary Edit View page. Secondary tabs for each Display are added in + // views_ui_menu_local_tasks_alter(). + $items['admin/structure/views/view/%views_ui_cache'] = array( + 'title callback' => 'views_ui_edit_page_title', + 'title arguments' => array(4), + 'page callback' => 'views_ui_edit_page', + 'page arguments' => array(4), + ) + $base; + $items['admin/structure/views/view/%views_ui_cache/edit'] = array( + 'title' => 'Edit view', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'weight' => -10, + 'theme callback' => 'ajax_base_page_theme', + ) + $base; + $items['admin/structure/views/view/%views_ui_cache/edit/%/ajax'] = array( + 'page callback' => 'views_ui_ajax_get_form', + 'page arguments' => array('views_ui_edit_form', 4, 6), + 'delivery callback' => 'ajax_deliver', + 'theme callback' => 'ajax_base_page_theme', + 'type' => MENU_CALLBACK, + ) + $base; + $items['admin/structure/views/view/%views_ui_cache/preview/%'] = array( + 'page callback' => 'views_ui_build_preview', + 'page arguments' => array(4, 6), + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'type' => MENU_VISIBLE_IN_BREADCRUMB, + ) + $base; + $items['admin/structure/views/view/%views_ui_cache/preview/%/ajax'] = array( + 'page callback' => 'views_ui_build_preview', + 'page arguments' => array(4, 6), + 'delivery callback' => 'ajax_deliver', + 'theme callback' => 'ajax_base_page_theme', + 'type' => MENU_CALLBACK, + ) + $base; + + // Additional pages for acting on a View. + + $items['admin/structure/views/view/%views_ui_cache/break-lock'] = array( + 'title' => 'Break lock', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('views_ui_break_lock_confirm', 4), + 'type' => MENU_VISIBLE_IN_BREADCRUMB, + ) + $base; + + // NoJS/AJAX callbacks that can use the default Views AJAX form system. + $items['admin/structure/views/nojs/%/%views_ui_cache'] = array( + 'page callback' => 'views_ui_ajax_form', + 'page arguments' => array(FALSE, 4, 5), + 'type' => MENU_CALLBACK, + ) + $base; + $items['admin/structure/views/ajax/%/%views_ui_cache'] = array( + 'page callback' => 'views_ui_ajax_form', + 'page arguments' => array(TRUE, 4, 5), + 'delivery callback' => 'ajax_deliver', + 'type' => MENU_CALLBACK, + ) + $base; + + $items['admin/structure/views/nojs/preview/%views_ui_cache/%'] = array( + 'page callback' => 'views_ui_preview', + 'page arguments' => array(5, 6), + ) + $base; + $items['admin/structure/views/ajax/preview/%views_ui_cache/%'] = array( + 'page callback' => 'views_ui_preview', + 'page arguments' => array(5, 6), + 'delivery callback' => 'ajax_deliver', + ) + $base; + + // Autocomplete callback for tagging a View. + // Views module uses admin/views/... instead of admin/structure/views/... for + // autocomplete paths, so be consistent with that. + // @todo Change to admin/structure/views/... when the change can be made to + // Views module as well. + $items['admin/views/ajax/autocomplete/tag'] = array( + 'page callback' => 'views_ui_autocomplete_tag', + 'type' => MENU_CALLBACK, + ) + $base; + + // A page in the Reports section to show usage of fields in all views + $items['admin/reports/fields/list'] = array( + 'title' => 'List', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items['admin/reports/fields/views-fields'] = array( + 'title' => 'Used in views', + 'description' => 'Overview of fields used in all views.', + 'page callback' => 'views_ui_field_list', + 'type' => MENU_LOCAL_TASK, + 'weight' => 0, + ) + $base; + + // A page in the Reports section to show usage of plugins in all views. + $items['admin/reports/views-plugins'] = array( + 'title' => 'Views plugins', + 'description' => 'Overview of plugins used in all views.', + 'page callback' => 'views_ui_plugin_list', + ) + $base; + + return $items; +} + +/** + * Implements hook_theme(). + */ +function views_ui_theme() { + $path = drupal_get_path('module', 'views_ui'); + + return array( + // edit a view + 'views_ui_display_tab_setting' => array( + 'variables' => array('description' => '', 'link' => '', 'settings_links' => array(), 'overridden' => FALSE, 'defaulted' => FALSE, 'description_separator' => TRUE, 'class' => array()), + 'template' => 'views-ui-display-tab-setting', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + 'views_ui_display_tab_bucket' => array( + 'render element' => 'element', + 'template' => 'views-ui-display-tab-bucket', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + 'views_ui_edit_item' => array( + 'variables' => array('type' => NULL, 'view' => NULL, 'display' => NULL, 'no_fields' => FALSE), + 'template' => 'views-ui-edit-item', + 'path' => "$path/theme", + ), + 'views_ui_rearrange_form' => array( + 'render element' => 'form', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + 'views_ui_rearrange_filter_form' => array( + 'render element' => 'form', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + 'views_ui_expose_filter_form' => array( + 'render element' => 'form', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + + // list views + 'views_ui_view_info' => array( + 'variables' => array('view' => NULL, 'base' => NULL), + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + + // Group of filters. + 'views_ui_build_group_filter_form' => array( + 'render element' => 'form', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + + // tab themes + 'views_tabset' => array( + 'variables' => array('tabs' => NULL), + ), + 'views_tab' => array( + 'variables' => array('body' => NULL), + ), + 'views_ui_reorder_displays_form' => array( + 'render element' => 'form', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + + // On behalf of a plugin + 'views_ui_style_plugin_table' => array( + 'render element' => 'form', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + + // When previewing a view. + 'views_ui_view_preview_section' => array( + 'variables' => array('view' => NULL, 'section' => NULL, 'content' => NULL, 'links' => ''), + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + + // Generic container wrapper, to use instead of theme_container when an id + // is not desired. + 'views_ui_container' => array( + 'render element' => 'element', + 'path' => "$path/theme", + 'file' => 'theme.inc', + ), + ); +} + +/** + * Impements hook_custom_theme() + */ +function views_ui_custom_theme() { + $theme = config('views.settings')->get('ui.custom_theme'); + + if ($theme != '_default') { + $available = list_themes(); + + if (isset($available[$theme]) && $available[$theme]->status && preg_match('/^admin\/structure\/views/', current_path())) { + return $theme; + } + } +} + +/** + * Page title callback for the Edit View page. + */ +function views_ui_edit_page_title(ViewUI $view) { + module_load_include('inc', 'views_ui', 'admin'); + $bases = views_fetch_base_tables(); + $name = $view->storage->getHumanName(); + if (isset($bases[$view->storage->base_table])) { + $name .= ' (' . $bases[$view->storage->base_table]['title'] . ')'; + } + + return $name; +} + +/** + * Specialized menu callback to load a view and check its locked status. + * + * @param $name + * The machine name of the view. + * + * @return + * The view object, with a "locked" property indicating whether or not + * someone else is already editing the view. + */ +function views_ui_cache_load($name) { + $views_temp_store = drupal_container()->get('user.tempstore')->get('views'); + $view = $views_temp_store->get($name); + $storage = entity_load('view', $name); + $original_view = $storage ? new ViewUI($storage) : NULL; + + if (empty($view)) { + $view = $original_view; + if (!empty($view)) { + // Check to see if someone else is already editing this view. + // Set a flag to indicate that this view is being edited. + // This flag will be used e.g. to determine whether strings + // should be localized. + $view->editing = TRUE; + } + } + else { + // Keep disabled/enabled status real. + if ($original_view) { + $view->storage->disabled = $original_view->storage->disabled; + } + } + + if (empty($view)) { + return FALSE; + } + $view->locked = $views_temp_store->getMetadata($view->storage->name); + + return $view; +} + +/** + * Specialized cache function to add a flag to our view, include an appropriate + * include, and cache more easily. + */ +function views_ui_cache_set(ViewUI $view) { + if (isset($view->locked) && is_object($view->locked) && $view->locked->owner != $GLOBALS['user']->uid) { + drupal_set_message(t('Changes cannot be made to a locked view.'), 'error'); + return; + } + + $view->changed = TRUE; // let any future object know that this view has changed. + + if (isset($view->current_display)) { + // Add the knowledge of the changed display, too. + $view->changed_display[$view->current_display] = TRUE; + unset($view->current_display); + } + + // Unset handlers; we don't want to write these into the cache + unset($view->display_handler); + unset($view->default_display); + $view->query = NULL; + $view->displayHandlers = array(); + drupal_container()->get('user.tempstore')->get('views')->set($view->storage->name, $view); +} + +/** + * Theme preprocess for views-view.tpl.php. + */ +function views_ui_preprocess_views_view(&$vars) { + $view = $vars['view']; + if (!empty($view->views_ui_context) && module_exists('contextual')) { + $view->hide_admin_links = TRUE; + foreach (array('title', 'header', 'exposed', 'rows', 'pager', 'more', 'footer', 'empty', 'attachment_after', 'attachment_before') as $section) { + if (!empty($vars[$section])) { + $vars[$section] = array( + '#theme' => 'views_ui_view_preview_section', + '#view' => $view, + '#section' => $section, + '#content' => is_array($vars[$section]) ? drupal_render($vars[$section]) : $vars[$section], + '#theme_wrappers' => array('views_ui_container'), + '#attributes' => array('class' => 'contextual-region'), + ); + $vars[$section] = drupal_render($vars[$section]); + } + } + } +} + +/** + * Returns contextual links for each handler of a certain section. + * + * @TODO + * Bring in relationships + * Refactor this function to use much stuff of views_ui_edit_form_get_bucket. + * + * @param $title + * Add a bolded title of this section. + */ +function views_ui_view_preview_section_handler_links(ViewUI $view, $type, $title = FALSE) { + $display = $view->display_handler->display; + $handlers = $view->display_handler->getHandlers($type); + $links = array(); + + $types = ViewExecutable::viewsHandlerTypes(); + if ($title) { + $links[$type . '-title'] = array( + 'title' => $types[$type]['title'], + ); + } + + foreach ($handlers as $id => $handler) { + $field_name = $handler->adminLabel(TRUE); + $links[$type . '-edit-' . $id] = array( + 'title' => t('Edit @section', array('@section' => $field_name)), + 'href' => "admin/structure/views/nojs/config-item/{$view->storage->name}/{$display['id']}/$type/$id", + 'attributes' => array('class' => array('views-ajax-link')), + ); + } + $links[$type . '-add'] = array( + 'title' => t('Add new'), + 'href' => "admin/structure/views/nojs/add-item/{$view->storage->name}/{$display['id']}/$type", + 'attributes' => array('class' => array('views-ajax-link')), + ); + + return $links; +} + +/** + * Returns a link to editing a certain display setting. + */ +function views_ui_view_preview_section_display_category_links(ViewUI $view, $type, $title) { + $display = $view->display_handler->display; + $links = array( + $type . '-edit' => array( + 'title' => t('Edit @section', array('@section' => $title)), + 'href' => "admin/structure/views/nojs/display/{$view->storage->name}/{$display['id']}/$type", + 'attributes' => array('class' => array('views-ajax-link')), + ), + ); + + return $links; +} + +/** + * Returns all contextual links for the main content part of the view. + */ +function views_ui_view_preview_section_rows_links(ViewUI $view) { + $display = $view->display_handler->display; + $links = array(); + $links = array_merge($links, views_ui_view_preview_section_handler_links($view, 'filter', TRUE)); + $links = array_merge($links, views_ui_view_preview_section_handler_links($view, 'field', TRUE)); + $links = array_merge($links, views_ui_view_preview_section_handler_links($view, 'sort', TRUE)); + $links = array_merge($links, views_ui_view_preview_section_handler_links($view, 'argument', TRUE)); + $links = array_merge($links, views_ui_view_preview_section_handler_links($view, 'relationship', TRUE)); + + return $links; +} + +/** + * Fetch metadata on a specific views ui wizard plugin. + * + * @param $wizard_type + * Name of a wizard, or name of a base table. + * + * @return + * An array with information about the requested wizard type. + */ +function views_ui_get_wizard($wizard_type) { + $wizard = views_get_plugin_definition('wizard', $wizard_type); + // @todo - handle this via an alter hook instead. + if (!$wizard) { + // Must be a base table using the default wizard plugin. + $base_tables = views_fetch_base_tables(); + if (!empty($base_tables[$wizard_type])) { + $wizard = views_ui_views_wizard_defaults(); + $wizard['base_table'] = $wizard_type; + $wizard['title'] = $base_tables[$wizard_type]['title']; + } + } + return $wizard; +} + +/** + * Fetch metadata for all content_type plugins. + * + * @return + * An array of arrays with information about all available views wizards. + */ +function views_ui_get_wizards() { + $wizard_plugins = views_get_plugin_definitions('wizard'); + $wizard_tables = array(); + foreach ($wizard_plugins as $name => $info) { + $wizard_tables[$info['base_table']] = TRUE; + } + $base_tables = views_fetch_base_tables(); + $default_wizard = views_ui_views_wizard_defaults(); + // Find base tables with no wizard. + // @todo - handle this via an alter hook for plugins? + foreach ($base_tables as $table => $info) { + if (!isset($wizard_tables[$table])) { + $wizard = $default_wizard; + $wizard['title'] = $info['title']; + $wizard['base_table'] = $table; + $wizard_plugins[$table] = $wizard; + } + } + return $wizard_plugins; +} + +/** + * Helper function to define the default values for a Views wizard plugin. + * + * @return + * An array of defaults for a views wizard. + */ +function views_ui_views_wizard_defaults() { + return array( + // The children may, for example, be a different variant for each node type. + 'get children' => NULL, + 'get child' => NULL, + // title and base table must be populated. They are empty here just + // so they are documented. + 'title' => '', + 'base_table' => NULL, + ); +} + +function views_ui_get_form_wizard_instance($wizard) { + return views_get_plugin('wizard', $wizard['name']); +} + +/** + * Implements hook_views_plugins_display_alter(). + */ +function views_ui_views_plugins_display_alter(&$plugins) { + // Attach contextual links to each display plugin. The links will point to + // paths underneath "admin/structure/views/view/{$view->storage->name}" (i.e., paths + // for editing and performing other contextual actions on the view). + foreach ($plugins as &$display) { + $display['contextual links']['views_ui'] = array( + 'parent path' => 'admin/structure/views/view', + 'argument properties' => array('name'), + ); + } +} + +/** + * Implements hook_contextual_links_view_alter(). + */ +function views_ui_contextual_links_view_alter(&$element, $items) { + // Remove contextual links from being rendered, when so desired, such as + // within a View preview. + if (views_ui_contextual_links_suppress()) { + $element['#links'] = array(); + } + // Append the display ID to the Views UI edit links, so that clicking on the + // contextual link takes you directly to the correct display tab on the edit + // screen. + elseif (!empty($element['#links']['views-ui-edit']) && !empty($element['#element']['#views_contextual_links_info']['views_ui']['view_display_id'])) { + $display_id = $element['#element']['#views_contextual_links_info']['views_ui']['view_display_id']; + $element['#links']['views-ui-edit']['href'] .= '/' . $display_id; + } +} + +/** + * Sets a static variable for controlling whether contextual links are rendered. + * + * @see views_ui_contextual_links_view_alter() + */ +function views_ui_contextual_links_suppress($set = NULL) { + $suppress = &drupal_static(__FUNCTION__); + if (isset($set)) { + $suppress = $set; + } + return $suppress; +} + +/** + * Increments the views_ui_contextual_links_suppress() static variable. + * + * When this function is added to the #pre_render of an element, and + * 'views_ui_contextual_links_suppress_pop' is added to the #post_render of the + * same element, then all contextual links within the element and its + * descendants are suppressed from being rendered. This is used, for example, + * during a View preview, when it is not desired for nodes in the Views result + * to have contextual links. + * + * @see views_ui_contextual_links_suppress_pop() + */ +function views_ui_contextual_links_suppress_push() { + views_ui_contextual_links_suppress(((int) views_ui_contextual_links_suppress())+1); +} + +/** + * Decrements the views_ui_contextual_links_suppress() static variable. + * + * @see views_ui_contextual_links_suppress_push() + */ +function views_ui_contextual_links_suppress_pop() { + views_ui_contextual_links_suppress(((int) views_ui_contextual_links_suppress())-1); +} + +/** + * Menu callback; handles AJAX form submissions similar to ajax_form_callback(), but can be used for uncached forms. + * + * ajax_form_callback(), the menu callback for the system/ajax path, requires + * the form to be retrievable from the form cache, because it lacks a trusted + * $form_id argument with which to call drupal_retrieve_form(). When AJAX is + * wanted on a non-cacheable form, #ajax['path'] can be set to a path whose + * menu router item's 'page callback' is this function, and whose + * 'page arguments' is the form id, optionally followed by additional build + * arguments, as expected by drupal_get_form(). + * + * The same caution must be used when defining a hook_menu() entry with this + * page callback as is used when defining a hook_menu() entry with the + * 'drupal_get_form' page callback: a 'page arguments' must be specified with a + * literal value as the first argument, because $form_id determines which form + * builder function gets called, so must be safe from user tampering. + * + * @see drupal_get_form() + * @see ajax_form_callback() + * @see http://drupal.org/node/774876 + */ +function views_ui_ajax_get_form($form_id) { + // @see ajax_get_form() + $form_state = array( + 'no_redirect' => TRUE, + ); + $form_state['rebuild_info']['copy']['#build_id'] = TRUE; + $form_state['rebuild_info']['copy']['#action'] = TRUE; + + // @see drupal_get_form() + $args = func_get_args(); + array_shift($args); + $form_state['build_info']['args'] = $args; + $form = drupal_build_form($form_id, $form_state); + + // @see ajax_form_callback() + if (!empty($form_state['triggering_element'])) { + $callback = $form_state['triggering_element']['#ajax']['callback']; + } + if (!empty($callback) && function_exists($callback)) { + return $callback($form, $form_state); + } +} + +/** + * This is part of a patch to address a jQueryUI bug. The bug is responsible + * for the inability to scroll a page when a modal dialog is active. If the content + * of the dialog extends beyond the bottom of the viewport, the user is only able + * to scroll with a mousewheel or up/down keyboard keys. + * + * @see http://bugs.jqueryui.com/ticket/4671 + * @see https://bugs.webkit.org/show_bug.cgi?id=19033 + * @see /js/jquery.ui.dialog.patch.js + * @see /js/jquery.ui.dialog.min.js + * + * The javascript patch overwrites the $.ui.dialog.overlay.events object to remove + * the mousedown, mouseup and click events from the list of events that are bound + * in $.ui.dialog.overlay.create. + */ + +function views_ui_library_alter(&$libraries, $module) { + if ($module == 'system' && isset($libraries['jquery.ui.dialog'])) { + if (version_compare($libraries['jquery.ui.dialog']['version'], '1.7.2', '>=')) { + $libraries['jquery.ui.dialog']['js'][drupal_get_path('module', 'views') . '/js/jquery.ui.dialog.patch.js'] = array(); + } + } +} + +/** + * Implements hook_views_analyze(). + * + * This is the basic views analysis that checks for very minimal problems. + * There are other analysis tools in core specific sections, such as + * node.views.inc as well. + */ +function views_ui_views_analyze($view) { + $ret = array(); + // Check for something other than the default display: + if (count($view->displayHandlers) < 2) { + $ret[] = Analyzer::formatMessage(t('This view has only a default display and therefore will not be placed anywhere on your site; perhaps you want to add a page or a block display.'), 'warning'); + } + // You can give a page display the same path as an alias existing in the + // system, so the alias will not work anymore. Report this to the user, + // because he probably wanted something else. + foreach ($view->displayHandlers as $display) { + if (empty($display)) { + continue; + } + if ($display->hasPath() && $path = $display->getOption('path')) { + $normal_path = drupal_get_normal_path($path); + if ($path != $normal_path) { + $ret[] = Analyzer::formatMessage(t('You have configured display %display with a path which is an path alias as well. This might lead to unwanted effects so better use an internal path.', array('%display' => $display['display_title'])), 'warning'); + } + } + } + + return $ret; +} + +/** + * Truncate strings to a set length and provide a ... if they truncated. + * + * This is often used in the UI to ensure long strings fit. + */ +function views_ui_truncate($string, $length) { + if (drupal_strlen($string) > $length) { + $string = drupal_substr($string, 0, $length); + $string .= '...'; + } + + return $string; +} + +/** + * Magic load function. Wrapper to load a view. + */ +function views_ui_load($name) { + return views_get_view($name); +} + +/** + * Page callback: Calls a method on a view and reloads the listing page. + * + * @param Drupal\views\ViewExectuable $view + * The config entity being acted upon. + * @param string $op + * The operation to perform, e.g., 'enable' or 'disable'. + * + * @return mixed + * Either returns the listing page as JSON, or calls drupal_goto() to + * redirect back to the listing page. + */ +function views_ui_ajax_callback(ViewExecutable $view, $op) { + // Perform the operation. + $view->storage->$op(); + + // If the request is via AJAX, return the rendered list as JSON. + if (drupal_container()->get('request')->request->get('js')) { + $list = entity_list_controller('view')->render(); + $commands = array(ajax_command_replace('#views-entity-list', drupal_render($list))); + return new JsonResponse(ajax_render($commands)); + } + // Otherwise, redirect back to the page. + else { + $entity_info = entity_get_info('view'); + drupal_goto($entity_info['list path']); + } +} + +/** + * Page callback: Lists all of the views. + * + * @return array + * A render array for a page containing a list of views. + * + * @see views_ui_menu() + */ +function views_ui_list_page() { + return entity_list_controller('view')->render(); +}