Changed implementation of DateTokens to use localization abstractions. Added more tokens. Beefed up unit tests to cover all tokens.

This commit is contained in:
Daniel Stolt
2014-08-02 21:49:41 +02:00
parent dc21e33294
commit f019392a93
7 changed files with 153 additions and 109 deletions

View File

@@ -2,27 +2,27 @@
namespace Orchard.AuditTrail.Helpers {
public static class DateTimeExtensions {
public static DateTime? Earliest(this DateTime? value) {
if (value == null)
return null;
return Earliest(value.Value);
}
public static DateTime Earliest(this DateTime value) {
public static DateTime StartOfDay(this DateTime value) {
return new DateTime(value.Year, value.Month, value.Day, 0, 0, 0, 0, value.Kind);
}
public static DateTime? Latest(this DateTime? value) {
public static DateTime? StartOfDay(this DateTime? value) {
if (value == null)
return null;
return StartOfDay(value.Value);
}
public static DateTime EndOfDay(this DateTime value) {
return new DateTime(value.Year, value.Month, value.Day, 23, 59, 59, 999, value.Kind);
}
public static DateTime? EndOfDay(this DateTime? value) {
if (value == null)
return null;
var v = value.Value;
return new DateTime(v.Year, v.Month, v.Day, 23, 59, 59, 999, v.Kind);
}
public static DateTime Latest(this DateTime value) {
return new DateTime(value.Year, value.Month, value.Day, 23, 59, 59, 999, value.Kind);
}
}
}

View File

@@ -206,7 +206,7 @@ namespace Orchard.AuditTrail.Services {
}
public IEnumerable<AuditTrailEventRecord> Trim(TimeSpan retentionPeriod) {
var dateThreshold = (_clock.UtcNow.Latest() - retentionPeriod);
var dateThreshold = (_clock.UtcNow.EndOfDay() - retentionPeriod);
var query = _auditTrailRepository.Table.Where(x => x.CreatedUtc <= dateThreshold);
var records = query.ToArray();

View File

@@ -18,8 +18,8 @@ namespace Orchard.AuditTrail.Services {
public override void Filter(QueryFilterContext context) {
var userName = context.Filters.Get("username");
var category = context.Filters.Get("category");
var from = GetDateFromFilter(context.Filters, "From", "from").Earliest();
var to = GetDateFromFilter(context.Filters, "To", "to").Latest();
var from = GetDateFromFilter(context.Filters, "From", "from").StartOfDay();
var to = GetDateFromFilter(context.Filters, "To", "to").EndOfDay();
var query = context.Query;
if (!String.IsNullOrWhiteSpace(userName)) {
@@ -57,13 +57,12 @@ namespace Orchard.AuditTrail.Services {
}
private DateTime? GetDateFromFilter(Filters filters, string fieldName, string prefix) {
var dateString = filters.Get(prefix + ".Date");
try {
var dateString = filters.Get(prefix + ".Date");
var timeString = filters.Get(prefix + ".Time");
return _dateLocalizationServices.ConvertFromLocalizedString(dateString, timeString);
return _dateLocalizationServices.ConvertFromLocalizedDateString(dateString);
}
catch (FormatException ex) {
filters.UpdateModel.AddModelError(prefix, T(@"Error parsing ""{0}"" date: {1}", fieldName, ex.Message));
filters.UpdateModel.AddModelError(prefix, T(@"Error parsing '{0}' date string '{1}': {2}", fieldName, dateString, ex.Message));
return null;
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using Orchard.Localization;
using Orchard.Localization.Models;
using Orchard.Localization.Services;
using Orchard.Mvc.Html;
using Orchard.Services;
@@ -8,66 +9,79 @@ using Orchard.Services;
namespace Orchard.Tokens.Providers {
public class DateTokens : ITokenProvider {
private readonly IClock _clock;
private readonly IDateTimeFormatProvider _dateTimeLocalization;
private readonly IWorkContextAccessor _workContextAccessor;
private readonly Lazy<CultureInfo> _cultureInfo;
private readonly IDateTimeFormatProvider _dateTimeFormats;
private readonly IDateFormatter _dateFormatter;
private readonly IDateLocalizationServices _dateLocalizationServices;
//private readonly Lazy<CultureInfo> _cultureInfo;
public DateTokens(
IClock clock,
IDateTimeFormatProvider dateTimeLocalization,
IWorkContextAccessor workContextAccessor,
IDateLocalizationServices dateServices) {
IDateTimeFormatProvider dateTimeFormats,
IDateFormatter dateFormatter,
IDateLocalizationServices dateLocalizationServices) {
_clock = clock;
_dateTimeLocalization = dateTimeLocalization;
_workContextAccessor = workContextAccessor;
_cultureInfo = new Lazy<CultureInfo>(() => CultureInfo.GetCultureInfo(_workContextAccessor.GetContext().CurrentCulture));
_dateLocalizationServices = dateServices;
_dateTimeFormats = dateTimeFormats;
_dateFormatter = dateFormatter;
_dateLocalizationServices = dateLocalizationServices;
//_cultureInfo = new Lazy<CultureInfo>(() => CultureInfo.GetCultureInfo(_workContextAccessor.GetContext().CurrentCulture));
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(DescribeContext context) {
context.For("Date", T("Date/time"), T("Current date/time tokens"))
context.For("Date", T("Date/Time"), T("Current date/time tokens"))
.Token("Since", T("Since"), T("Relative to the current date/time."), "Date")
.Token("Local", T("Local"), T("Based on the configured time zone and calendar."), "Date")
.Token("Short", T("Short Date and Time"), T("Short date and time format."))
.Token("ShortDate", T("Short Date"), T("Short date format."))
.Token("ShortTime", T("Short Time"), T("Short time format."))
.Token("Long", T("Long Date and Time"), T("Long date and time format."))
.Token("Format:*", T("Format:<date format>"), T("Optional format specifier (e.g. yyyy/MM/dd). See format strings at <a target=\"_blank\" href=\"http://msdn.microsoft.com/en-us/library/az4se3k1.aspx\">Standard Formats</a> and <a target=\"_blank\" href=\"http://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx\">Custom Formats</a>"), "DateTime");
.Token("LongDate", T("Long Date"), T("Long date format."))
.Token("LongTime", T("Long Time"), T("Long time format."))
.Token("Format:*", T("Format:<formatString>"), T("Optional custom date/time format string (e.g. yyyy/MM/dd). For reference see <a target=\"_blank\" href=\"http://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx\">Custom Date and Time Format Strings</a>"), "DateTime");
}
public void Evaluate(EvaluateContext context) {
context.For("Date", () => _clock.UtcNow)
// {Date.Since}
.Token("Since", DateTimeRelative)
.Chain("Since", "Date", DateTimeRelative)
// {Date.Local}
.Token("Local", d => _dateLocalizationServices.ConvertToSiteTimeZone(d))
.Token("Local", d => _dateLocalizationServices.ConvertToLocalizedString(d))
.Chain("Local", "Date", d => _dateLocalizationServices.ConvertToSiteTimeZone(d))
// {Date.Short}
.Token("Short", d => _dateLocalizationServices.ConvertToLocalizedString(d, _dateTimeFormats.ShortDateTimeFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false }))
// {Date.ShortDate}
.Token("ShortDate", d => d.ToString(_dateTimeLocalization.ShortDateFormat, _cultureInfo.Value))
.Token("ShortDate", d => _dateLocalizationServices.ConvertToLocalizedString(d, _dateTimeFormats.ShortDateFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false }))
// {Date.ShortTime}
.Token("ShortTime", d => d.ToString(_dateTimeLocalization.ShortTimeFormat, _cultureInfo.Value))
.Token("ShortTime", d => _dateLocalizationServices.ConvertToLocalizedString(d, _dateTimeFormats.ShortTimeFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false }))
// {Date.Long}
.Token("Long", d => d.ToString(_dateTimeLocalization.LongDateTimeFormat, _cultureInfo.Value))
.Token("Long", d => _dateLocalizationServices.ConvertToLocalizedString(d, _dateTimeFormats.LongDateTimeFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false }))
// {Date.LongDate}
.Token("LongDate", d => _dateLocalizationServices.ConvertToLocalizedString(d, _dateTimeFormats.LongDateFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false }))
// {Date.LongTime}
.Token("LongTime", d => _dateLocalizationServices.ConvertToLocalizedString(d, _dateTimeFormats.LongTimeFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false }))
// {Date}
.Token(
token => token == String.Empty ? String.Empty : null,
(token, d) => d.ToString(_dateTimeLocalization.ShortDateFormat + " " + _dateTimeLocalization.ShortTimeFormat, _cultureInfo.Value))
// {Date.Format:<formatstring>}
(token, d) => _dateLocalizationServices.ConvertToLocalizedString(d, new DateLocalizationOptions() { EnableTimeZoneConversion = false }))
// {Date.Format:<formatString>}
.Token(
token => token.StartsWith("Format:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Format:".Length) : null,
(token, d) => d.ToString(token, _cultureInfo.Value));
(token, d) => _dateLocalizationServices.ConvertToLocalizedString(d, token, new DateLocalizationOptions() { EnableTimeZoneConversion = false }));
}
private string DateTimeRelative(DateTime dateTimeUtc) {
var time = _clock.UtcNow - dateTimeUtc.ToUniversalTime();
if (time.TotalDays > 7)
return dateTimeUtc.ToString(T("'on' MMM d yyyy 'at' h:mm tt").ToString(), _cultureInfo.Value);
return _dateFormatter.FormatDateTime(DateTimeParts.FromDateTime(dateTimeUtc), T("'on' MMM d yyyy 'at' h:mm tt").ToString());
if (time.TotalHours > 24)
return T.Plural("1 day ago", "{0} days ago", time.Days).ToString();
if (time.TotalMinutes > 60)

View File

@@ -6,6 +6,7 @@ using Orchard.Localization.Services;
using Orchard.Services;
using Orchard.Tokens.Implementation;
using Orchard.Tokens.Providers;
using Orchard.Localization.Models;
namespace Orchard.Tokens.Tests {
[TestFixture]
@@ -13,6 +14,9 @@ namespace Orchard.Tokens.Tests {
private IContainer _container;
private ITokenizer _tokenizer;
private IClock _clock;
private IDateTimeFormatProvider _dateTimeFormats;
private IDateLocalizationServices _dateLocalizationServices;
private IDateFormatter _dateFormatter;
[SetUp]
public void Init() {
@@ -22,39 +26,107 @@ namespace Orchard.Tokens.Tests {
builder.RegisterType<Tokenizer>().As<ITokenizer>();
builder.RegisterType<DateTokens>().As<ITokenProvider>();
builder.RegisterType<StubClock>().As<IClock>();
builder.RegisterType<CultureDateTimeFormatProvider>().As<IDateTimeFormatProvider>();
builder.RegisterType<DefaultDateFormatter>().As<IDateFormatter>();
builder.RegisterType<DefaultDateLocalizationServices>().As<IDateLocalizationServices>();
builder.RegisterType<StubWorkContextAccessor>().As<IWorkContextAccessor>();
builder.RegisterType<SiteCalendarSelector>().As<ICalendarSelector>();
builder.RegisterType<DefaultCalendarManager>().As<ICalendarManager>();
builder.RegisterType<CultureDateTimeFormatProvider>().As<IDateTimeFormatProvider>();
builder.RegisterType<DefaultDateFormatter>().As<IDateFormatter>();
builder.RegisterType<DefaultDateLocalizationServices>().As<IDateLocalizationServices>();
_container = builder.Build();
_tokenizer = _container.Resolve<ITokenizer>();
_clock = _container.Resolve<IClock>();
_dateTimeFormats = _container.Resolve<IDateTimeFormatProvider>();
_dateLocalizationServices = _container.Resolve<IDateLocalizationServices>();
_dateFormatter = _container.Resolve<IDateFormatter>();
}
[Test]
public void TestDateTokens() {
var dateTimeLocalization = _container.Resolve<IDateTimeFormatProvider>();
var culture = CultureInfo.GetCultureInfo(_container.Resolve<IOrchardServices>().WorkContext.CurrentCulture);
var dateTimeFormat = dateTimeLocalization.ShortDateFormat + " " + dateTimeLocalization.ShortTimeFormat;
Assert.That(_tokenizer.Replace("{Date}", null), Is.EqualTo(_clock.UtcNow.ToString(dateTimeFormat, culture)));
Assert.That(_tokenizer.Replace("{Date}", new { Date = new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc) }), Is.EqualTo(new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc).ToString(dateTimeFormat, culture)));
public void TestDate() {
Assert.That(_tokenizer.Replace("{Date}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
Assert.That(_tokenizer.Replace("{Date}", new { Date = new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc) }), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc), new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
}
[Test]
public void TestFormat() {
Assert.That(_tokenizer.Replace("{Date.Format:yyyy}", null), Is.EqualTo(_clock.UtcNow.ToString("yyyy")));
}
[Test]
public void TestSince() {
var date = _clock.UtcNow.Subtract(TimeSpan.FromHours(25));
public void TestDateSince() {
var date = _clock.UtcNow.AddHours(-25);
Assert.That(_tokenizer.Replace("{Date.Since}", new { Date = date }), Is.EqualTo("1 day ago"));
}
[Test]
public void TestDateShort() {
Assert.That(_tokenizer.Replace("{Date.Short}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.ShortDateTimeFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
}
[Test]
public void TestDateShortDate() {
Assert.That(_tokenizer.Replace("{Date.ShortDate}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.ShortDateFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
}
[Test]
public void TestDateShortTime() {
Assert.That(_tokenizer.Replace("{Date.ShortTime}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.ShortTimeFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
}
[Test]
public void TestDateLong() {
Assert.That(_tokenizer.Replace("{Date.Long}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.LongDateTimeFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
}
[Test]
public void TestDateLongDate() {
Assert.That(_tokenizer.Replace("{Date.LongDate}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.LongDateFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
}
[Test]
public void TestDateLongTime() {
Assert.That(_tokenizer.Replace("{Date.LongTime}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.LongTimeFormat, new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
}
[Test]
public void TestDateFormat() {
Assert.That(_tokenizer.Replace("{Date.Format:yyyyMMdd}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, "yyyyMMdd", new DateLocalizationOptions() { EnableTimeZoneConversion = false })));
}
[Test]
public void TestDateLocal() {
Assert.That(_tokenizer.Replace("{Date.Local}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow)));
Assert.That(_tokenizer.Replace("{Date.Local}", new { Date = new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc) }), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc))));
}
[Test]
public void TestDateLocalShort() {
Assert.That(_tokenizer.Replace("{Date.Local.Short}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.ShortDateTimeFormat)));
}
[Test]
public void TestDateLocalShortDate() {
Assert.That(_tokenizer.Replace("{Date.Local.ShortDate}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.ShortDateFormat)));
}
[Test]
public void TestDateLocalShortTime() {
Assert.That(_tokenizer.Replace("{Date.Local.ShortTime}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.ShortTimeFormat)));
}
[Test]
public void TestDateLocalLong() {
Assert.That(_tokenizer.Replace("{Date.Local.Long}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.LongDateTimeFormat)));
}
[Test]
public void TestDateLocalLongDate() {
Assert.That(_tokenizer.Replace("{Date.Local.LongDate}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.LongDateFormat)));
}
[Test]
public void TestDateLocalLongTime() {
Assert.That(_tokenizer.Replace("{Date.Local.LongTime}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, _dateTimeFormats.LongTimeFormat)));
}
[Test]
public void TestDateLocalFormat() {
Assert.That(_tokenizer.Replace("{Date.Local.Format:yyyyMMdd}", null), Is.EqualTo(_dateLocalizationServices.ConvertToLocalizedString(_clock.UtcNow, "yyyyMMdd")));
}
}
}

View File

@@ -37,7 +37,7 @@ namespace Orchard.Tokens.Tests {
_initMethod(this);
}
_contextDictonary["CurrentTimeZone"] = TimeZoneInfo.Local;
_contextDictonary["CurrentTimeZone"] = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
_contextDictonary["CurrentCulture"] = "en-US";
_contextDictonary["CurrentCalendar"] = null;
}

View File

@@ -1,53 +1,12 @@
DateTime? ConvertToSiteTimeZone(DateTime? date)
DateTime? ConvertFromSiteTimeZone(DateTime? date)
- Also provides overloads with non-nullable DateTime parameters for compatibility with existing callers.
- Does only time zone conversion. No calendar conversion.
DateTimeParts? ConvertToSiteCalendar(DateTime? date)
DateTime? ConvertFromSiteCalendar(Parts? dateParts)
- Does only calendar conversion. No time zone conversion.
- Instantiates the site calendar and does "manual" conversion.
- Uses a custom storage structure DateTimeParts for non-gregorian representation.
string ConvertToLocalizedDateString(DateTime? date, DateLocalizationOptions options = DateLocalizationOptions.Default)
string ConvertToLocalizedTimeString(DateTime? date, DateLocalizationOptions options = DateLocalizationOptions.Default)
string ConvertToLocalizedString(DateTime? date, DateLocalizationOptions options)
string ConvertToLocalizedString(DateTime? date, string format, DateLocalizationOptions options)
- Also provides overloads with non-nullable DateTime parameters for compatibility with existing callers.
- By default performs the full conversion (time zone, calendar and formatting). Can be overridden by DateLocalizationOptions.
- If NOT ConvertCalendar, uses DateTime.ToString() with an cloned CultureInfo using GregorianCalendar of type Localized.
- If ConvertCalendar and UNSUPPORTED non-default calendar, uses ConvertToSiteCalendar() and Mahsa's custom formatting.
- If ConvertCalendar and SUPPORTED non-default calendar, uses DateTime.ToString() with cloned CultureInfo using an optional calendar instance, which does both calendar conversion and formatting.
- Otherwise uses DateTime.ToString() with configured CultureInfo which does both calendar conversion and formatting.
DateTime? ConvertFromLocalizedDateString(string dateString, DateLocalizationOptions options = DateLocalizationOptions.Default)
DateTime? ConvertFromLocalizedTimeString(string timeString, DateLocalizationOptions options = DateLocalizationOptions.Default)
DateTime? ConvertFromLocalizedString(string dateTimeString, DateLocalizationOptions options = DateLocalizationOptions.Default)
DateTime? ConvertFromLocalizedString(string dateString, string timeString, DateLocalizationOptions options = DateLocalizationOptions.Default)
- By default performs the full conversion (parsing, calendar and time zone). Can be overridden by DateLocalizationOptions.
- ConvertFromLocalizedDateString() returns a DateTime? with the time component set to 00:00:00. ConvertTimeZone is ignored.
- ConvertFromLocalizedTimeString() returns a DateTime? with the date component set to DateTime.MinValue. ConvertCalendar is ignored.
- If NOT ConvertCalendar, uses DateTime.Parse() with an cloned CultureInfo using GregorianCalendar of type Localized.
- If ConvertCalendar and UNSUPPORTED non-default calendar, uses Mahsa's custom parsing and ConvertToSiteCalendar().
- If ConvertCalendar and SUPPORTED non-default calendar, uses DateTime.Parse() with cloned CultureInfo using an optional calendar instance, which does both parsing and calendar conversion.
- Otherwise uses DateTime.Parse() with configured CultureInfo which does both parsing and calendar conversion.
- ConvertFromLocalizedString() with dateString or timeString set to null is the same as ConvertFromLocalizedTimeString() and ConvertFromLocalizedDateString() respectively.
- Checks if the parsed string matches NullText, and if so returns null.
struct DateLocalizationOptions {
bool ConvertTimeZone;
bool ConvertCalendar;
string NullText;
}
TODO:
* Test for proper handling of fraction (f) format specifier - suspect it does not work properly in current state
* Add formatting and parsing of time zone information (add timezone properties to TimeParts structure)
* Write unit tests for DefaultDateLocalizationServices
* Add warning message when saving unsupported combination in settings
* Add support for the different Gregorian calendar types
* Go over date conversion logic in audit trail filtering to make sure it's correct
* Go over date conversion logic in DateTokens to make sure it's correct
* Improve DateTimeField:
- Surface the field mode (date, time or both)
- Do not perform time-zone conversion in date-only mode
* Test for proper handling of fraction (f) format specifier - suspect it does not work properly in current state
* Add formatting and parsing of time zone information (add timezone properties to TimeParts structure)
* 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
BREAKING:
* DateTokens "Date.Format:<formatString>" and "Date.Local.Format:<formatString>" only supports custom date/time format strings.