Commit f6b4d4a2 authored by Dries's avatar Dries

Issue #501428 by KarenS, swentel, tim.plunkett, webchick, effulgentsia, nod_,...

Issue #501428 by KarenS, swentel, tim.plunkett, webchick, effulgentsia, nod_, scor, Lars Toomre: Added Date and time field type in core.
parent 8edd4427
......@@ -1954,6 +1954,30 @@ function _format_date_callback(array $matches = NULL, $new_langcode = NULL) {
return $cache[$langcode][$code][$string];
}
/**
* Retrieves the correct datetime format type for this system.
*
* This value is sometimes required when the format type needs to be determined
* before a date can be created.
*
* @return string
* A string as defined in \DrupalComponent\Datetime\DateTimePlus.php: either
* 'intl' or 'php', depending on whether IntlDateFormatter is available.
*/
function datetime_default_format_type() {
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['format_type'] = &drupal_static(__FUNCTION__);
}
$format_type = &$drupal_static_fast['format_type'];
if (!isset($format_type)) {
$date = new DrupalDateTime();
$format_type = $date->canUseIntl() ? DrupalDateTime::INTL : DrupalDateTime::PHP;
}
return $format_type;
}
/**
* @} End of "defgroup format".
*/
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Utility\Color;
/**
......@@ -1728,8 +1729,8 @@ function form_error(&$element, $message = '') {
* - $element['#process']: An array of functions called after user input has
* been mapped to the element's #value property. These functions can be used
* to dynamically add child elements: for example, for the 'date' element
* type, one of the functions in this array is form_process_date(), which adds
* the individual 'year', 'month', 'day', etc. child elements. These functions
* type, one of the functions in this array is form_process_datetime(), which adds
* the individual 'date', and 'time'. child elements. These functions
* can also be used to set additional properties or implement special logic
* other than adding child elements: for example, for the 'details' element
* type, one of the functions in this array is form_process_details(), which
......@@ -3031,118 +3032,29 @@ function password_confirm_validate($element, &$element_state) {
}
/**
* Returns HTML for a date selection form element.
* Returns HTML for an #date form element.
*
* @param $variables
* Supports HTML5 types of 'date', 'datetime', 'datetime-local', and 'time'.
* Falls back to a plain textfield. Used as a sub-element by the datetime
* element type.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
* Properties used: #title, #value, #options, #description, #required,
* #attributes.
* #attributes, #id, #name, #type, #min, #max, #step, #value, #size.
*
* @ingroup themeable
*/
function theme_date($variables) {
$element = $variables['element'];
$attributes = array();
if (isset($element['#id'])) {
$attributes['id'] = $element['#id'];
}
if (!empty($element['#attributes']['class'])) {
$attributes['class'] = (array) $element['#attributes']['class'];
}
$attributes['class'][] = 'container-inline';
return '<div' . new Attribute($attributes) . '>' . drupal_render_children($element) . '</div>';
}
/**
* Expands a date element into year, month, and day select elements.
*/
function form_process_date($element) {
// Default to current date
if (empty($element['#value'])) {
$element['#value'] = array(
'day' => format_date(REQUEST_TIME, 'custom', 'j'),
'month' => format_date(REQUEST_TIME, 'custom', 'n'),
'year' => format_date(REQUEST_TIME, 'custom', 'Y'),
);
if (empty($element['attribute']['type'])) {
$element['attribute']['type'] = 'date';
}
element_set_attributes($element, array('id', 'name', 'type', 'min', 'max', 'step', 'value', 'size'));
_form_set_attributes($element, array('form-' . $element['attribute']['type']));
$element['#tree'] = TRUE;
// Determine the order of day, month, year in the site's chosen date format.
$format = config('system.date')->get('formats.short.pattern');
$format = $format['php'];
$sort = array();
$sort['day'] = max(strpos($format, 'd'), strpos($format, 'j'));
$sort['month'] = max(strpos($format, 'm'), strpos($format, 'M'));
$sort['year'] = strpos($format, 'Y');
asort($sort);
$order = array_keys($sort);
// Output multi-selector for date.
foreach ($order as $type) {
switch ($type) {
case 'day':
$options = drupal_map_assoc(range(1, 31));
$title = t('Day');
break;
case 'month':
$options = drupal_map_assoc(range(1, 12), 'map_month');
$title = t('Month');
break;
case 'year':
$options = drupal_map_assoc(range(1900, 2050));
$title = t('Year');
break;
}
$element[$type] = array(
'#type' => 'select',
'#title' => $title,
'#title_display' => 'invisible',
'#value' => $element['#value'][$type],
'#attributes' => $element['#attributes'],
'#options' => $options,
);
}
return $element;
}
/**
* Validates the date type to prevent invalid dates (e.g., February 30, 2006).
*/
function date_validate($element) {
if (!checkdate($element['#value']['month'], $element['#value']['day'], $element['#value']['year'])) {
form_error($element, t('The specified date is invalid.'));
}
}
/**
* Renders a month name for display.
*
* Callback for drupal_map_assoc() within form_process_date().
*/
function map_month($month) {
$months = &drupal_static(__FUNCTION__, array(
1 => 'Jan',
2 => 'Feb',
3 => 'Mar',
4 => 'Apr',
5 => 'May',
6 => 'Jun',
7 => 'Jul',
8 => 'Aug',
9 => 'Sep',
10 => 'Oct',
11 => 'Nov',
12 => 'Dec',
));
return t($months[$month]);
return '<input' . new Attribute($element['#attributes']) . ' />';
}
/**
......
......@@ -1564,6 +1564,12 @@ function theme_disable($theme_list) {
/**
* Preprocess variables for theme_datetime().
*
* @param array $variables
* An associative array possbily containing:
* - attributes['timestamp']:
* - timestamp:
* - text:
*/
function template_preprocess_datetime(&$variables) {
// Format the 'datetime' attribute based on the timestamp.
......@@ -1585,6 +1591,7 @@ function template_preprocess_datetime(&$variables) {
$variables['html'] = FALSE;
}
}
$variables['attributes'] = new Attribute($variables['attributes']);
}
/**
......
......@@ -4,5 +4,6 @@ package = Core
version = VERSION
core = 8.x
dependencies[] = node
dependencies[] = datetime
dependencies[] = text
configure = admin/content/comment
......@@ -61,7 +61,7 @@ public function form(array $form, array &$form_state, EntityInterface $comment)
if ($is_admin) {
$author = $comment->name->value;
$status = (isset($comment->status->value) ? $comment->status->value : COMMENT_NOT_PUBLISHED);
$date = (!empty($comment->date) ? $comment->date : format_date($comment->created->value, 'custom', 'Y-m-d H:i O'));
$date = (!empty($comment->date) ? $comment->date : new DrupalDateTime($comment->created->value));
}
else {
if ($user->uid) {
......@@ -117,10 +117,9 @@ public function form(array $form, array &$form_state, EntityInterface $comment)
// Add administrative comment publishing options.
$form['author']['date'] = array(
'#type' => 'textfield',
'#type' => 'datetime',
'#title' => t('Authored on'),
'#default_value' => $date,
'#maxlength' => 25,
'#size' => 20,
'#access' => $is_admin,
);
......@@ -212,8 +211,8 @@ public function validate(array $form, array &$form_state) {
$account = user_load_by_name($form_state['values']['name']);
$form_state['values']['uid'] = $account ? $account->uid : 0;
$date = new DrupalDateTime(!empty($form_state['values']['date']) ? $form_state['values']['date'] : 'now');
if ($date->hasErrors()) {
$date = $form_state['values']['date'];
if ($date instanceOf DrupalDateTime && $date->hasErrors()) {
form_set_error('date', t('You have to specify a valid date.'));
}
if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) {
......@@ -244,8 +243,12 @@ public function validate(array $form, array &$form_state) {
*/
public function buildEntity(array $form, array &$form_state) {
$comment = parent::buildEntity($form, $form_state);
$date = new DrupalDateTime(!empty($form_state['values']['date']) ? $form_state['values']['date'] : 'now');
$comment->created->value = $date->getTimestamp();
if (!empty($form_state['values']['date']) && $form_state['values']['date'] instanceOf DrupalDateTime) {
$comment->created->value = $form_state['values']['date']->getTimestamp();
}
else {
$comment->created->value = REQUEST_TIME;
}
$comment->changed->value = REQUEST_TIME;
return $comment;
}
......
......@@ -7,6 +7,8 @@
namespace Drupal\comment\Tests;
use Drupal\Core\Datetime\DrupalDateTime;
/**
* Tests previewing comments.
*/
......@@ -86,13 +88,16 @@ function testCommentEditPreviewSave() {
$this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED, 'Comment paging changed.');
$edit = array();
$date = new DrupalDateTime('2008-03-02 17:23');
$edit['subject'] = $this->randomName(8);
$edit['comment_body[' . $langcode . '][0][value]'] = $this->randomName(16);
$edit['name'] = $web_user->name;
$edit['date'] = '2008-03-02 17:23 +0300';
$raw_date = strtotime($edit['date']);
$edit['date[date]'] = $date->format('Y-m-d');
$edit['date[time]'] = $date->format('H:i:s');
$raw_date = $date->getTimestamp();
$expected_text_date = format_date($raw_date);
$expected_form_date = format_date($raw_date, 'custom', 'Y-m-d H:i O');
$expected_form_date = $date->format('Y-m-d');
$expected_form_time = $date->format('H:i:s');
$comment = $this->postComment($this->node, $edit['subject'], $edit['comment_body[' . $langcode . '][0][value]'], TRUE);
$this->drupalPost('comment/' . $comment->id() . '/edit', $edit, t('Preview'));
......@@ -107,7 +112,8 @@ function testCommentEditPreviewSave() {
$this->assertFieldByName('subject', $edit['subject'], 'Subject field displayed.');
$this->assertFieldByName('comment_body[' . $langcode . '][0][value]', $edit['comment_body[' . $langcode . '][0][value]'], 'Comment field displayed.');
$this->assertFieldByName('name', $edit['name'], 'Author field displayed.');
$this->assertFieldByName('date', $edit['date'], 'Date field displayed.');
$this->assertFieldByName('date[date]', $edit['date[date]'], 'Date field displayed.');
$this->assertFieldByName('date[time]', $edit['date[time]'], 'Time field displayed.');
// Check that saving a comment produces a success message.
$this->drupalPost('comment/' . $comment->id() . '/edit', $edit, t('Save'));
......@@ -118,14 +124,16 @@ function testCommentEditPreviewSave() {
$this->assertFieldByName('subject', $edit['subject'], 'Subject field displayed.');
$this->assertFieldByName('comment_body[' . $langcode . '][0][value]', $edit['comment_body[' . $langcode . '][0][value]'], 'Comment field displayed.');
$this->assertFieldByName('name', $edit['name'], 'Author field displayed.');
$this->assertFieldByName('date', $expected_form_date, 'Date field displayed.');
$this->assertFieldByName('date[date]', $expected_form_date, 'Date field displayed.');
$this->assertFieldByName('date[time]', $expected_form_time, 'Time field displayed.');
// Submit the form using the displayed values.
$displayed = array();
$displayed['subject'] = (string) current($this->xpath("//input[@id='edit-subject']/@value"));
$displayed['comment_body[' . $langcode . '][0][value]'] = (string) current($this->xpath("//textarea[@id='edit-comment-body-" . $langcode . "-0-value']"));
$displayed['name'] = (string) current($this->xpath("//input[@id='edit-name']/@value"));
$displayed['date'] = (string) current($this->xpath("//input[@id='edit-date']/@value"));
$displayed['date[date]'] = (string) current($this->xpath("//input[@id='edit-date-date']/@value"));
$displayed['date[time]'] = (string) current($this->xpath("//input[@id='edit-date-time']/@value"));
$this->drupalPost('comment/' . $comment->id() . '/edit', $displayed, t('Save'));
// Check that the saved comment is still correct.
......
......@@ -20,7 +20,7 @@ abstract class CommentTestBase extends WebTestBase {
*
* @var array
*/
public static $modules = array('comment', 'node', 'history', 'field_ui');
public static $modules = array('comment', 'node', 'history', 'field_ui', 'datetime');
/**
* An administrative user with permission to configure comment settings.
......
name = Datetime
description = Defines datetime form elements and a datetime field type.
package = Core
version = VERSION
core = 8.x
dependencies[] = field
<?php
/**
* @file
* Install, update and uninstall functions for the Datetime module.
*/
/**
* Implements hook_field_schema().
*/
function datetime_field_schema($field) {
$db_columns = array();
$db_columns['value'] = array(
'description' => 'The date value',
'type' => 'varchar',
'length' => 20,
'not null' => FALSE,
);
$indexes = array(
'value' => array('value'),
);
return array('columns' => $db_columns, 'indexes' => $indexes);
}
/**
* Install the new to D8 Datetime module.
*
* As part of adding this new module to Drupal 8, the Datetime namespace is now
* reserved for this module. This is a possible conflict with a popular contrib
* field DateTime that existed in D7. Hence, any Datetime fields that may have
* existed prior to D8 need to renamed for later upgrade by contrib modules like
* the Date module.
*/
function datetime_install() {
db_update('field_config')
->fields(array(
'type' => 'datetime_old',
))
->condition('type', 'datetime')
->execute();
}
This diff is collapsed.
This diff is collapsed.
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\field\formatter\DateTimeDefaultFormatter.
*/
namespace Drupal\datetime\Plugin\field\formatter;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\field\Plugin\Type\Formatter\FormatterBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Template\Attribute;
/**
* Plugin implementation of the 'datetime_default' formatter.
*
* @Plugin(
* id = "datetime_default",
* module = "datetime",
* label = @Translation("Default"),
* field_types = {
* "datetime"
* },
* settings = {
* "format_type" = "medium",
* }
* )
*/
class DateTimeDefaultFormatter extends FormatterBase {
/**
* Implements \Drupal\field\Plugin\Type\Formatter\FormatterInterface::viewElements().
*/
public function viewElements(EntityInterface $entity, $langcode, array $items) {
$elements = array();
foreach ($items as $delta => $item) {
$formatted_date = '';
$iso_date = '';
if (!empty($item['date'])) {
// The date was created and verified during field_load(), so it is safe
// to use without further inspection.
$date = $item['date'];
// Create the ISO date in Universal Time.
$iso_date = $date->format("Y-m-d\TH:i:s") . 'Z';
// The formatted output will be in local time.
$date->setTimeZone(timezone_open(drupal_get_user_timezone()));
if ($this->field['settings']['datetime_type'] == 'date') {
// A date without time will pick up the current time, use the default.
datetime_date_default_time($date);
}
$formatted_date = $this->dateFormat($date);
}
// Display the date using theme datetime.
// @todo How should RDFa attributes be added to this?
$elements[$delta] = array(
'#theme' => 'datetime',
'#text' => $formatted_date,
'#html' => FALSE,
'#attributes' => array(
'datetime' => $iso_date,
'property' => array('dc:date'),
'datatype' => 'xsd:dateTime',
),
);
}
return $elements;
}
/**
* Creates a formatted date value as a string.
*
* @param object $date
* A date object.
*
* @return string
* A formatted date string using the chosen format.
*/
function dateFormat($date) {
$format_type = $this->getSetting('format_type');
return format_date($date->getTimestamp(), $format_type);
}
/**
* Implements \Drupal\field\Plugin\Type\Formatter\FormatterInterface::settingsForm().
*/
public function settingsForm(array $form, array &$form_state) {
$element = array();
$time = new DrupalDateTime();
$format_types = system_get_date_formats();
if (!empty($format_types)) {
foreach ($format_types as $type => $type_info) {
$options[$type] = $type_info['name'] . ' (' . format_date($time->format('U'), $type) . ')';
}
}
$elements['format_type'] = array(
'#type' => 'select',
'#title' => t('Date format'),
'#description' => t("Choose a format for displaying the date. Be sure to set a format appropriate for the field, i.e. omitting time for a field that only has a date."),
'#options' => $options,
'#default_value' => $this->getSetting('format_type'),
);
return $elements;
}
/**
* Implements \Drupal\field\Plugin\Type\Formatter\FormatterInterface::settingsSummary().
*/
public function settingsSummary() {
$date = new DrupalDateTime();
$output = array();
$output[] = t('Format: @display', array('@display' => $this->dateFormat($date, FALSE)));
return implode('<br />', $output);
}
}
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\field\formatter\DateTimePlainFormatter.
*/
namespace Drupal\datetime\Plugin\field\formatter;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\field\Plugin\Type\Formatter\FormatterBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Datetime\DrupalDateTime;
/**
* Plugin implementation of the 'datetime_plain' formatter.
*
* @Plugin(
* id = "datetime_plain",
* module = "datetime",
* label = @Translation("Plain"),
* field_types = {
* "datetime"
* }
*)
*/
class DateTimePlainFormatter extends FormatterBase {
/**
* Implements Drupal\field\Plugin\Type\Formatter\FormatterInterface::viewElements().
*/
public function viewElements(EntityInterface $entity, $langcode, array $items) {
$elements = array();
foreach ($items as $delta => $item) {
$output = '';
if (!empty($item['date'])) {
// The date was created and verified during field_load(), so it is safe
// to use without further inspection.
$date = $item['date'];
$date->setTimeZone(timezone_open(drupal_get_user_timezone()));
$format = DATETIME_DATETIME_STORAGE_FORMAT;
if ($this->field['settings']['datetime_type'] == 'date') {
// A date without time will pick up the current time, use the default.
datetime_date_default_time($date);
$format = DATETIME_DATE_STORAGE_FORMAT;
}
$output = $date->format($format);
}
$elements[$delta] = array('#markup' => $output);
}
return $elements;
}
}
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\field\widget\DateTimeDatelistWidget.
*/
namespace Drupal\datetime\Plugin\field\widget;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\field\Plugin\Type\Widget\WidgetBase;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Plugin\PluginSettingsBase;
use Drupal\field\FieldInstance;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\DateHelper;
/**
* Plugin implementation of the 'datetime_datelist' widget.
*
* @Plugin(
* id = "datetime_datelist",
* module = "datetime",
* label = @Translation("Select list"),
* field_types = {
* "datetime"
* },
* settings = {
* "increment" = 15,
* "date_order" = "YMD",
* "time_type" = "24",
* }
* )
*/
class DateTimeDatelistWidget extends WidgetBase {
/**
* Constructs a DateTimeDatelist Widget object.
*
* @param array $plugin_id
* The plugin_id for the widget.
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $discovery
* The Discovery class that holds access to the widget implementation
* definition.
* @param \Drupal\field\FieldInstance $instance
* The field instance to which the widget is associated.
* @param array $settings
* The widget settings.
* @param int $weight
* The widget weight.
*/
public function __construct($plugin_id, DiscoveryInterface $discovery, FieldInstance $instance, array $settings, $weight) {
// Identify the function used to set the default value.
$instance['default_value_function'] = $this->defaultValueFunction();
parent::__construct($plugin_id, $discovery, $instance, $settings, $weight);
}
/**
* Returns the callback used to set a date default value.
*
* @return string
* The name of the callback to use when setting a default date value.
*/
public function defaultValueFunction() {
return 'datetime_default_value';
}
/**
* Implements \Drupal\field\Plugin\Type\Widget\WidgetInterface::formElement().
*/
public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) {
$field = $this->field;
$instance = $this->instance;
$date_order = $this->getSetting('date_order');
$time_type = $this->getSetting('time_type');
$increment = $this->getSetting('increment');
// We're nesting some sub-elements inside the parent, so we
// need a wrapper. We also need to add another #title attribute
// at the top level for ease in identifying this item in error
// messages. We don't want to display this title because the
// actual title display is handled at a higher level by the Field
// module.
$element['#theme_wrappers'][] = 'datetime_wrapper';
$element['#attributes']['class'][] = 'container-inline';
$element['#element_validate'][] = 'datetime_datelist_widget_validate';
// Identify the type of date and time elements to use.
switch ($field['settings']['datetime_type']) {
case 'date':
$storage_format = DATETIME_DATE_STORAGE_FORMAT;
$type_type = 'none';
break;
default:
$storage_format = DATETIME_DATETIME_STORAGE_FORMAT;
break;
}
// Set up the date part order array.
switch ($date_order) {
case 'YMD':
$date_part_order = array('year', 'month', 'day');
break;
case 'MDY':
$date_part_order = array('month', 'day', 'year');
break;
case 'DMY':
$date_part_order = array('day', 'month', 'year');
break;