ENHANCEMENT #5352 Decouple date display from i18n locales, users now have access to change their date and time formats in Member::getCMSFields() using Member_DatetimeOptionsetField field

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.4@107326 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Sean Harvey 2010-06-30 05:02:29 +00:00 committed by Sam Minnee
parent d55e38b6ab
commit 3be26a4cec
12 changed files with 422 additions and 13 deletions

View File

@ -74,6 +74,16 @@ class i18n extends Object {
*/ */
protected static $js_i18n = true; protected static $js_i18n = true;
/**
* @var string
*/
protected static $date_format;
/**
* @var string
*/
protected static $time_format;
/** /**
* Use javascript i18n through the ss.i18n class (enabled by default). * Use javascript i18n through the ss.i18n class (enabled by default).
* If set to TRUE, includes javascript requirements for the base library * If set to TRUE, includes javascript requirements for the base library
@ -100,7 +110,37 @@ class i18n extends Object {
public static function get_js_i18n() { public static function get_js_i18n() {
return self::$js_i18n; return self::$js_i18n;
} }
/**
* @param string ISO date format
*/
public static function set_date_format($format) {
self::$date_format = $format;
}
/**
* @return string ISO date format
*/
public static function get_date_format() {
require_once 'Zend/Date.php';
return (self::$date_format) ? self::$date_format : Zend_Locale_Format::getDateFormat(self::get_locale());
}
/**
* @param string ISO time format
*/
public static function set_time_format($format) {
self::$time_format = $format;
}
/**
* @return string ISO time format
*/
public static function get_time_format() {
require_once 'Zend/Date.php';
return (self::$time_format) ? self::$time_format : Zend_Locale_Format::getTimeFormat(self::get_locale());
}
/** /**
* An exhaustive list of possible locales (code => language and country) * An exhaustive list of possible locales (code => language and country)
* *

View File

@ -0,0 +1,25 @@
input.customFormat {
border: 1px solid #ccc !important;
padding: 3px;
margin-left: 2px;
}
.formattingHelpToggle {
font-size: 11px;
padding: 3px;
}
.formattingHelpText {
margin: 5px auto;
color: #333;
padding: 5px 10px;
width: 90%;
background: #fff;
border: 1px solid #ccc;
}
.formattingHelpText ul {
padding: 0;
}
.formattingHelpText li {
font-size: 11px;
color: #333;
margin-bottom: 2px;
}

View File

@ -96,6 +96,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->originalIsRunningTest = self::$is_running_test; $this->originalIsRunningTest = self::$is_running_test;
self::$is_running_test = true; self::$is_running_test = true;
i18n::set_date_format(null);
i18n::set_time_format(null);
// Remove password validation // Remove password validation
$this->originalMemberPasswordValidator = Member::password_validator(); $this->originalMemberPasswordValidator = Member::password_validator();
$this->originalRequirements = Requirements::backend(); $this->originalRequirements = Requirements::backend();

View File

@ -78,15 +78,13 @@ class DateField extends TextField {
} }
if(!$this->getConfig('dateformat')) { if(!$this->getConfig('dateformat')) {
$this->setConfig('dateformat', Zend_Locale_Format::getDateFormat($this->locale)); $this->setConfig('dateformat', i18n::get_date_format());
} }
parent::__construct($name, $title, $value, $form, $rightTitle); parent::__construct($name, $title, $value, $form, $rightTitle);
} }
function FieldHolder() { function FieldHolder() {
return parent::FieldHolder(); return parent::FieldHolder();
} }

View File

@ -53,7 +53,7 @@ class TimeField extends TextField {
} }
if(!$this->getConfig('timeformat')) { if(!$this->getConfig('timeformat')) {
$this->setConfig('timeformat', Zend_Locale_Format::getDateFormat($this->locale)); $this->setConfig('timeformat', i18n::get_time_format());
} }
parent::__construct($name,$title,$value); parent::__construct($name,$title,$value);

View File

@ -0,0 +1,7 @@
(function($) {
$('.formattingHelpText').hide();
$('.formattingHelpToggle').click(function() {
$(this).parent().find('.formattingHelpText').toggle();
return false;
})
})(jQuery);

View File

@ -27,7 +27,10 @@ class Member extends DataObject {
'LockedOutUntil' => 'SS_Datetime', 'LockedOutUntil' => 'SS_Datetime',
'Locale' => 'Varchar(6)', 'Locale' => 'Varchar(6)',
// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
'FailedLoginCount' => 'Int', 'FailedLoginCount' => 'Int',
// In ISO format
'DateFormat' => 'Varchar(30)',
'TimeFormat' => 'Varchar(30)',
); );
static $belongs_many_many = array( static $belongs_many_many = array(
@ -798,7 +801,7 @@ class Member extends DataObject {
} }
} }
} }
/** /**
* Return a SQL CONCAT() fragment suitable for a SELECT statement. * Return a SQL CONCAT() fragment suitable for a SELECT statement.
* Useful for custom queries which assume a certain member title format. * Useful for custom queries which assume a certain member title format.
@ -858,6 +861,42 @@ class Member extends DataObject {
return $this->setName($name); return $this->setName($name);
} }
/**
* Override the default getter for DateFormat so the
* default format for the user's locale is used
* if the user has not defined their own.
*
* @return string ISO date format
*/
public function getDateFormat() {
if($this->getField('DateFormat')) {
return $this->getField('DateFormat');
} elseif($this->getField('Locale')) {
require_once 'Zend/Date.php';
return Zend_Locale_Format::getDateFormat($this->Locale);
} else {
return i18n::get_date_format();
}
}
/**
* Override the default getter for TimeFormat so the
* default format for the user's locale is used
* if the user has not defined their own.
*
* @return string ISO date format
*/
public function getTimeFormat() {
if($this->getField('TimeFormat')) {
return $this->getField('TimeFormat');
} elseif($this->getField('Locale')) {
require_once 'Zend/Date.php';
return Zend_Locale_Format::getTimeFormat($this->Locale);
} else {
return i18n::get_time_format();
}
}
//---------------------------------------------------------------------// //---------------------------------------------------------------------//
@ -1040,6 +1079,8 @@ class Member extends DataObject {
* editing this member. * editing this member.
*/ */
public function getCMSFields() { public function getCMSFields() {
require_once('Zend/Date.php');
$fields = parent::getCMSFields(); $fields = parent::getCMSFields();
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children; $mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
@ -1114,7 +1155,41 @@ class Member extends DataObject {
$fields->addFieldToTab('Root.Permissions', $permissionsField); $fields->addFieldToTab('Root.Permissions', $permissionsField);
} }
} }
$defaultDateFormat = Zend_Locale_Format::getDateFormat($this->Locale);
$dateFormatMap = array(
'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
);
$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
$mainFields->push(
$dateFormatField = new Member_DatetimeOptionsetField(
'DateFormat',
$this->fieldLabel('DateFormat'),
$dateFormatMap
)
);
$dateFormatField->setValue($this->DateFormat);
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat($this->Locale);
$timeFormatMap = array(
'h:mm a' => Zend_Date::now()->toString('h:mm a'),
'H:mm' => Zend_Date::now()->toString('H:mm'),
);
$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
$mainFields->push(
$timeFormatField = new Member_DatetimeOptionsetField(
'TimeFormat',
$this->fieldLabel('TimeFormat'),
$timeFormatMap
)
);
$timeFormatField->setValue($this->TimeFormat);
$this->extend('updateCMSFields', $fields); $this->extend('updateCMSFields', $fields);
return $fields; return $fields;
@ -1525,7 +1600,7 @@ class Member_ProfileForm extends Form {
Requirements::css(SAPPHIRE_DIR . "/css/MemberProfileForm.css"); Requirements::css(SAPPHIRE_DIR . "/css/MemberProfileForm.css");
$fields = singleton('Member')->getCMSFields(); $fields = $member->getCMSFields();
$fields->push(new HiddenField('ID','ID',$member->ID)); $fields->push(new HiddenField('ID','ID',$member->ID));
$actions = new FieldSet( $actions = new FieldSet(
@ -1753,4 +1828,110 @@ class Member_Validator extends RequiredFields {
} }
} }
?> /**
* @package sapphire
* @subpackage security
*/
class Member_DatetimeOptionsetField extends OptionsetField {
function Field() {
Requirements::css(SAPPHIRE_DIR . '/css/MemberDatetimeOptionsetField.css');
Requirements::javascript(THIRDPARTY_DIR . '/thirdparty/jquery/jquery.js');
Requirements::javascript(SAPPHIRE_DIR . '/javascript/MemberDatetimeOptionsetField.js');
$options = '';
$odd = 0;
$source = $this->getSource();
foreach($source as $key => $value) {
$itemID = $this->id() . "_" . ereg_replace('[^a-zA-Z0-9]+', '', $key);
if($key == $this->value) {
$useValue = false;
$checked = " checked=\"checked\"";
} else {
$checked = "";
}
$odd = ($odd + 1) % 2;
$extraClass = $odd ? "odd" : "even";
$extraClass .= " val" . preg_replace('/[^a-zA-Z0-9\-\_]/', '_', $key);
$disabled = ($this->disabled || in_array($key, $this->disabledItems)) ? "disabled=\"disabled\"" : "";
$options .= "<li class=\"".$extraClass."\"><input id=\"$itemID\" name=\"$this->name\" type=\"radio\" value=\"$key\"$checked $disabled class=\"radio\" /> <label title=\"$key\" for=\"$itemID\">$value</label></li>\n";
}
// Add "custom" input field
$value = ($this->value && !array_key_exists($this->value, $this->source)) ? $this->value : null;
$checked = ($value) ? " checked=\"checked\"" : '';
$options .= "<li class=\"valCustom\">"
. sprintf("<input id=\"%s_custom\" name=\"%s\" type=\"radio\" value=\"__custom__\" class=\"radio\" %s />", $itemID, $this->name, $checked)
. sprintf('<label for="%s_custom">%s:</label>', $itemID, _t('MemberDatetimeOptionsetField.Custom', 'Custom'))
. sprintf("<input class=\"customFormat\" name=\"%s_custom\" value=\"%s\" />\n", $this->name, $value)
. sprintf("<input type=\"hidden\" class=\"formatValidationURL\" value=\"%s\" />", $this->Link() . '/validate');
$options .= ($value) ? sprintf(
'<span class="preview">(%s: "%s")</span>',
_t('MemberDatetimeOptionsetField.Preview', 'Preview'),
Zend_Date::now()->toString($value)
) : '';
$options .= "<a class=\"formattingHelpToggle\" href=\"#\">toggle formatting help</a>";
$options .= "<div class=\"formattingHelpText\">";
$options .= $this->getFormattingHelpText();
$options .= "</div>";
$options .= "</li>\n";
$id = $this->id();
return "<ul id=\"$id\" class=\"optionset {$this->extraClass()}\">\n$options</ul>\n";
}
/**
* @todo Put this text into a template?
*/
function getFormattingHelpText() {
return '<ul>
<li>YYYY = four-digit year</li>
<li>YY = two-digit year</li>
<li>MMMM = full name of month (e.g. June)</li>
<li>MMM = shortened name of month (e.g. Jun)</li>
<li>MM = two-digit month (01=January, etc.)</li>
<li>M = day of month without leading zero</li>
<li>dd = two-digit day of month (01 through 31)</li>
<li>d = day of month without leading zero</li>
<li>hh = two digits of hour (00 through 23)</li>
<li>h = hour without leading zero</li>
<li>mm = two digits of minute (00 through 59)</li>
<li>m = minute without leading zero</li>
<li>ss = two digits of second (00 through 59)</li>
<li>s = one or more digits representing a decimal fraction of a second</li>
<li>a = AM or PM</li>
</ul>';
}
function setValue($value) {
if($value == '__custom__') {
$value = isset($_REQUEST[$this->name . '_custom']) ? $_REQUEST[$this->name . '_custom'] : null;
}
if($value) {
parent::setValue($value);
}
}
function validate() {
$value = isset($_POST[$this->name . '_custom']) ? $_POST[$this->name . '_custom'] : null;
if(!$value) return true; // no custom value, don't validate
// Check that the current date with the date format is valid or not
$validator = $this->form ? $this->form->getValidator() : null;
require_once 'Zend/Date.php';
$date = Zend_Date::now()->toString($value);
$valid = Zend_Date::isDate($date, $value);
if($valid) {
return true;
} else {
if($validator) {
$validator->validationError($this->name, _t('Member.DATEFORMATBAD',"Date format is invalid"), "validation", false);
}
return false;
}
}
}

View File

@ -0,0 +1,100 @@
<?php
/**
* @package sapphire
* @subpackage forms
*/
class MemberDatetimeOptionsetFieldTest extends SapphireTest {
public static $fixture_file = 'sapphire/tests/forms/MemberDatetimeOptionsetFieldTest.yml';
protected function createDateFormatFieldForMember($member) {
require_once 'Zend/Date.php';
$defaultDateFormat = Zend_Locale_Format::getDateFormat($member->Locale);
$dateFormatMap = array(
'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
);
$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat) . ' (default)';
$field = new Member_DatetimeOptionsetField(
'DateFormat',
'Date format',
$dateFormatMap
);
$field->setValue($member->DateFormat);
return $field;
}
protected function createTimeFormatFieldForMember($member) {
require_once 'Zend/Date.php';
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat($member->Locale);
$timeFormatMap = array(
'h:mm a' => Zend_Date::now()->toString('h:mm a'),
'H:mm' => Zend_Date::now()->toString('H:mm'),
);
$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat) . ' (default)';
$field = new Member_DatetimeOptionsetField(
'TimeFormat',
'Time format',
$timeFormatMap
);
$field->setValue($member->TimeFormat);
return $field;
}
function testDateFormatDefaultCheckedInFormField() {
$field = $this->createDateFormatFieldForMember($this->objFromFixture('Member', 'noformatmember'));
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MMMdyyyy');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
}
function testTimeFormatDefaultCheckedInFormField() {
$field = $this->createTimeFormatFieldForMember($this->objFromFixture('Member', 'noformatmember'));
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_TimeFormat_hmmssa');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
}
function testDateFormatChosenIsCheckedInFormField() {
$member = $this->objFromFixture('Member', 'noformatmember');
$member->setField('DateFormat', 'MM/dd/yyyy');
$field = $this->createDateFormatFieldForMember($member);
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MMddyyyy');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
}
function testDateFormatCustomFormatAppearsInCustomInputInField() {
$member = $this->objFromFixture('Member', 'noformatmember');
$member->setField('DateFormat', 'dd MM yy');
$field = $this->createDateFormatFieldForMember($member);
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form
$parser = new CSSContentParser($field->Field());
$xmlInputArr = $parser->getBySelector('.valCustom input');
$xmlPreview = $parser->getBySelector('.preview');
$this->assertEquals('checked', (string) $xmlInputArr[0]['checked']);
$this->assertEquals('dd MM yy', (string) $xmlInputArr[1]['value']);
}
function testDateFormValid() {
$field = new Member_DatetimeOptionsetField('DateFormat', 'DateFormat');
$this->assertTrue($field->validate());
$_POST['DateFormat_custom'] = 'dd MM yyyy';
$this->assertTrue($field->validate());
$_POST['DateFormat_custom'] = 'sdfdsfdfd1244';
$this->assertFalse($field->validate());
}
}
class MemberDatetimeOptionsetFieldTest_Controller extends Controller {
function Link() {
return 'test';
}
}

