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 .= "
\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 "extraClass()}\">\n$options
\n";
+ }
+
+ /**
+ * @todo Put this text into a template?
+ */
+ function getFormattingHelpText() {
+ return '
+- YYYY = four-digit year
+- YY = two-digit year
+- MMMM = full name of month (e.g. June)
+- MMM = shortened name of month (e.g. Jun)
+- MM = two-digit month (01=January, etc.)
+- M = day of month without leading zero
+- dd = two-digit day of month (01 through 31)
+- d = day of month without leading zero
+- hh = two digits of hour (00 through 23)
+- h = hour without leading zero
+- mm = two digits of minute (00 through 59)
+- m = minute without leading zero
+- ss = two digits of second (00 through 59)
+- s = one or more digits representing a decimal fraction of a second
+- a = AM or PM
+
';
+ }
+
+ 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