Commit 3d64cb5e authored by Dries's avatar Dries

- Patch #372743 by bjaspan, yched, KarenS, catch et al: node body and teasers as fields. Oh, my.

parent bfdea953
......@@ -110,6 +110,8 @@ Drupal 7.0, xxxx-xx-xx (development version)
for RDFa support.
- Field API:
* Custom data fields may be attached to nodes and users in Drupal.
* Node bodies and teasers are now Field API fields instead of
being a hard-coded property of node objects.
* In addition, any other object type may register with Field API
and allow custom data fields to be attached to itself.
* Provides a subset of the features of the Content Construction
......
......@@ -378,11 +378,11 @@ function theme_aggregator_page_rss($feeds, $category = NULL) {
foreach ($feeds as $feed) {
switch ($feed_length) {
case 'teaser':
$teaser = node_teaser($feed->description, NULL, variable_get('aggregator_teaser_length', 600));
if ($teaser != $feed->description) {
$teaser .= '<p><a href="' . check_url($feed->link) . '">' . t('read more') . "</a></p>\n";
$summary = text_summary($feed->description, NULL, variable_get('aggregator_teaser_length', 600));
if ($summary != $feed->description) {
$summary .= '<p><a href="' . check_url($feed->link) . '">' . t('read more') . "</a></p>\n";
}
$feed->description = $teaser;
$feed->description = $summary;
break;
case 'title':
$feed->description = '';
......
......@@ -252,7 +252,7 @@ EOF;
for ($i = 0; $i < 5; $i++) {
$edit = array();
$edit['title'] = $this->randomName();
$edit['body'] = $this->randomName();
$edit['body[0][value]'] = $this->randomName();
$this->drupalPost('node/add/article', $edit, t('Save'));
}
}
......
......@@ -73,12 +73,7 @@ function blog_help($path, $arg) {
* Implement hook_form().
*/
function blog_form($node, $form_state) {
global $nid;
$type = node_type_get_type($node);
$form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => !empty($node->title) ? $node->title : NULL, '#weight' => -5);
$form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);
return $form;
return node_content_form($node, $form_state);
}
/**
......@@ -89,8 +84,7 @@ function blog_view($node, $teaser) {
// Breadcrumb navigation.
drupal_set_breadcrumb(array(l(t('Home'), NULL), l(t('Blogs'), 'blog'), l(t("!name's blog", array('!name' => $node->name)), 'blog/' . $node->uid)));
}
return node_prepare($node, $teaser);
return $node;
}
/**
......
......@@ -154,7 +154,7 @@ class BlogTestCase extends DrupalWebTestCase {
// Edit blog node.
$edit = array();
$edit['title'] = 'node/' . $node->nid;
$edit['body'] = $this->randomName(256);
$edit['body[0][value]'] = $this->randomName(256);
$this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
$this->assertRaw(t('Blog entry %title has been updated.', array('%title' => $edit['title'])), t('Blog node was edited'));
......
......@@ -213,7 +213,7 @@ function blogapi_blogger_new_post($appkey, $blogid, $username, $password, $conte
}
else {
$edit['title'] = blogapi_blogger_title($content);
$edit['body'] = $content;
$edit['body'][0]['value'] = $content;
}
if (!node_access('create', $edit['type'])) {
......@@ -274,12 +274,12 @@ function blogapi_blogger_edit_post($appkey, $postid, $username, $password, $cont
// Check for bloggerAPI vs. metaWeblogAPI.
if (is_array($content)) {
$node->title = $content['title'];
$node->body = $content['description'];
$node->body[0]['value'] = $content['description'];
_blogapi_mt_extra($node, $content);
}
else {
$node->title = blogapi_blogger_title($content);
$node->body = $content;
$node->body[0]['value'] = $content;
}
module_invoke_all('node_blogapi_edit', $node);
......@@ -379,7 +379,7 @@ function blogapi_blogger_get_recent_posts($appkey, $blogid, $username, $password
}
if ($bodies) {
$result = db_query_range("SELECT n.nid, n.title, r.body, r.format, n.comment, n.created, u.name FROM {node} n, {node_revision} r, {users} u WHERE n.uid = u.uid AND n.vid = r.vid AND n.type = :type AND n.uid = :uid ORDER BY n.created DESC", array(
$result = db_query_range("SELECT n.nid, n.title, n.comment, n.created, u.name FROM {node} n, {node_revision} r, {users} u WHERE n.uid = u.uid AND n.vid = r.vid AND n.type = :type AND n.uid = :uid ORDER BY n.created DESC", array(
':type' => $blogid,
':uid' => $user->uid
), 0, $number_of_posts);
......@@ -892,14 +892,14 @@ function _blogapi_mt_extra($node, $struct) {
// Merge the 3 body sections (description, mt_excerpt, mt_text_more) into one body.
if ($struct['mt_excerpt']) {
$node->body = $struct['mt_excerpt'] . '<!--break-->' . $node->body;
$node->body[0]['value'] = $struct['mt_excerpt'] . '<!--break-->' . $node->body[0]['value'];
}
if ($struct['mt_text_more']) {
$node->body = $node->body . '<!--extended-->' . $struct['mt_text_more'];
$node->body[0]['value'] = $node->body[0]['value'] . '<!--extended-->' . $struct['mt_text_more'];
}
if ($struct['mt_convert_breaks']) {
$node->format = $struct['mt_convert_breaks'];
$node->body[0]['format'] = $struct['mt_convert_breaks'];
}
if ($struct['dateCreated']) {
......@@ -922,17 +922,19 @@ function _blogapi_get_post($node, $bodies = TRUE) {
);
if ($bodies) {
$body = $node->body[0]['value'];
$format = $node->body[0]['format'];
if ($node->comment == 1) {
$comment = 2;
}
elseif ($node->comment == 2) {
$comment = 1;
}
$xmlrpcval['content'] = "<title>$node->title</title>$node->body";
$xmlrpcval['description'] = $node->body;
$xmlrpcval['content'] = "<title>$node->title</title>$body";
$xmlrpcval['description'] = $body;
// Add MT specific fields
$xmlrpcval['mt_allow_comments'] = (int) $comment;
$xmlrpcval['mt_convert_breaks'] = $node->format;
$xmlrpcval['mt_convert_breaks'] = $format;
}
return $xmlrpcval;
......
......@@ -1037,7 +1037,7 @@ function book_export_traverse($tree, $visit_func) {
function book_node_export($node, $children = '') {
$node->build_mode = NODE_BUILD_PRINT;
$node = node_build_content($node, FALSE, FALSE);
$node->body = drupal_render($node->content);
$node->rendered = drupal_render($node->content);
return theme('book_node_export_html', $node, $children);
}
......@@ -1054,7 +1054,7 @@ function book_node_export($node, $children = '') {
function template_preprocess_book_node_export_html(&$variables) {
$variables['depth'] = $variables['node']->book['depth'];
$variables['title'] = check_plain($variables['node']->title);
$variables['content'] = $variables['node']->body;
$variables['content'] = $variables['node']->rendered;
}
/**
......
......@@ -141,8 +141,7 @@ class BookTestCase extends DrupalWebTestCase {
// Check printer friendly version.
$this->drupalGet('book/export/html/' . $node->nid);
$this->assertText($node->title, t('Printer friendly title found.'));
$node->body = str_replace('<!--break-->', '', $node->body);
$this->assertRaw(check_markup($node->body, $node->format), t('Printer friendly body found.'));
$this->assertRaw(check_markup($node->body[0]['value'], $node->body[0]['format']), t('Printer friendly body found.'));
$number++;
}
......@@ -174,7 +173,7 @@ class BookTestCase extends DrupalWebTestCase {
$edit = array();
$edit['title'] = $number . ' - SimpleTest test node ' . $this->randomName(10);
$edit['body'] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32);
$edit['body[0][value]'] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32);
$edit['book[bid]'] = $book_nid;
if ($parent !== NULL) {
......
......@@ -327,7 +327,7 @@ class DBLogTestCase extends DrupalWebTestCase {
default:
$content = array(
'title' => $this->randomName(8),
'body' => $this->randomName(32),
'body[0][value]' => $this->randomName(32),
);
break;
}
......@@ -351,7 +351,7 @@ class DBLogTestCase extends DrupalWebTestCase {
default:
$content = array(
'body' => $this->randomName(32),
'body[0][value]' => $this->randomName(32),
);
break;
}
......
......@@ -425,7 +425,9 @@ function field_create_instance($instance) {
// Set the field id.
$instance['field_id'] = $field['id'];
// TODO: Check that the specifed bundle exists.
// Note that we do *not* prevent creating a field on non-existing bundles,
// because that would break the 'Body as field' upgrade for contrib
// node types.
// TODO: Check that the widget type is known and can handle the field type ?
// TODO: Check that the formatters are known and can handle the field type ?
......
......@@ -930,10 +930,9 @@ class FieldInfoTestCase extends DrupalWebTestCase {
$this->assertEqual($info[$w_key]['module'], 'field_test', t("Widget type field_test module appears"));
}
// Verify that no fields or instances exist
$fields = field_info_fields();
// Verify that no unexpected instances exist.
$core_fields = field_info_fields();
$instances = field_info_instances(FIELD_TEST_BUNDLE);
$this->assertTrue(empty($fields), t('With no fields, info fields is empty.'));
$this->assertTrue(empty($instances), t('With no instances, info bundles is empty.'));
// Create a field, verify it shows up.
......@@ -943,7 +942,7 @@ class FieldInfoTestCase extends DrupalWebTestCase {
);
field_create_field($field);
$fields = field_info_fields();
$this->assertEqual(count($fields), 1, t('One field exists'));
$this->assertEqual(count($fields), count($core_fields) + 1, t('One new field exists'));
$this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], t('info fields contains field name'));
$this->assertEqual($fields[$field['field_name']]['type'], $field['type'], t('info fields contains field type'));
$this->assertEqual($fields[$field['field_name']]['module'], 'field_test', t('info fields contains field module'));
......
......@@ -5,3 +5,4 @@ package = Core - fields
version = VERSION
core = 7.x
files[]=list.module
required = TRUE
......@@ -5,3 +5,4 @@ package = Core - fields
version = VERSION
core = 7.x
files[]=number.module
required = TRUE
......@@ -5,3 +5,4 @@ package = Core - fields
version = VERSION
core = 7.x
files[]=options.module
required = TRUE
......@@ -6,3 +6,4 @@ version = VERSION
core = 7.x
files[] = text.module
files[] = text.test
required = TRUE
\ No newline at end of file
......@@ -14,6 +14,9 @@ function text_theme() {
'text_textarea' => array(
'arguments' => array('element' => NULL),
),
'text_textarea_with_summary' => array(
'arguments' => array('element' => NULL),
),
'text_textfield' => array(
'arguments' => array('element' => NULL),
),
......@@ -26,11 +29,22 @@ function text_theme() {
'field_formatter_text_trimmed' => array(
'arguments' => array('element' => NULL),
),
'field_formatter_text_summary_or_trimmed' => array(
'arguments' => array('element' => NULL),
),
);
}
/**
* Implement hook_field_info().
*
* Field settings:
* - max_length: the maximum length for a varchar field.
* Instance settings:
* - text_processing: whether text input filters should be used.
* - display_summary: whether the summary field should be displayed.
* When empty and not displayed the summary will take its value from the
* trimmed value of the main text field.
*/
function text_field_info() {
return array(
......@@ -39,18 +53,25 @@ function text_field_info() {
'description' => t('This field stores varchar text in the database.'),
'settings' => array('max_length' => 255),
'instance_settings' => array('text_processing' => 0),
'widget_settings' => array('size' => 60),
'default_widget' => 'text_textfield',
'default_formatter' => 'text_default',
),
'text_long' => array(
'label' => t('Long text'),
'description' => t('This field stores long text in the database.'),
'settings' => array('max_length' => ''),
'instance_settings' => array('text_processing' => 0),
'widget_settings' => array('rows' => 5),
'default_widget' => 'text_textarea',
'default_formatter' => 'text_default',
),
'text_with_summary' => array(
'label' => t('Long text and summary'),
'description' => t('This field stores long text in the database along with optional summary text.'),
'settings' => array('max_length' => ''),
'instance_settings' => array('text_processing' => 1, 'display_summary' => 0),
'default_widget' => 'text_textarea_with_summary',
'default_formatter' => 'text_summary_or_trimmed',
),
);
}
......@@ -58,23 +79,39 @@ function text_field_info() {
* Implement hook_field_schema().
*/
function text_field_schema($field) {
if ($field['type'] == 'text_long') {
$columns = array(
'value' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
);
}
else {
$columns = array(
'value' => array(
'type' => 'varchar',
'length' => $field['settings']['max_length'],
'not null' => FALSE,
),
);
switch ($field['type']) {
case 'text':
$columns = array(
'value' => array(
'type' => 'varchar',
'length' => $field['settings']['max_length'],
'not null' => FALSE,
),
);
break;
case 'text_long':
$columns = array(
'value' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
);
break;
case 'text_with_summary':
$columns = array(
'value' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
'summary' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
);
break;
}
$columns += array(
'format' => array(
......@@ -95,16 +132,27 @@ function text_field_schema($field) {
* Implement hook_field_validate().
*
* Possible error codes:
* - 'text_max_length': The value exceeds the maximum length.
* - 'text_value_max_length': The value exceeds the maximum length.
* - 'text_summary_max_length': The summary exceeds the maximum length.
*/
function text_field_validate($obj_type, $object, $field, $instance, $items, &$errors) {
foreach ($items as $delta => $item) {
if (!empty($item['value'])) {
if (!empty($field['settings']['max_length']) && drupal_strlen($item['value']) > $field['settings']['max_length']) {
$errors[$field['field_name']][$delta][] = array(
'error' => 'text_max_length',
'message' => t('%name: the value may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length'])),
);
foreach (array('value' => t('full text'), 'summary' => t('summary')) as $column => $desc) {
if (!empty($item[$column])) {
if (!empty($field['settings']['max_length']) && drupal_strlen($item[$column]) > $field['settings']['max_length']) {
switch ($column) {
case 'value':
$message = t('%name: the text may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']));
break;
case 'summary':
$message = t('%name: the summary may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']));
break;
}
$errors[$field['field_name']][$delta][] = array(
'error' => "text_{$column}_length",
'message' => $message,
);
}
}
}
}
......@@ -126,17 +174,22 @@ function text_field_load($obj_type, $objects, $field, $instances, &$items) {
if (!empty($instances[$id]['settings']['text_processing'])) {
// Only process items with a cacheable format, the rest will be
// handled by text_field_sanitize().
if (filter_format_allowcache($item['format'])) {
$format = $item['format'];
if (filter_format_allowcache($format)) {
// TODO D7 : this code is really node-related.
$check = is_null($object) || (isset($object->build_mode) && $object->build_mode == NODE_BUILD_PREVIEW);
$text = isset($item['value']) ? check_markup($item['value'], $item['format'], isset($object->language) ? $object->language : $language->language, $check, FALSE) : '';
$lang = isset($object->language) ? $object->language : $language->language;
$items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, $check, FALSE) : '';
if ($field['type'] == 'text_with_summary') {
$items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, $check, FALSE) : '';
}
}
}
else {
$text = check_plain($item['value']);
}
if (isset($text)) {
$items[$id][$delta]['safe'] = $text;
$items[$id][$delta]['safe'] = check_plain($item['value']);
if ($field['type'] == 'text_with_summary') {
$items[$id][$delta]['safe_summary'] = check_plain($item['summary']);
}
}
}
}
......@@ -155,14 +208,21 @@ function text_field_sanitize($obj_type, $object, $field, $instance, &$items) {
// from a form preview.
if (!isset($items[$delta]['safe'])) {
if (!empty($instance['settings']['text_processing'])) {
$format = $item['format'];
// TODO D7 : this code is really node-related.
$check = is_null($object) || (isset($object->build_mode) && $object->build_mode == NODE_BUILD_PREVIEW);
$text = isset($item['value']) ? check_markup($item['value'], $item['format'], isset($object->language) ? $object->language : $language->language, $check) : '';
$lang = isset($object->language) ? $object->language : $language->language;
$items[$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, $check) : '';
if ($field['type'] == 'text_with_summary') {
$items[$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, $check) : '';
}
}
else {
$text = check_plain($item['value']);
$item[$delta]['safe'] = check_plain($item['value']);
if ($field['type'] == 'text_with_summary') {
$item[$delta]['safe_summary'] = check_plain($item['summary']);
}
}
$items[$delta]['safe'] = $text;
}
}
}
......@@ -184,21 +244,39 @@ function text_field_formatter_info() {
return array(
'text_default' => array(
'label' => t('Default'),
'field types' => array('text', 'text_long'),
'field types' => array('text', 'text_long', 'text_with_summary'),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
),
),
'text_plain' => array(
'label' => t('Plain text'),
'field types' => array('text', 'text_long'),
'field types' => array('text', 'text_long', 'text_with_summary'),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
),
),
// The text_trimmed formatter displays the trimmed version of the
// full element of the field. It is intended to be used with text
// and text_long fields. It also works with text_with_summary
// fields though the text_summary_or_trimmed formatter makes more
// sense for that field type.
'text_trimmed' => array(
'label' => t('Trimmed'),
'field types' => array('text', 'text_long'),
'field types' => array('text', 'text_long', 'text_with_summary'),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
),
),
// The 'summary or trimmed' field formatter for text_with_summary
// fields displays returns the summary element of the field or, if
// the summary is empty, the trimmed version of the full element
// of the field.
'text_summary_or_trimmed' => array(
'label' => t('Summary or trimmed'),
'field types' => array('text_with_summary'),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
),
......@@ -226,7 +304,150 @@ function theme_field_formatter_text_plain($element) {
function theme_field_formatter_text_trimmed($element) {
$field = field_info_field($element['#field_name']);
$instance = field_info_instance($element['#field_name'], $element['#bundle']);
return $instance['settings']['text_processing'] ? $element['#item']['format'] : NULL;
return text_summary($element['#item']['safe'], $instance['settings']['text_processing'] ? $element['#item']['format'] : NULL);
}
/**
* Theme function for 'summary or trimmed' field formatter for
* text_with_summary fields. This formatter returns the summary
* element of the field or, if the summary is empty, the trimmed
* version of the full element of the field.
*/
function theme_field_formatter_text_summary_or_trimmed($element) {
$field = field_info_field($element['#field_name']);
$instance = field_info_instance($element['#field_name'], $element['#bundle']);
if (!empty($element['#item']['safe_summary'])) {
return $element['#item']['safe_summary'];
}
else {
return text_summary($element['#item']['safe'], $instance['settings']['text_processing'] ? $element['#item']['format'] : NULL);
}
}
/**
* Generate a trimmed, formatted version of a text field value.
*
* If the end of the summary is not indicated using the <!--break--> delimiter
* then we generate the summary automatically, trying to end it at a sensible
* place such as the end of a paragraph, a line break, or the end of a
* sentence (in that order of preference).
*
* @param $text
* The content for which a summary will be generated.
* @param $format
* The format of the content.
* If the PHP filter is present and $text contains PHP code, we do not
* split it up to prevent parse errors.
* If the line break filter is present then we treat newlines embedded in
* $text as line breaks.
* If the htmlcorrector filter is present, it will be run on the generated
* summary (if different from the incoming $text).
* @param $size
* The desired character length of the summary. If omitted, the default
* value will be used. Ignored if the special delimiter is present
* in $text.
* @return
* The generated summary.
*/
function text_summary($text, $format = NULL, $size = NULL) {
if (!isset($size)) {
// What used to be called 'teaser' is now called 'summary', but
// the variable 'teaser_length' is preserved for backwards compatibility.
$size = variable_get('teaser_length', 600);
}
// Find where the delimiter is in the body
$delimiter = strpos($text, '<!--break-->');
// If the size is zero, and there is no delimiter, the entire body is the summary.
if ($size == 0 && $delimiter === FALSE) {
return $text;
}
// If a valid delimiter has been specified, use it to chop off the summary.
if ($delimiter !== FALSE) {
return substr($text, 0, $delimiter);
}
// We check for the presence of the PHP evaluator filter in the current
// format. If the body contains PHP code, we do not split it up to prevent
// parse errors.
if (isset($format)) {
$filters = filter_list_format($format);
if (isset($filters['php/0']) && strpos($text, '<?') !== FALSE) {
return $text;
}
}
// If we have a short body, the entire body is the summary.
if (drupal_strlen($text) <= $size) {
return $text;
}
// If the delimiter has not been specified, try to split at paragraph or
// sentence boundaries.
// The summary may not be longer than maximum length specified. Initial slice.
$summary = truncate_utf8($text, $size);
// Store the actual length of the UTF8 string -- which might not be the same
// as $size.
$max_rpos = strlen($summary);
// How much to cut off the end of the summary so that it doesn't end in the
// middle of a paragraph, sentence, or word.
// Initialize it to maximum in order to find the minimum.
$min_rpos = $max_rpos;
// Store the reverse of the summary. We use strpos on the reversed needle and
// haystack for speed and convenience.
$reversed = strrev($summary);
// Build an array of arrays of break points grouped by preference.
$break_points = array();
// A paragraph near the end of sliced summary is most preferable.
$break_points[] = array('</p>' => 0);
// If no complete paragraph then treat line breaks as paragraphs.
$line_breaks = array('<br />' => 6, '<br>' => 4);
// Newline only indicates a line break if line break converter
// filter is present.
if (isset($filters['filter/1'])) {
$line_breaks["\n"] = 1;
}
$break_points[] = $line_breaks;
// If the first paragraph is too long, split at the end of a sentence.
$break_points[] = array('. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1);
// Iterate over the groups of break points until a break point is found.
foreach ($break_points as $points) {
// Look for each break point, starting at the end of the summary.
foreach ($points as $point => $offset) {
// The summary is already reversed, but the break point isn't.
$rpos = strpos($reversed, strrev($point));
if ($rpos !== FALSE) {
$min_rpos = min($rpos + $offset, $min_rpos);
}
}
// If a break point was found in this group, slice and stop searching.
if ($min_rpos !== $max_rpos) {
// Don't slice with length 0. Length must be <0 to slice from RHS.
$summary = ($min_rpos === 0) ? $summary : substr($summary, 0, 0 - $min_rpos);
break;
}
}
// If the htmlcorrector filter is present, apply it to the generated summary.
if (isset($filters['filter/3'])) {
$summary = _filter_htmlcorrector($summary);
}
return $summary;
}