View File

@ -0,0 +1,6 @@
Member:
noformatmember:
Email: noformat@test.com
delocalemember:
Email: delocalemember@test.com
Locale: de_DE

View File

@ -53,6 +53,8 @@ class i18nTest extends SapphireTest {
'main' => $this->alternateBasePath . '/i18ntestmodule/templates/i18nTestModule.ss', 'main' => $this->alternateBasePath . '/i18ntestmodule/templates/i18nTestModule.ss',
'Layout' => $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss', 'Layout' => $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss',
); );
$this->originalLocale = i18n::get_locale();
} }
function tearDown() { function tearDown() {
@ -66,11 +68,41 @@ class i18nTest extends SapphireTest {
unset($_TEMPLATE_MANIFEST['i18nTestModule.ss']); unset($_TEMPLATE_MANIFEST['i18nTestModule.ss']);
unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']); unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']);
i18n::set_locale('en_US'); i18n::set_locale($this->originalLocale);
parent::tearDown(); parent::tearDown();
} }
function testDateFormatFromLocale() {
i18n::set_locale('en_US');
$this->assertEquals('MMM d, yyyy', i18n::get_date_format());
i18n::set_locale('en_NZ');
$this->assertEquals('d/MM/yyyy', i18n::get_date_format());
i18n::set_locale('en_US');
}
function testTimeFormatFromLocale() {
i18n::set_locale('en_US');
$this->assertEquals('h:mm:ss a', i18n::get_time_format());
i18n::set_locale('de_DE');
$this->assertEquals('HH:mm:ss', i18n::get_time_format());
i18n::set_locale('en_US');
}
function testDateFormatCustom() {
i18n::set_locale('en_US');
$this->assertEquals('MMM d, yyyy', i18n::get_date_format());
i18n::set_date_format('d/MM/yyyy');
$this->assertEquals('d/MM/yyyy', i18n::get_date_format());
}
function testTimeFormatCustom() {
i18n::set_locale('en_US');
$this->assertEquals('h:mm:ss a', i18n::get_time_format());
i18n::set_time_format('HH:mm:ss');
$this->assertEquals('HH:mm:ss', i18n::get_time_format());
}
function testGetExistingTranslations() { function testGetExistingTranslations() {
$translations = i18n::get_existing_translations(); $translations = i18n::get_existing_translations();
$this->assertTrue(isset($translations['en_US']), 'Checking for en_US translation'); $this->assertTrue(isset($translations['en_US']), 'Checking for en_US translation');

View File

@ -263,7 +263,19 @@ class MemberTest extends FunctionalTest {
$this->assertFalse($member->isPasswordExpired()); $this->assertFalse($member->isPasswordExpired());
} }
function testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat() {
$member = $this->objFromFixture('Member', 'noformatmember');
$this->assertEquals('MMM d, yyyy', $member->DateFormat);
$this->assertEquals('h:mm:ss a', $member->TimeFormat);
}
function testMemberWithNoDateFormatFallsbackToTheirLocaleDefaultFormat() {
$member = $this->objFromFixture('Member', 'delocalemember');
$this->assertEquals('dd.MM.yyyy', $member->DateFormat);
$this->assertEquals('HH:mm:ss', $member->TimeFormat);
}
function testInGroups() { function testInGroups() {
$staffmember = $this->objFromFixture('Member', 'staffmember'); $staffmember = $this->objFromFixture('Member', 'staffmember');
$managementmember = $this->objFromFixture('Member', 'managementmember'); $managementmember = $this->objFromFixture('Member', 'managementmember');

View File

@ -56,4 +56,9 @@ Member:
Email: ceomember@test.com Email: ceomember@test.com
Groups: =>Group.ceogroup Groups: =>Group.ceogroup
grouplessmember: grouplessmember:
FirstName: Groupless Member FirstName: Groupless Member
noformatmember:
Email: noformat@test.com
delocalemember:
Email: delocalemember@test.com
Locale: de_DE