From 2dbf6f9b2b05ebf14aa26b9c76443db51dd23a7e Mon Sep 17 00:00:00 2001 From: Daniel Stolt Date: Sun, 3 Aug 2014 00:20:48 +0200 Subject: [PATCH] Improved DateTimeField (bypass time zone conversion if date-only, store/retrieve only the relevant components from the repository, surface the display mode as a property on the field). --- src/Orchard.Web/Core/Shapes/DateTimeShapes.cs | 2 +- .../Views/Admin/Detail.cshtml | 2 +- .../Views/Admin/Index.cshtml | 2 +- .../Drivers/DateTimeFieldDriver.cs | 64 +++++++++++++++---- .../Orchard.Fields/Fields/DateTimeField.cs | 24 ++++++- .../DefaultDateLocalizationServices.cs | 43 ++++++++++++- .../Services/IDateLocalizationServices.txt | 6 +- 7 files changed, 122 insertions(+), 21 deletions(-) diff --git a/src/Orchard.Web/Core/Shapes/DateTimeShapes.cs b/src/Orchard.Web/Core/Shapes/DateTimeShapes.cs index f63f4bedc..f9fb9bea8 100644 --- a/src/Orchard.Web/Core/Shapes/DateTimeShapes.cs +++ b/src/Orchard.Web/Core/Shapes/DateTimeShapes.cs @@ -65,7 +65,7 @@ namespace Orchard.Core.Shapes { //using a LocalizedString forces the caller to use a localizable format if (CustomFormat == null || String.IsNullOrWhiteSpace(CustomFormat.Text)) { - return new MvcHtmlString(_dateLocalizationServices.ConvertToLocalizedString(DateTimeUtc, _dateTimeLocalization.LongDateTimeFormat)); + return new MvcHtmlString(_dateLocalizationServices.ConvertToLocalizedString(DateTimeUtc, _dateTimeLocalization.ShortDateTimeFormat)); } return new MvcHtmlString(_dateLocalizationServices.ConvertToLocalizedString(DateTimeUtc, CustomFormat.Text)); diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/Admin/Detail.cshtml b/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/Admin/Detail.cshtml index f7201d498..bc4637cf2 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/Admin/Detail.cshtml +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/Admin/Detail.cshtml @@ -10,7 +10,7 @@
@T("Event:") @descriptor.Name
@T("Category:") @descriptor.CategoryDescriptor.Name
- @T("Timestamp:") @Display.DateTime(DateTimeUtc: record.CreatedUtc, CustomFormat: T("g"))
+ @T("Timestamp:") @Display.DateTime(DateTimeUtc: record.CreatedUtc)
@T("Username:") @record.UserName
diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/Admin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/Admin/Index.cshtml index 6819294d3..30e3078ae 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/Admin/Index.cshtml +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/Admin/Index.cshtml @@ -49,7 +49,7 @@ @record.CategoryDescriptor.Name @record.EventDescriptor.Name @record.Record.UserName - @Display.DateTime(DateTimeUtc: record.Record.CreatedUtc, CustomFormat: T("g")) + @Display.DateTime(DateTimeUtc: record.Record.CreatedUtc) @Display(record.SummaryShape) @record.Record.Comment @Html.ActionLink(T("Details").Text, "Detail", "Admin", new { id = record.Record.Id, area = "Orchard.AuditTrail" }, null) diff --git a/src/Orchard.Web/Modules/Orchard.Fields/Drivers/DateTimeFieldDriver.cs b/src/Orchard.Web/Modules/Orchard.Fields/Drivers/DateTimeFieldDriver.cs index 12becc6cf..6c6794107 100644 --- a/src/Orchard.Web/Modules/Orchard.Fields/Drivers/DateTimeFieldDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.Fields/Drivers/DateTimeFieldDriver.cs @@ -11,6 +11,7 @@ using Orchard.ContentManagement.Handlers; using Orchard.Localization; using Orchard.Localization.Services; using Orchard.Core.Common.ViewModels; +using Orchard.Localization.Models; namespace Orchard.Fields.Drivers { [UsedImplicitly] @@ -41,16 +42,25 @@ namespace Orchard.Fields.Drivers { () => { var settings = field.PartFieldDefinition.Settings.GetModel(); var value = field.DateTime; + var options = new DateLocalizationOptions(); + + // Don't do any time zone conversion if field is semantically a date-only field, because that might mutate the date component. + if (settings.Display == DateTimeFieldDisplays.DateOnly) { + options.EnableTimeZoneConversion = false; + } + + var showDate = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly; + var showTime = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.TimeOnly; var viewModel = new DateTimeFieldViewModel { Name = field.DisplayName, Hint = settings.Hint, IsRequired = settings.Required, Editor = new DateTimeEditor() { - Date = DateLocalizationServices.ConvertToLocalizedDateString(value), - Time = DateLocalizationServices.ConvertToLocalizedTimeString(value), - ShowDate = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly, - ShowTime = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.TimeOnly, + Date = showDate ? DateLocalizationServices.ConvertToLocalizedDateString(value, options) : null, + Time = showTime ? DateLocalizationServices.ConvertToLocalizedTimeString(value, options) : null, + ShowDate = showDate, + ShowTime = showTime, } }; @@ -63,16 +73,25 @@ namespace Orchard.Fields.Drivers { protected override DriverResult Editor(ContentPart part, DateTimeField field, dynamic shapeHelper) { var settings = field.PartFieldDefinition.Settings.GetModel(); var value = field.DateTime; + var options = new DateLocalizationOptions(); + + // Don't do any time zone conversion if field is semantically a date-only field, because that might mutate the date component. + if (settings.Display == DateTimeFieldDisplays.DateOnly) { + options.EnableTimeZoneConversion = false; + } + + var showDate = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly; + var showTime = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.TimeOnly; var viewModel = new DateTimeFieldViewModel { Name = field.DisplayName, Hint = settings.Hint, IsRequired = settings.Required, Editor = new DateTimeEditor() { - Date = DateLocalizationServices.ConvertToLocalizedDateString(value), - Time = DateLocalizationServices.ConvertToLocalizedTimeString(value), - ShowDate = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly, - ShowTime = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.TimeOnly, + Date = showDate ? DateLocalizationServices.ConvertToLocalizedDateString(value, options) : null, + Time = showTime ? DateLocalizationServices.ConvertToLocalizedTimeString(value, options) : null, + ShowDate = showDate, + ShowTime = showTime, } }; @@ -86,13 +105,34 @@ namespace Orchard.Fields.Drivers { if (updater.TryUpdateModel(viewModel, GetPrefix(field, part), null, null)) { var settings = field.PartFieldDefinition.Settings.GetModel(); - if (settings.Required && (((settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly) && String.IsNullOrWhiteSpace(viewModel.Editor.Date)) || ((settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.TimeOnly) && String.IsNullOrWhiteSpace(viewModel.Editor.Time)))) { + + var options = new DateLocalizationOptions(); + + // Don't do any time zone conversion if field is semantically a date-only field, because that might mutate the date component. + if (settings.Display == DateTimeFieldDisplays.DateOnly) { + options.EnableTimeZoneConversion = false; + } + + var showDate = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly; + var showTime = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.TimeOnly; + + if (settings.Required && ((showDate && String.IsNullOrWhiteSpace(viewModel.Editor.Date)) || (showTime && String.IsNullOrWhiteSpace(viewModel.Editor.Time)))) { updater.AddModelError(GetPrefix(field, part), T("{0} is required.", field.DisplayName)); - } else { + } + else { try { - var utcDateTime = DateLocalizationServices.ConvertFromLocalizedString(viewModel.Editor.Date, viewModel.Editor.Time); + var utcDateTime = DateLocalizationServices.ConvertFromLocalizedString(viewModel.Editor.Date, viewModel.Editor.Time, options); + if (utcDateTime.HasValue) { - field.DateTime = utcDateTime.Value; + // Hackish workaround to make sure a time-only field with an entered time equivalent to + // 00:00 UTC doesn't get stored as a full DateTime.MinValue in the database, resulting + // in it being interpreted as an empty value when subsequently retrieved. + if (settings.Display == DateTimeFieldDisplays.TimeOnly && utcDateTime.Value == DateTime.MinValue) { + field.DateTime = utcDateTime.Value.AddDays(1); + } + else { + field.DateTime = utcDateTime.Value; + } } else { field.DateTime = DateTime.MinValue; } diff --git a/src/Orchard.Web/Modules/Orchard.Fields/Fields/DateTimeField.cs b/src/Orchard.Web/Modules/Orchard.Fields/Fields/DateTimeField.cs index 58d7c69fd..ca47a2f22 100644 --- a/src/Orchard.Web/Modules/Orchard.Fields/Fields/DateTimeField.cs +++ b/src/Orchard.Web/Modules/Orchard.Fields/Fields/DateTimeField.cs @@ -1,17 +1,37 @@ using System; using Orchard.ContentManagement; using Orchard.ContentManagement.FieldStorage; +using Orchard.Fields.Settings; namespace Orchard.Fields.Fields { public class DateTimeField : ContentField { public DateTime DateTime { get { + var settings = this.PartFieldDefinition.Settings.GetModel(); var value = Storage.Get(); + if (settings.Display == DateTimeFieldDisplays.DateOnly) { + return new DateTime(value.Year, value.Month, value.Day); + } return value; } - set { Storage.Set(value); } + set { + var settings = this.PartFieldDefinition.Settings.GetModel(); + if (settings.Display == DateTimeFieldDisplays.DateOnly) { + Storage.Set(new DateTime(value.Year, value.Month, value.Day)); + } + else { + Storage.Set(value); + } + } } - } + + public DateTimeFieldDisplays Display { + get { + var settings = this.PartFieldDefinition.Settings.GetModel(); + return settings.Display; + } + } + } } diff --git a/src/Orchard/Localization/Services/DefaultDateLocalizationServices.cs b/src/Orchard/Localization/Services/DefaultDateLocalizationServices.cs index dd71a76b7..6fc6e29fb 100644 --- a/src/Orchard/Localization/Services/DefaultDateLocalizationServices.cs +++ b/src/Orchard/Localization/Services/DefaultDateLocalizationServices.cs @@ -2,22 +2,26 @@ using System.Globalization; using Orchard.ContentManagement; using Orchard.Localization.Models; +using Orchard.Services; using Orchard.Settings; namespace Orchard.Localization.Services { public class DefaultDateLocalizationServices : IDateLocalizationServices { + private readonly IClock _clock; private readonly IWorkContextAccessor _workContextAccessor; private readonly IDateTimeFormatProvider _dateTimeFormatProvider; private readonly IDateFormatter _dateFormatter; private readonly ICalendarManager _calendarManager; public DefaultDateLocalizationServices( + IClock clock, IWorkContextAccessor workContextAccessor, IDateTimeFormatProvider dateTimeFormatProvider, IDateFormatter dateFormatter, ICalendarManager calendarManager) { + _clock = clock; _workContextAccessor = workContextAccessor; _dateTimeFormatProvider = dateTimeFormatProvider; _dateFormatter = dateFormatter; @@ -63,7 +67,32 @@ namespace Orchard.Localization.Services { } public string ConvertToLocalizedTimeString(DateTime? date, DateLocalizationOptions options = null) { - return ConvertToLocalizedString(date, _dateTimeFormatProvider.LongTimeFormat, options); + options = options ?? new DateLocalizationOptions(); + + if (!date.HasValue) { + return options.NullText; + } + + var dateValue = date.Value; + + if (options.EnableTimeZoneConversion) { + // Since no date component is expected (technically the date component is that of DateTime.MinValue) then + // we must employ some trickery, for two reasons: + // * DST can be active or not dependeng on the time of the year. We want the conversion to always act as if the time represents today, but we don't want that date stored. + // * Time zone conversion cannot wrap DateTime.MinValue around to the previous day, resulting in undefined result. + // Therefore we convert the date to today's date before the conversion, and back to DateTime.MinValue after. + var now = _clock.UtcNow; + dateValue = new DateTime(now.Year, now.Month, now.Day, dateValue.Hour, dateValue.Minute, dateValue.Second, dateValue.Millisecond, dateValue.Kind); + dateValue = ConvertToSiteTimeZone(dateValue); + dateValue = new DateTime(DateTime.MinValue.Year, DateTime.MinValue.Month, DateTime.MinValue.Day, dateValue.Hour, dateValue.Minute, dateValue.Second, dateValue.Millisecond, dateValue.Kind); + } + + var parts = DateTimeParts.FromDateTime(dateValue); + if (options.EnableCalendarConversion && !(CurrentCalendar is GregorianCalendar)) { + parts = ConvertToSiteCalendar(dateValue); + } + + return _dateFormatter.FormatDateTime(parts, _dateTimeFormatProvider.LongTimeFormat); } public string ConvertToLocalizedString(DateTime date, DateLocalizationOptions options = null) { @@ -127,7 +156,19 @@ namespace Orchard.Localization.Services { } if (hasTime && options.EnableTimeZoneConversion) { + // If there is no date component (technically the date component is that of DateTime.MinValue) then + // we must employ some trickery, for two reasons: + // * DST can be active or not dependeng on the time of the year. We want the conversion to always act as if the time represents today, but we don't want that date stored. + // * Time zone conversion cannot wrap DateTime.MinValue around to the previous day, resulting in undefined result. + // Therefore we convert the date to today's date before the conversion, and back to DateTime.MinValue after. + if (!hasDate) { + var now = _clock.UtcNow; + dateValue = new DateTime(now.Year, now.Month, now.Day, dateValue.Hour, dateValue.Minute, dateValue.Second, dateValue.Millisecond, dateValue.Kind); + } dateValue = ConvertFromSiteTimeZone(dateValue); + if (!hasDate) { + dateValue = new DateTime(DateTime.MinValue.Year, DateTime.MinValue.Month, DateTime.MinValue.Day, dateValue.Hour, dateValue.Minute, dateValue.Second, dateValue.Millisecond, dateValue.Kind); + } } return dateValue; diff --git a/src/Orchard/Localization/Services/IDateLocalizationServices.txt b/src/Orchard/Localization/Services/IDateLocalizationServices.txt index 98933257c..a6df3e138 100644 --- a/src/Orchard/Localization/Services/IDateLocalizationServices.txt +++ b/src/Orchard/Localization/Services/IDateLocalizationServices.txt @@ -4,9 +4,9 @@ TODO: * Write unit tests for DefaultDateLocalizationServices * Add warning message when saving unsupported combination in settings * Add support for the different Gregorian calendar types - * Improve DateTimeField: - - Surface the field mode (date, time or both) - - Do not perform time-zone conversion in date-only mode + * Make sure DateTimeField, ArchiveLaterPart and PublishLaterPart handle Persian dates correctly. + * Improve CultureDateTimeFormatProvider to return correct information for fa-IR culture when PersianCalendar is in use. + * Check for all uses of Display.DateTime that use a standard format string - no can do! BREAKING: * DateTokens "Date.Format:" and "Date.Local.Format:" only supports custom date/time format strings. \ No newline at end of file