diff --git a/core/i18n.php b/core/i18n.php index 4c4a9243e..9b9824807 100755 --- a/core/i18n.php +++ b/core/i18n.php @@ -74,6 +74,16 @@ class i18n extends Object { */ 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). * 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() { 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) * diff --git a/css/MemberDatetimeOptionsetField.css b/css/MemberDatetimeOptionsetField.css new file mode 100644 index 000000000..7c7de217d --- /dev/null +++ b/css/MemberDatetimeOptionsetField.css @@ -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; + } diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php index 90abdefe3..2f2564ff0 100755 --- a/dev/SapphireTest.php +++ b/dev/SapphireTest.php @@ -96,6 +96,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase { $this->originalIsRunningTest = self::$is_running_test; self::$is_running_test = true; + i18n::set_date_format(null); + i18n::set_time_format(null); + // Remove password validation $this->originalMemberPasswordValidator = Member::password_validator(); $this->originalRequirements = Requirements::backend(); diff --git a/forms/DateField.php b/forms/DateField.php index 6d9de1d4a..cabd8a29e 100755 --- a/forms/DateField.php +++ b/forms/DateField.php @@ -78,15 +78,13 @@ class DateField extends TextField { } 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); } function FieldHolder() { - - return parent::FieldHolder(); } diff --git a/forms/TimeField.php b/forms/TimeField.php index 6f891fa9d..2c26391ee 100755 --- a/forms/TimeField.php +++ b/forms/TimeField.php @@ -53,7 +53,7 @@ class TimeField extends TextField { } 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); diff --git a/javascript/MemberDatetimeOptionsetField.js b/javascript/MemberDatetimeOptionsetField.js new file mode 100644 index 000000000..a88feae02 --- /dev/null +++ b/javascript/MemberDatetimeOptionsetField.js @@ -0,0 +1,7 @@ +(function($) { + $('.formattingHelpText').hide(); + $('.formattingHelpToggle').click(function() { + $(this).parent().find('.formattingHelpText').toggle(); + return false; + }) +})(jQuery); \ No newline at end of file diff --git a/security/Member.php b/security/Member.php index 02883a639..1d12e3de5 100755 --- a/security/Member.php +++ b/security/Member.php @@ -27,7 +27,10 @@ class Member extends DataObject { 'LockedOutUntil' => 'SS_Datetime', 'Locale' => 'Varchar(6)', // 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( @@ -798,7 +801,7 @@ class Member extends DataObject { } } } - + /** * Return a SQL CONCAT() fragment suitable for a SELECT statement. * Useful for custom queries which assume a certain member title format. @@ -858,6 +861,42 @@ class Member extends DataObject { 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. */ public function getCMSFields() { + require_once('Zend/Date.php'); + $fields = parent::getCMSFields(); $mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children; @@ -1114,7 +1155,41 @@ class Member extends DataObject { $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); return $fields; @@ -1525,7 +1600,7 @@ class Member_ProfileForm extends Form { Requirements::css(SAPPHIRE_DIR . "/css/MemberProfileForm.css"); - $fields = singleton('Member')->getCMSFields(); + $fields = $member->getCMSFields(); $fields->push(new HiddenField('ID','ID',$member->ID)); $actions = new FieldSet( @@ -1753,4 +1828,110 @@ class Member_Validator extends RequiredFields { } } -?> \ No newline at end of file +/** + * @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 .= "
  • name\" type=\"radio\" value=\"$key\"$checked $disabled class=\"radio\" />
  • \n"; + } + + // Add "custom" input field + $value = ($this->value && !array_key_exists($this->value, $this->source)) ? $this->value : null; + $checked = ($value) ? " checked=\"checked\"" : ''; + $options .= "
  • " + . sprintf("", $itemID, $this->name, $checked) + . sprintf('', $itemID, _t('MemberDatetimeOptionsetField.Custom', 'Custom')) + . sprintf("\n", $this->name, $value) + . sprintf("", $this->Link() . '/validate'); + $options .= ($value) ? sprintf( + '(%s: "%s")', + _t('MemberDatetimeOptionsetField.Preview', 'Preview'), + Zend_Date::now()->toString($value) + ) : ''; + $options .= "toggle formatting help"; + $options .= "
    "; + $options .= $this->getFormattingHelpText(); + $options .= "
    "; + $options .= "
  • \n"; + + $id = $this->id(); + return "\n"; + } + + /** + * @todo Put this text into a template? + */ + function getFormattingHelpText() { + return ''; + } + + 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; + } + } + +} \ No newline at end of file diff --git a/tests/forms/MemberDatetimeOptionsetFieldTest.php b/tests/forms/MemberDatetimeOptionsetFieldTest.php new file mode 100644 index 000000000..0f216867f --- /dev/null +++ b/tests/forms/MemberDatetimeOptionsetFieldTest.php @@ -0,0 +1,100 @@ +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'; + } + +} \ No newline at end of file diff --git a/tests/forms/MemberDatetimeOptionsetFieldTest.yml b/tests/forms/MemberDatetimeOptionsetFieldTest.yml new file mode 100644 index 000000000..423e49c8e --- /dev/null +++ b/tests/forms/MemberDatetimeOptionsetFieldTest.yml @@ -0,0 +1,6 @@ +Member: + noformatmember: + Email: noformat@test.com + delocalemember: + Email: delocalemember@test.com + Locale: de_DE \ No newline at end of file diff --git a/tests/i18n/i18nTest.php b/tests/i18n/i18nTest.php index acb28f8bd..e1f010aea 100644 --- a/tests/i18n/i18nTest.php +++ b/tests/i18n/i18nTest.php @@ -53,6 +53,8 @@ class i18nTest extends SapphireTest { 'main' => $this->alternateBasePath . '/i18ntestmodule/templates/i18nTestModule.ss', 'Layout' => $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss', ); + + $this->originalLocale = i18n::get_locale(); } function tearDown() { @@ -66,11 +68,41 @@ class i18nTest extends SapphireTest { unset($_TEMPLATE_MANIFEST['i18nTestModule.ss']); unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']); - i18n::set_locale('en_US'); + i18n::set_locale($this->originalLocale); 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() { $translations = i18n::get_existing_translations(); $this->assertTrue(isset($translations['en_US']), 'Checking for en_US translation'); diff --git a/tests/security/MemberTest.php b/tests/security/MemberTest.php index 12a2f7f2b..fa8aa2586 100644 --- a/tests/security/MemberTest.php +++ b/tests/security/MemberTest.php @@ -263,7 +263,19 @@ class MemberTest extends FunctionalTest { $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() { $staffmember = $this->objFromFixture('Member', 'staffmember'); $managementmember = $this->objFromFixture('Member', 'managementmember'); diff --git a/tests/security/MemberTest.yml b/tests/security/MemberTest.yml index 65ba802d7..22d30eb83 100644 --- a/tests/security/MemberTest.yml +++ b/tests/security/MemberTest.yml @@ -56,4 +56,9 @@ Member: Email: ceomember@test.com Groups: =>Group.ceogroup grouplessmember: - FirstName: Groupless Member \ No newline at end of file + FirstName: Groupless Member + noformatmember: + Email: noformat@test.com + delocalemember: + Email: delocalemember@test.com + Locale: de_DE \ No newline at end of file