Adding TimeZone

--HG--
branch : 1.x
This commit is contained in:
Sebastien Ros
2012-01-05 14:49:10 -08:00
parent 07dc0197ad
commit b1dc3b21c1
28 changed files with 351 additions and 56 deletions

View File

@@ -278,6 +278,7 @@
<Compile Include="Stubs\StubVirtualPathMonitor.cs" />
<Compile Include="Stubs\StubCacheManager.cs" />
<Compile Include="Stubs\StubWebSiteFolder.cs" />
<Compile Include="Time\TimeZoneSelectorTests.cs" />
<Compile Include="UI\Resources\ResourceManagerTests.cs" />
<Compile Include="UI\ShapeTests.cs" />
<Compile Include="Utility\ContainerExtensions.cs" />

View File

@@ -77,6 +77,8 @@ namespace Orchard.Tests.Stubs {
}
public string BaseUrl { get; set;}
public string SiteTimeZone { get; set; }
}
public class StubUser : IUser {

View File

@@ -0,0 +1,85 @@
using System;
using System.Web;
using Autofac;
using NUnit.Framework;
using Orchard.Tests.Stubs;
using Orchard.Time;
namespace Orchard.Tests.Time {
[TestFixture]
public class TimeZoneProviderTests {
private IContainer _container;
private IWorkContextStateProvider _workContextStateProvider;
private TestTimeZoneSelector _timeZoneSelector;
[SetUp]
public void Init() {
var builder = new ContainerBuilder();
builder.RegisterInstance(_timeZoneSelector = new TestTimeZoneSelector()).As<ITimeZoneSelector>();
builder.RegisterType<CurrentTimeZoneWorkContext>().As<IWorkContextStateProvider>();
builder.RegisterType<FallbackTimeZoneSelector>().As<ITimeZoneSelector>();
builder.RegisterType<SiteTimeZoneSelector>().As<ITimeZoneSelector>();
builder.RegisterType<StubWorkContextAccessor>().As<IWorkContextAccessor>();
_container = builder.Build();
_workContextStateProvider = _container.Resolve<IWorkContextStateProvider>();
}
[Test]
public void ShouldProvideCurrentTimeZoneOnly() {
_timeZoneSelector.TimeZone = null;
var timeZone = _workContextStateProvider.Get<TimeZoneInfo>("Foo");
Assert.That(timeZone, Is.Null);
}
[Test]
public void DefaultTimeZoneIsLocal() {
_timeZoneSelector.Priority = -200;
var timeZone = _workContextStateProvider.Get<TimeZoneInfo>("CurrentTimeZone");
Assert.That(timeZone(new StubWorkContext()), Is.EqualTo(TimeZoneInfo.Local));
}
[Test]
public void TimeZoneProviderReturnsTimeZoneFromSelector() {
_timeZoneSelector.Priority = 999;
_timeZoneSelector.TimeZone = TimeZoneInfo.Utc;
var timeZone = _workContextStateProvider.Get<TimeZoneInfo>("CurrentTimeZone");
Assert.That(timeZone(new StubWorkContext()), Is.EqualTo(TimeZoneInfo.Utc));
}
}
public class TestTimeZoneSelector : ITimeZoneSelector {
public TimeZoneInfo TimeZone { get; set; }
public int Priority { get; set; }
public TimeZoneSelectorResult GetTimeZone(HttpContextBase context) {
return new TimeZoneSelectorResult {
Priority = Priority,
TimeZone= TimeZone
};
}
}
public class StubWorkContext : WorkContext {
public override T Resolve<T>() {
throw new NotImplementedException();
}
public override bool TryResolve<T>(out T service) {
throw new NotImplementedException();
}
public override T GetState<T>(string name) {
return default(T);
}
public override void SetState<T>(string name, T value) {
}
}
}

View File

@@ -23,12 +23,12 @@ namespace Orchard.Core.Common {
}
[Shape]
public IHtmlString PublishedState(HtmlHelper html, DateTime createdDateTimeUtc, DateTime? publisheddateTimeUtc) {
public IHtmlString PublishedState(dynamic Display, DateTime createdDateTimeUtc, DateTime? publisheddateTimeUtc) {
if (!publisheddateTimeUtc.HasValue) {
return T("Draft");
}
return html.DateTime(createdDateTimeUtc);
return Display.DateTime(DateTimeUtc: createdDateTimeUtc);
}
[Shape]

View File

@@ -1,4 +1,5 @@
using System.Net;
using System.Linq;
using System.Net;
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
@@ -44,7 +45,8 @@ namespace Orchard.Core.Settings.Drivers {
var model = new SiteSettingsPartViewModel {
Site = site,
SiteCultures = _cultureManager.ListCultures()
SiteCultures = _cultureManager.ListCultures(),
TimeZones = TimeZoneInfo.GetSystemTimeZones()
};
return ContentShape("Parts_Settings_SiteSettingsPart",
@@ -55,7 +57,8 @@ namespace Orchard.Core.Settings.Drivers {
var site = _siteService.GetSiteSettings().As<SiteSettingsPart>();
var model = new SiteSettingsPartViewModel {
Site = site,
SiteCultures = _cultureManager.ListCultures()
SiteCultures = _cultureManager.ListCultures(),
TimeZones = TimeZoneInfo.GetSystemTimeZones()
};
var previousBaseUrl = model.Site.BaseUrl;

View File

@@ -91,9 +91,16 @@ namespace Orchard.Core.Settings {
.Column<string>("SiteCulture")
.Column<string>("ResourceDebugMode", c => c.WithDefault("FromAppSetting"))
.Column<int>("PageSize")
.Column<string>("SiteTimeZone")
);
return 1;
SchemaBuilder.CreateTable("SiteSettings2PartRecord",
table => table
.ContentPartRecord()
.Column<string>("BaseUrl", c => c.Unlimited())
);
return 3;
}
public int UpdateFrom1() {
@@ -105,5 +112,14 @@ namespace Orchard.Core.Settings {
return 2;
}
public int UpdateFrom2() {
SchemaBuilder.AlterTable("SiteSettingsPartRecord",
table => table
.AddColumn<string>("SiteTimeZone")
);
return 3;
}
}
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using Orchard.ContentManagement;
using Orchard.Data.Conventions;
using Orchard.Settings;
@@ -45,6 +46,11 @@ namespace Orchard.Core.Settings.Models {
set { Record.PageSize = value; }
}
public string SiteTimeZone {
get { return Record.SiteTimeZone; }
set { Record.SiteTimeZone = value; }
}
[StringLengthMax]
public string BaseUrl {
get {

View File

@@ -1,4 +1,5 @@
using Orchard.ContentManagement.Records;
using System;
using Orchard.ContentManagement.Records;
using Orchard.Settings;
namespace Orchard.Core.Settings.Models {
@@ -24,5 +25,7 @@ namespace Orchard.Core.Settings.Models {
public virtual ResourceDebugMode ResourceDebugMode { get; set; }
public virtual int PageSize { get; set; }
public virtual string SiteTimeZone { get; set; }
}
}

View File

@@ -36,6 +36,7 @@ namespace Orchard.Core.Settings.Services {
item.Record.SiteSalt = Guid.NewGuid().ToString("N");
item.Record.SiteName = "My Orchard Project Application";
item.Record.PageTitleSeparator = " - ";
item.Record.SiteTimeZone = TimeZoneInfo.Local.Id;
}).ContentItem;
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Orchard.Core.Settings.Models;
using Orchard.Settings;
@@ -7,6 +8,7 @@ namespace Orchard.Core.Settings.ViewModels {
public class SiteSettingsPartViewModel {
public SiteSettingsPart Site { get; set; }
public IEnumerable<string> SiteCultures { get; set; }
public IEnumerable<TimeZoneInfo> TimeZones { get; set; }
[HiddenInput(DisplayValue = false)]
public int Id {
@@ -47,5 +49,10 @@ namespace Orchard.Core.Settings.ViewModels {
get { return Site.BaseUrl; }
set { Site.BaseUrl = value; }
}
public string TimeZone {
get { return Site.SiteTimeZone; }
set { Site.SiteTimeZone = value; }
}
}
}

View File

@@ -26,6 +26,12 @@
@Html.ValidationMessage("SiteCulture", "*")
<p>@Html.ActionLink(T("Add or remove supported cultures for the site.").ToString(), "Culture")</p>
</div>
<div>
<label for="TimeZone">@T("Default Time Zone")</label>
@Html.DropDownList("TimeZone", new[] { new SelectListItem { Text = T("Local to server").Text, Value = "" } }.Union(new SelectList(Model.TimeZones, "Id", "", Model.TimeZone)))
@Html.ValidationMessage("TimeZone", "*")
<span class="hint">@T("Determines the default time zone which will should be used to display date and times.")</span>
</div>
<div>
<label for="PageTitleSeparator">@T("Page title separator")</label>
@Html.EditorFor(x => x.PageTitleSeparator)

View File

@@ -7,22 +7,27 @@ using Orchard.Mvc.Html;
using Orchard.Services;
namespace Orchard.Core.Shapes {
public class DateTimeShapes : ISingletonDependency {
public class DateTimeShapes : IDependency {
private readonly IClock _clock;
private readonly IWorkContextAccessor _workContextAccessor;
public DateTimeShapes(IClock clock) {
public DateTimeShapes(
IClock clock,
IWorkContextAccessor workContextAccessor
) {
_clock = clock;
_workContextAccessor = workContextAccessor;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
[Shape]
public IHtmlString DateTimeRelative(HtmlHelper Html, DateTime dateTimeUtc) {
public IHtmlString DateTimeRelative(dynamic Display, DateTime dateTimeUtc) {
var time = _clock.UtcNow - dateTimeUtc;
if (time.TotalDays > 7)
return Html.DateTime(dateTimeUtc.ToLocalTime(), T("'on' MMM d yyyy 'at' h:mm tt"));
return Display.DateTime(DateTimeUtc: dateTimeUtc, CustomFormat: T("'on' MMM d yyyy 'at' h:mm tt"));
if (time.TotalHours > 24)
return T.Plural("1 day ago", "{0} days ago", time.Days);
if (time.TotalMinutes > 60)
@@ -34,5 +39,30 @@ namespace Orchard.Core.Shapes {
return T("a moment ago");
}
[Shape]
public IHtmlString DateTime(DateTime DateTimeUtc, LocalizedString CustomFormat) {
//using a LocalizedString forces the caller to use a localizable format
if (CustomFormat == null || String.IsNullOrWhiteSpace(CustomFormat.Text)) {
return DateTime(DateTimeUtc, T("MMM d yyyy h:mm tt"));
}
return new MvcHtmlString(ConvertToDisplayTime(DateTimeUtc).ToString(CustomFormat.Text));
}
/// <summary>
/// Converts a Coordinated Universal Time (UTC) to the time in the current time zone.
/// </summary>
/// <param name="dateTimeUtc">The Coordinated Universal Time (UTC).</param>
/// <returns>The date and time in the selected time zone. Its System.DateTime.Kind property is System.DateTimeKind.Utc if the current zone is System.TimeZoneInfo.Utc; otherwise, its System.DateTime.Kind property is System.DateTimeKind.Unspecified.</returns>
private DateTime ConvertToDisplayTime(DateTime dateTimeUtc) {
// get the time zone for the current request
var timeZone = _workContextAccessor.GetContext().CurrentTimeZone;
return TimeZoneInfo.ConvertTimeFromUtc(dateTimeUtc, timeZone);
}
}
}

View File

@@ -4,7 +4,7 @@
<ul class="pageStatus">
<li>
<img class="icon" src="@Href("~/Modules/Orchard.ArchiveLater/Content/Admin/images/scheduled.gif")" alt="@T("Scheduled")" title="@T("The page is scheduled for archiving")" />@T("Unpublish on")
@Html.DateTime((DateTime)Model.ScheduledArchiveUtc.ToLocalTime(), T("M/d/yyyy h:mm tt"))
@Display.DateTime(DateTimeUtc: (DateTime)Model.ScheduledArchiveUtc.ToLocalTime(), CustomFormat: T("M/d/yyyy h:mm tt"))
&nbsp;&#124;&nbsp;
</li>
</ul>

View File

@@ -83,7 +83,7 @@
@text
}
</td>
<td>@Html.DateTime(commentEntry.Comment.CommentDateUtc.GetValueOrDefault())</td>
<td>@Display.DateTime(DateTimeUtc: commentEntry.Comment.CommentDateUtc.GetValueOrDefault())</td>
<td>
<ul class="actions">
<li class="construct">

View File

@@ -77,7 +77,7 @@
</td>
<td>
@* would ideally have permalinks for individual comments *@
<p><a href="@Url.ItemDisplayUrl(commentEntry.CommentedOn)#comments"><time>@Html.DateTime(commentEntry.Comment.CommentDateUtc.GetValueOrDefault())</time></a></p>
<p><a href="@Url.ItemDisplayUrl(commentEntry.CommentedOn)#comments"><time>@Display.DateTime(DateTimeUtc: commentEntry.Comment.CommentDateUtc.GetValueOrDefault())</time></a></p>
@if (commentEntry.Comment.CommentText != null) {
var ellipsized = Html.Ellipsize(commentEntry.Comment.CommentText, 500);
var paragraphed = new HtmlString(ellipsized.ToHtmlString().Replace("\r\n", "</p><p>"));

View File

@@ -30,7 +30,7 @@
}
else {
<img class="icon" src="@Href("~/Modules/Orchard.PublishLater/Content/Admin/images/scheduled.gif")" alt="@T("Scheduled")" title="@T("The page is scheduled for publishing")" /><text> @T("Scheduled") </text>
@Html.DateTime(((DateTime?)Model.ScheduledPublishUtc).Value.ToLocalTime(), T("M/d/yyyy h:mm tt"))
@Display.DateTime(((DateTime?)Model.ScheduledPublishUtc).Value.ToLocalTime(), T("M/d/yyyy h:mm tt"))
}&nbsp;&#124;&nbsp;</li>
}
</ul>

View File

@@ -188,6 +188,10 @@ namespace Orchard.Setup {
public string BaseUrl {
get { return ""; }
}
public string SiteTimeZone {
get { return TimeZoneInfo.Local.Id; }
}
}
}
}

View File

@@ -165,28 +165,6 @@ namespace Orchard.Mvc.Html {
#endregion
#region Format Date/Time
public static LocalizedString DateTime(this HtmlHelper htmlHelper, DateTime? value, LocalizedString defaultIfNull) {
return value.HasValue ? htmlHelper.DateTime(value.Value) : defaultIfNull;
}
public static LocalizedString DateTime(this HtmlHelper htmlHelper, DateTime? value, LocalizedString defaultIfNull, LocalizedString customFormat) {
return value.HasValue ? htmlHelper.DateTime(value.Value, customFormat) : defaultIfNull;
}
public static LocalizedString DateTime(this HtmlHelper htmlHelper, DateTime value) {
//TODO: (erikpo) This default format should come from a site setting
return htmlHelper.DateTime(value.ToLocalTime(), new LocalizedString("MMM d yyyy h:mm tt")); //todo: above comment and get rid of just wrapping this as a localized string
}
public static LocalizedString DateTime(this HtmlHelper htmlHelper, DateTime value, LocalizedString customFormat) {
//TODO: (erikpo) In the future, convert this to "local" time before calling ToString
return new LocalizedString(value.ToString(customFormat.Text));
}
#endregion
#region Image
public static MvcHtmlString Image(this HtmlHelper htmlHelper, string src, string alt, object htmlAttributes) {

View File

@@ -276,10 +276,16 @@
<Compile Include="Security\IEncryptionService.cs" />
<Compile Include="Security\CurrentUserWorkContext.cs" />
<Compile Include="Security\Providers\DefaultEncryptionService.cs" />
<Compile Include="Services\Clock.cs" />
<Compile Include="Settings\CurrentSiteWorkContext.cs" />
<Compile Include="Settings\ResourceDebugMode.cs" />
<Compile Include="Themes\CurrentThemeWorkContext.cs" />
<Compile Include="Themes\ThemeManager.cs" />
<Compile Include="Time\CurrentTimeZoneWorkContext.cs" />
<Compile Include="Time\FallbackTimeZoneSelector.cs" />
<Compile Include="Time\ITimeZoneSelector.cs" />
<Compile Include="Time\SiteTimeZoneSelector.cs" />
<Compile Include="Time\TimeZoneSelectorResult.cs" />
<Compile Include="UI\FlatPositionComparer.cs" />
<Compile Include="UI\Navigation\Pager.cs" />
<Compile Include="UI\Navigation\PagerParameters.cs" />
@@ -884,7 +890,7 @@
<Compile Include="Security\Permissions\IPermissionProvider.cs" />
<Compile Include="Security\Permissions\Permission.cs" />
<Compile Include="Security\Providers\OrchardRoleProvider.cs" />
<Compile Include="Services\Clock.cs" />
<Compile Include="Services\IClock.cs" />
<Compile Include="FileSystems\Media\IStorageFile.cs" />
<Compile Include="FileSystems\Media\IStorageFolder.cs" />
<Compile Include="FileSystems\Media\IStorageProvider.cs" />

View File

@@ -2,20 +2,6 @@
using Orchard.Caching;
namespace Orchard.Services {
public interface IClock : IVolatileProvider {
DateTime UtcNow { get; }
/// <summary>
/// Each retrieved value is cached during the specified amount of time.
/// </summary>
IVolatileToken When(TimeSpan duration);
/// <summary>
/// The cache is active until the specified time. Each subsequent access won't be cached.
/// </summary>
IVolatileToken WhenUtc(DateTime absoluteUtc);
}
public class Clock : IClock {
public DateTime UtcNow {
get { return DateTime.UtcNow; }

View File

@@ -0,0 +1,56 @@
using System;
using Orchard.Caching;
namespace Orchard.Services {
/// <summary>
/// Provides the current Utc <see cref="DateTime"/>, and time related method for cache management.
/// This service should be used whenever the current date and time are needed, instead of <seealso cref="DateTime"/> directly.
/// It also makes implementations more testable, as time can be mocked.
/// </summary>
public interface IClock : IVolatileProvider {
/// <summary>
/// Gets the current <see cref="DateTime"/> of the system, expressed in Utc
/// </summary>
DateTime UtcNow { get; }
/// <summary>
/// Provides a <see cref="IVolatileToken"/> instance which can be used to cache some information for a
/// specific duration.
/// </summary>
/// <param name="duration">The duration that the token must be valid.</param>
/// <example>
/// This sample shows how to use the <see cref="When"/> method by returning the result of
/// a method named LoadVotes(), which is computed every 10 minutes only.
/// <code>
/// _cacheManager.Get("votes",
/// ctx => {
/// ctx.Monitor(_clock.When(TimeSpan.FromMinutes(10)));
/// return LoadVotes();
/// });
/// </code>
/// </example>
IVolatileToken When(TimeSpan duration);
/// <summary>
/// Provides a <see cref="IVolatileToken"/> instance which can be used to cache some
/// until a specific date and time.
/// </summary>
/// <param name="absoluteUtc">The date and time that the token must be valid until.</param>
/// <example>
/// This sample shows how to use the <see cref="WhenUtc"/> method by returning the result of
/// a method named LoadVotes(), which is computed once, and no more until the end of the year.
/// <code>
/// var endOfYear = _clock.UtcNow;
/// endOfYear.Month = 12;
/// endOfYear.Day = 31;
///
/// _cacheManager.Get("votes",
/// ctx => {
/// ctx.Monitor(_clock.WhenUtc(endOfYear));
/// return LoadVotes();
/// });
/// </code>
/// </example>
IVolatileToken WhenUtc(DateTime absoluteUtc);
}
}

View File

@@ -1,4 +1,5 @@
using Orchard.ContentManagement;
using System;
using Orchard.ContentManagement;
namespace Orchard.Settings {
/// <summary>
@@ -14,5 +15,6 @@ namespace Orchard.Settings {
ResourceDebugMode ResourceDebugMode { get; set; }
int PageSize { get; set; }
string BaseUrl { get; }
string SiteTimeZone { get; }
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Orchard.Time {
public class CurrentTimeZoneWorkContext : IWorkContextStateProvider {
private readonly IEnumerable<ITimeZoneSelector> _timeZoneSelectors;
public CurrentTimeZoneWorkContext(IEnumerable<ITimeZoneSelector> timeZoneSelectors) {
_timeZoneSelectors = timeZoneSelectors;
}
public Func<WorkContext, T> Get<T>(string name) {
if (name == "CurrentTimeZone") {
return ctx => (T)(object)CurrentTimeZone(ctx.HttpContext);
}
return null;
}
TimeZoneInfo CurrentTimeZone(HttpContextBase httpContext) {
var timeZone = _timeZoneSelectors
.Select(x => x.GetTimeZone(httpContext))
.Where(x => x != null)
.OrderByDescending(x => x.Priority)
.FirstOrDefault();
if (timeZone == null) {
return null;
}
return timeZone.TimeZone;
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Web;
namespace Orchard.Time {
/// <summary>
/// Implements <see cref="ITimeZoneSelector"/> by providing the timezone defined in the machine's local settings.
/// </summary>
public class FallbackTimeZoneSelector : ITimeZoneSelector {
public TimeZoneSelectorResult GetTimeZone(HttpContextBase context) {
return new TimeZoneSelectorResult {
Priority = -100,
TimeZone = TimeZoneInfo.Local
};
}
}
}

View File

@@ -0,0 +1,7 @@
using System.Web;
namespace Orchard.Time {
public interface ITimeZoneSelector : IDependency {
TimeZoneSelectorResult GetTimeZone(HttpContextBase context);
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Web;
namespace Orchard.Time {
/// <summary>
/// Implements <see cref="ITimeZoneSelector"/> by providing the timezone defined in the sites settings.
/// </summary>
public class SiteTimeZoneSelector : ITimeZoneSelector {
private readonly IWorkContextAccessor _workContextAccessor;
public SiteTimeZoneSelector(IWorkContextAccessor workContextAccessor) {
_workContextAccessor = workContextAccessor;
}
public TimeZoneSelectorResult GetTimeZone(HttpContextBase context) {
var siteTimeZoneId = _workContextAccessor.GetContext(context).CurrentSite.SiteTimeZone;
if (String.IsNullOrEmpty(siteTimeZoneId)) {
return null;
}
return new TimeZoneSelectorResult {
Priority = -5,
TimeZone = TimeZoneInfo.FindSystemTimeZoneById(siteTimeZoneId)
};
}
}
}

View File

@@ -0,0 +1,8 @@
using System;
namespace Orchard.Time {
public class TimeZoneSelectorResult {
public int Priority { get; set; }
public TimeZoneInfo TimeZone { get; set; }
}
}

View File

@@ -1,4 +1,5 @@
using System.Web;
using System;
using System.Web;
using Orchard.Environment.Extensions.Models;
using Orchard.Security;
using Orchard.Settings;
@@ -75,5 +76,13 @@ namespace Orchard {
get { return GetState<string>("CurrentCulture"); }
set { SetState("CurrentCulture", value); }
}
/// <summary>
/// Time zone of the work context
/// </summary>
public TimeZoneInfo CurrentTimeZone {
get { return GetState<TimeZoneInfo>("CurrentTimeZone"); }
set { SetState("CurrentTimeZone", value); }
}
}
}