Commit 94d95e5b authored by alexpott's avatar alexpott

Issue #2154957 by ParisLiakos, xjm: Add OPML functionality to views.

parent c20145fe
......@@ -196,6 +196,23 @@ function aggregator_views_data() {
),
);
$data['aggregator_feed']['url'] = array(
'title' => t('URL'),
'help' => t('The fully-qualified URL of the feed.'),
'field' => array(
'id' => 'url',
),
'argument' => array(
'id' => 'string',
),
'sort' => array(
'id' => 'standard',
),
'filter' => array(
'id' => 'string',
),
);
$data['aggregator_feed']['link'] = array(
'title' => t('Link'),
'help' => t('The link to the source URL of the feed.'),
......
<?php
/**
* @file
* Contains \Drupal\views\Plugin\views\row\OpmlFields.
*/
namespace Drupal\views\Plugin\views\row;
/**
* Renders an OPML item based on fields.
*
* @ViewsRow(
* id = "opml_fields",
* title = @Translation("OPML fields"),
* help = @Translation("Display fields as OPML items."),
* theme = "views_view_row_opml",
* display_types = {"feed"}
* )
*/
class OpmlFields extends RowPluginBase {
/**
* Does the row plugin support to add fields to it's output.
*
* @var bool
*/
protected $usesFields = TRUE;
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['text_field'] = array('default' => '');
$options['created_field'] = array('default' => '');
$options['type_field'] = array('default' => '');
$options['description_field'] = array('default' => '');
$options['html_url_field'] = array('default' => '');
$options['language_field'] = array('default' => '');
$options['xml_url_field'] = array('default' => '');
$options['url_field'] = array('default' => '');
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, &$form_state) {
parent::buildOptionsForm($form, $form_state);
$initial_labels = array('' => $this->t('- None -'));
$view_fields_labels = $this->displayHandler->getFieldLabels();
$view_fields_labels = array_merge($initial_labels, $view_fields_labels);
$types = array(
'rss' => $this->t('RSS'),
'link' => $this->t('Link'),
'include' => $this->t('Include'),
);
$types = array_merge($initial_labels, $types);
$form['type_field'] = array(
'#type' => 'select',
'#title' => $this->t('Type attribute'),
'#description' => $this->t('The type of this row.'),
'#options' => $types,
'#default_value' => $this->options['type_field'],
);
$form['text_field'] = array(
'#type' => 'select',
'#title' => $this->t('Text attribute'),
'#description' => $this->t('The field that is going to be used as the OPML text attribute for each row.'),
'#options' => $view_fields_labels,
'#default_value' => $this->options['text_field'],
'#required' => TRUE,
);
$form['created_field'] = array(
'#type' => 'select',
'#title' => $this->t('Created attribute'),
'#description' => $this->t('The field that is going to be used as the OPML created attribute for each row.'),
'#options' => $view_fields_labels,
'#default_value' => $this->options['created_field'],
);
$form['description_field'] = array(
'#type' => 'select',
'#title' => $this->t('Description attribute'),
'#description' => $this->t('The field that is going to be used as the OPML description attribute for each row.'),
'#options' => $view_fields_labels,
'#default_value' => $this->options['description_field'],
'#states' => array(
'visible' => array(
':input[name="row_options[type_field]"]' => array('value' => 'rss'),
),
),
);
$form['html_url_field'] = array(
'#type' => 'select',
'#title' => $this->t('HTML URL attribute'),
'#description' => $this->t('The field that is going to be used as the OPML htmlUrl attribute for each row.'),
'#options' => $view_fields_labels,
'#default_value' => $this->options['html_url_field'],
'#states' => array(
'visible' => array(
':input[name="row_options[type_field]"]' => array('value' => 'rss'),
),
),
);
$form['language_field'] = array(
'#type' => 'select',
'#title' => $this->t('Language attribute'),
'#description' => $this->t('The field that is going to be used as the OPML language attribute for each row.'),
'#options' => $view_fields_labels,
'#default_value' => $this->options['language_field'],
'#states' => array(
'visible' => array(
':input[name="row_options[type_field]"]' => array('value' => 'rss'),
),
),
);
$form['xml_url_field'] = array(
'#type' => 'select',
'#title' => $this->t('XML URL attribute'),
'#description' => $this->t('The field that is going to be used as the OPML text attribute for each row.'),
'#options' => $view_fields_labels,
'#default_value' => $this->options['xml_url_field'],
'#states' => array(
'visible' => array(
':input[name="row_options[type_field]"]' => array('value' => 'rss'),
),
),
);
$form['url_field'] = array(
'#type' => 'select',
'#title' => $this->t('URL attribute'),
'#description' => $this->t('The field that is going to be used as the OPML url attribute for each row.'),
'#options' => $view_fields_labels,
'#default_value' => $this->options['url_field'],
'#states' => array(
'visible' => array(
':input[name="row_options[type_field]"]' => array(
array('value' => 'link'),
array('value' => 'include'),
),
),
),
);
}
/**
* {@inheritdoc}
*/
public function validate() {
$errors = parent::validate();
if (empty($this->options['text_field'])) {
$errors[] = $this->t('Row style plugin requires specifying which views field to use for OPML text attribute.');
}
if (!empty($this->options['type_field'])) {
if ($this->options['type_field'] == 'rss') {
if (empty($this->options['xml_url_field'])) {
$errors[] = $this->t('Row style plugin requires specifying which views field to use for XML URL attribute.');
}
}
elseif (in_array($this->options['type_field'], array('link', 'include'))) {
if (empty($this->options['url_field'])) {
$errors[] = $this->t('Row style plugin requires specifying which views field to use for URL attribute.');
}
}
}
return $errors;
}
/**
* {@inheritdoc}
*/
public function render($row) {
// Create the OPML item array.
$item = array();
$row_index = $this->view->row_index;
$item['text'] = $this->getField($row_index, $this->options['text_field']);
$item['created'] = $this->getField($row_index, $this->options['created_field']);
if ($this->options['type_field']) {
$item['type'] = $this->options['type_field'];
if ($item['type'] == 'rss') {
$item['description'] = $this->getField($row_index, $this->options['description_field']);
$item['language'] = $this->getField($row_index, $this->options['language_field']);
$item['xmlUrl'] = $this->getField($row_index, $this->options['xml_url_field']);
$item['htmlUrl'] = $this->getField($row_index, $this->options['html_url_field']);
}
else {
$item['url'] = $this->getField($row_index, $this->options['url_field']);
}
}
// Remove empty attributes.
$item = array_filter($item);
$build = array(
'#theme' => $this->themeFunctions(),
'#view' => $this->view,
'#options' => $this->options,
'#row' => $item,
'#field_alias' => isset($this->field_alias) ? $this->field_alias : '',
);
return $build;
}
/**
* Retrieves a views field value from the style plugin.
*
* @param $index
* The index count of the row as expected by views_plugin_style::getField().
* @param $field_id
* The ID assigned to the required field in the display.
*/
public function getField($index, $field_id) {
if (empty($this->view->style_plugin) || !is_object($this->view->style_plugin) || empty($field_id)) {
return '';
}
return $this->view->style_plugin->getField($index, $field_id);
}
}
<?php
/**
* @file
* Contains \Drupal\views\Plugin\views\style\Opml.
*/
namespace Drupal\views\Plugin\views\style;
/**
* Default style plugin to render an OPML feed.
*
* @ingroup views_style_plugins
*
* @ViewsStyle(
* id = "opml",
* title = @Translation("OPML Feed"),
* help = @Translation("Generates an OPML feed from a view."),
* theme = "views_view_opml",
* display_types = {"feed"}
* )
*/
class Opml extends StylePluginBase {
/**
* Does the style plugin for itself support to add fields to its output.
*
* @var bool
*/
protected $usesRowPlugin = TRUE;
/**
* {@inheritdoc}
*/
public function attachTo($display_id, $path, $title) {
$display = $this->view->displayHandlers->get($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)) {
$build['#attached']['drupal_add_feed'][] = array($url, $title);
drupal_render($build);
}
}
else {
if (empty($this->view->feed_icon)) {
$this->view->feed_icon = '';
}
$feed_icon = array(
'#theme' => 'feed_icon',
'#url' => $url,
'#title' => $title,
);
$this->view->feed_icon .= drupal_render($feed_icon);
}
}
/**
* {@inheritdoc}
*/
public function render() {
if (empty($this->view->rowPlugin)) {
debug('Drupal\views\Plugin\views\style\Opml: Missing row plugin');
return;
}
$rows = array();
foreach ($this->view->result as $row_index => $row) {
$this->view->row_index = $row_index;
$rows[] = $this->view->rowPlugin->render($row);
}
$build = array(
'#theme' => $this->themeFunctions(),
'#view' => $this->view,
'#options' => $this->options,
'#rows' => $rows,
);
unset($this->view->row_index);
return drupal_render($build);
}
}
......@@ -2,7 +2,7 @@
/**
* @file
* Definition of Drupal\views\Tests\Plugin\DisplayFeedTest.
* Contains \Drupal\views\Tests\Plugin\DisplayFeedTest.
*/
namespace Drupal\views\Tests\Plugin;
......@@ -26,7 +26,7 @@ class DisplayFeedTest extends PluginTestBase {
*
* @var array
*/
public static $modules = array('block', 'node', 'views_ui');
public static $modules = array('block', 'node', 'views');
public static function getInfo() {
return array(
......@@ -41,47 +41,10 @@ protected function setUp() {
$this->enableViewsTestModule();
$admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration'));
$admin_user = $this->drupalCreateUser(array('administer site configuration'));
$this->drupalLogin($admin_user);
}
/**
* Tests feed display admin ui.
*/
public function testFeedUI() {
$this->drupalGet('admin/structure/views');
// Verify that the page lists the test_display_feed view.
// Regression test: ViewListBuilder::getDisplayPaths() did not properly
// check whether a DisplayBag was returned in iterating over all displays.
$this->assertText('test_display_feed');
// Check the attach TO interface.
$this->drupalGet('admin/structure/views/nojs/display/test_display_feed/feed_1/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', 'page'), 'Make sure all displays appears as expected.');
// Post and save this and check the output.
$this->drupalPostForm('admin/structure/views/nojs/display/test_display_feed/feed_1/displays', array('displays[page]' => 'page'), t('Apply'));
$this->drupalGet('admin/structure/views/view/test_display_feed/edit/feed_1');
$this->assertFieldByXpath('//*[@id="views-feed-1-displays"]', 'Page');
// Add the default display, so there should now be multiple displays.
$this->drupalPostForm('admin/structure/views/nojs/display/test_display_feed/feed_1/displays', array('displays[default]' => 'default'), t('Apply'));
$this->drupalGet('admin/structure/views/view/test_display_feed/edit/feed_1');
$this->assertFieldByXpath('//*[@id="views-feed-1-displays"]', 'Multiple displays');
}
/**
* Tests the rendered output.
*/
......
<?php
/**
* @file
* Contains \Drupal\views\Tests\Plugin\StyleOpmlTest.
*/
namespace Drupal\views\Tests\Plugin;
/**
* Tests the OPML feed style plugin.
*
* @see \Drupal\views\Plugin\views\style\Opml
*/
class StyleOpmlTest extends PluginTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_style_opml');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('aggregator');
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Style: OPML',
'description' => 'Tests the OPML feed style plugin.',
'group' => 'Views Plugins',
);
}
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->enableViewsTestModule();
$admin_user = $this->drupalCreateUser(array('administer news feeds'));
$this->drupalLogin($admin_user);
}
/**
* Tests the rendered output.
*/
public function testOpmlOutput() {
// Create a test feed.
$values = array(
'title' => $this->randomName(10),
'url' => 'http://example.com/rss.xml',
'refresh' => '900',
);
$feed = $this->container->get('entity.manager')
->getStorage('aggregator_feed')
->create($values);
$feed->save();
$this->drupalGet('test-feed-opml-style');
$outline = $this->xpath('//outline[1]');
$this->assertEqual($outline[0]['type'], 'rss', 'The correct type attribute is used for rss OPML.');
$this->assertEqual($outline[0]['text'], $feed->label(), 'The correct text attribute is used for rss OPML.');
$this->assertEqual($outline[0]['xmlurl'], $feed->getUrl(), 'The correct xmlUrl attribute is used for rss OPML.');
$view = $this->container->get('entity.manager')
->getStorage('view')
->load('test_style_opml');
$display = &$view->getDisplay('feed_1');
$display['display_options']['row']['options']['type_field'] = 'link';
$display['display_options']['row']['options']['url_field'] = 'url';
$view->save();
$this->drupalGet('test-feed-opml-style');
$outline = $this->xpath('//outline[1]');
$this->assertEqual($outline[0]['type'], 'link', 'The correct type attribute is used for link OPML.');
$this->assertEqual($outline[0]['text'], $feed->label(), 'The correct text attribute is used for link OPML.');
$this->assertEqual($outline[0]['url'], $feed->getUrl(), 'The correct url attribute is used for link OPML.');
// xmlUrl should not be present when type is link.
$this->assertNull($outline[0]['xmlUrl'], 'The xmlUrl attribute is not used for link OPML.');
}
}
{#
/**
* @file
* Default template for feed displays that use the OPML style.
*
* Available variables:
* - title: The title of the feed (as set in the view).
* - updated: The modified date of the feed.
* - items: The feed items themselves.
*
* @see template_preprocess_views_view_opml()
*
* @ingroup themeable
*/
#}
<?xml version="1.0" encoding="utf-8" ?>
<opml version="2.0">
<head>
<title>{{ title }}</title>
<dateModified>{{ updated }}</dateModified>
</head>
<body>
{{ items }}
</body>
</opml>
{#
/**
* @file
* Default theme implementation to display an item in a views OPML feed.
*
* Available variables:
* - attributes: Attributes for outline element.
*
* @see template_preprocess_views_view_row_opml()
*
* @ingroup themeable
*/
#}
<outline{{ attributes }}/>
base_field: fid
base_table: aggregator_feed
core: 8
description: ""
status: true
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
provider: views
display_options:
access:
type: none
cache:
type: none
query:
type: views_query
exposed_form:
type: basic
pager:
type: full
options:
items_per_page: 10
style:
type: default
row:
type: entity:aggregator_feed
fields:
title:
id: title
table: aggregator_feed
field: title
relationship: none
group_type: group
admin_label: ""
dependencies:
module:
- aggregator
- aggregator
- aggregator
label: Title
exclude: false
alter:
alter_text: false
text: ""
make_link: false
path: ""
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ""
rel: ""
link_class: ""
prefix: ""
suffix: ""
target: ""
nl2br: false
max_length: ""
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ""
more_link_path: ""
strip_tags: false
trim: false
preserve_tags: ""
html: false
element_type: ""
element_class: ""
element_label_type: ""
element_label_class: ""
element_label_colon: true
element_wrapper_type: ""
element_wrapper_class: ""
element_default_classes: true
empty: ""
hide_empty: false
empty_zero: false
hide_alter_empty: true
display_as_link: false
plugin_id: aggregator_title_link
provider: aggregator
url:
id: url
table: aggregator_feed
field: url
relationship: none
group_type: group
admin_label: ""
dependencies:
module:
- views
label: URL
exclude: false
alter:
alter_text: false
text: ""
make_link: false
path: ""
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ""
rel: ""
link_class: ""
prefix: ""
suffix: ""
target: ""
nl2br: false
max_length: ""
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ""
more_link_path: ""
strip_tags: false
trim: false
preserve_tags: ""
html: false
element_type: ""
element_class: ""
element_label_type: ""
element_label_class: ""
element_label_colon: true
element_wrapper_type: ""
element_wrapper_class: ""
element_default_classes: true
empty: ""
hide_empty: false
empty_zero: false
hide_alter_empty: true
display_as_link: false
plugin_id: url
provider: views
description:
id: description
table: aggregator_feed
field: description
relationship: none
group_type: group
admin_label: ""
dependencies:
module:
- views
label: Description
exclude: false
alter:
alter_text: false
text: ""
make_link: false
path: ""
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ""
rel: ""
link_class: ""