feat: handle timezones and conversions with NodaTime
This commit is contained in:
@ -54,7 +54,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
|
||||
services.AddScoped<ICookieConsentService, CookieConsentService>();
|
||||
services.AddScoped<IOutputCacheService, OutputCacheService>();
|
||||
services.AddScoped<ITimeZoneService, TimeZoneService>();
|
||||
|
||||
// providers
|
||||
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
|
||||
|
@ -3,7 +3,6 @@
|
||||
@inherits ModuleBase
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IUserService UserService
|
||||
@inject ITimeZoneService TimeZoneService
|
||||
@inject IStringLocalizer<Index> Localizer
|
||||
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
||||
@inject ISettingService SettingService
|
||||
@ -115,7 +114,7 @@
|
||||
{
|
||||
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
|
||||
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
|
||||
_timezones = await TimeZoneService.GetTimeZonesAsync();
|
||||
_timezones = Utilities.GetTimeZones();
|
||||
_timezoneid = PageState.Site.TimeZoneId;
|
||||
_initialized = true;
|
||||
}
|
||||
|
@ -10,7 +10,6 @@
|
||||
@inject IAliasService AliasService
|
||||
@inject IThemeService ThemeService
|
||||
@inject ISettingService SettingService
|
||||
@inject ITimeZoneService TimeZoneService
|
||||
@inject IServiceProvider ServiceProvider
|
||||
@inject IStringLocalizer<Index> Localizer
|
||||
@inject INotificationService NotificationService
|
||||
@ -508,7 +507,7 @@
|
||||
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
|
||||
if (site != null)
|
||||
{
|
||||
_timezones = await TimeZoneService.GetTimeZonesAsync();
|
||||
_timezones = Utilities.GetTimeZones();
|
||||
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
|
||||
|
||||
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
|
||||
|
@ -9,7 +9,6 @@
|
||||
@inject INotificationService NotificationService
|
||||
@inject IFileService FileService
|
||||
@inject IFolderService FolderService
|
||||
@inject ITimeZoneService TimeZoneService
|
||||
@inject IJSRuntime jsRuntime
|
||||
@inject IServiceProvider ServiceProvider
|
||||
@inject IStringLocalizer<Index> Localizer
|
||||
@ -404,7 +403,7 @@
|
||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
||||
_allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
|
||||
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
|
||||
_timezones = await TimeZoneService.GetTimeZonesAsync();
|
||||
_timezones = Utilities.GetTimeZones();
|
||||
|
||||
if (PageState.User != null)
|
||||
{
|
||||
|
@ -5,7 +5,6 @@
|
||||
@inject IUserService UserService
|
||||
@inject IProfileService ProfileService
|
||||
@inject ISettingService SettingService
|
||||
@inject ITimeZoneService TimeZoneService
|
||||
@inject IStringLocalizer<Add> Localizer
|
||||
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
||||
|
||||
@ -133,7 +132,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
_timezones = await TimeZoneService.GetTimeZonesAsync();
|
||||
_timezones = Utilities.GetTimeZones();
|
||||
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
|
||||
_settings = new Dictionary<string, string>();
|
||||
_timezoneid = PageState.Site.TimeZoneId;
|
||||
|
@ -6,7 +6,6 @@
|
||||
@inject IProfileService ProfileService
|
||||
@inject ISettingService SettingService
|
||||
@inject IFileService FileService
|
||||
@inject ITimeZoneService TimeZoneService
|
||||
@inject IServiceProvider ServiceProvider
|
||||
@inject IStringLocalizer<Edit> Localizer
|
||||
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
||||
@ -204,7 +203,7 @@
|
||||
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
|
||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
||||
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
|
||||
_timezones = await TimeZoneService.GetTimeZonesAsync();
|
||||
_timezones = Utilities.GetTimeZones();
|
||||
|
||||
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
|
||||
{
|
||||
|
@ -507,24 +507,18 @@ namespace Oqtane.Modules
|
||||
if (datetime == null)
|
||||
return null;
|
||||
|
||||
TimeZoneInfo timezone = null;
|
||||
try
|
||||
string timezoneId = null;
|
||||
|
||||
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
|
||||
{
|
||||
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
|
||||
{
|
||||
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
|
||||
{
|
||||
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId);
|
||||
}
|
||||
timezoneId = PageState.User.TimeZoneId;
|
||||
}
|
||||
catch
|
||||
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
|
||||
{
|
||||
// The time zone ID was not found on the local computer
|
||||
timezoneId = PageState.Site.TimeZoneId;
|
||||
}
|
||||
|
||||
return Utilities.UtcAsLocalDateTime(datetime, timezone);
|
||||
return Utilities.UtcAsLocalDateTime(datetime, timezoneId);
|
||||
}
|
||||
|
||||
public DateTime? LocalToUtc(DateTime? datetime)
|
||||
@ -533,24 +527,18 @@ namespace Oqtane.Modules
|
||||
if (datetime == null)
|
||||
return null;
|
||||
|
||||
TimeZoneInfo timezone = null;
|
||||
try
|
||||
string timezoneId = null;
|
||||
|
||||
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
|
||||
{
|
||||
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
|
||||
{
|
||||
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
|
||||
{
|
||||
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId);
|
||||
}
|
||||
timezoneId = PageState.User.TimeZoneId;
|
||||
}
|
||||
catch
|
||||
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
|
||||
{
|
||||
// The time zone ID was not found on the local computer
|
||||
timezoneId = PageState.Site.TimeZoneId;
|
||||
}
|
||||
|
||||
return Utilities.LocalDateAndTimeAsUtc(datetime, timezone);
|
||||
return Utilities.LocalDateAndTimeAsUtc(datetime, timezoneId);
|
||||
}
|
||||
|
||||
// logging methods
|
||||
|
@ -1,18 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service to store and retrieve <see cref="TimeZone"/> entries
|
||||
/// </summary>
|
||||
public interface ITimeZoneService
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the list of time zones
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<List<TimeZone>> GetTimeZonesAsync();
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Oqtane.Documentation;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
|
||||
public class TimeZoneService : ServiceBase, ITimeZoneService
|
||||
{
|
||||
public TimeZoneService(HttpClient http, SiteState siteState) : base(http, siteState) { }
|
||||
|
||||
private string Apiurl => CreateApiUrl("TimeZone");
|
||||
|
||||
public async Task<List<TimeZone>> GetTimeZonesAsync()
|
||||
{
|
||||
return await GetJsonAsync<List<TimeZone>>($"{Apiurl}");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Controllers
|
||||
{
|
||||
[Route(ControllerRoutes.ApiRoute)]
|
||||
public class TimeZoneController : Controller
|
||||
{
|
||||
public TimeZoneController() {}
|
||||
|
||||
// GET: api/<controller>
|
||||
[HttpGet]
|
||||
public IEnumerable<Models.TimeZone> Get()
|
||||
{
|
||||
return TimeZoneInfo.GetSystemTimeZones()
|
||||
.Select(item => new Models.TimeZone
|
||||
{
|
||||
Id = item.Id,
|
||||
DisplayName = item.DisplayName
|
||||
})
|
||||
.OrderBy(item => item.DisplayName);
|
||||
}
|
||||
}
|
||||
}
|
@ -104,7 +104,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
services.AddScoped<ISearchProvider, DatabaseSearchProvider>();
|
||||
services.AddScoped<IImageService, ImageService>();
|
||||
services.AddScoped<ICookieConsentService, ServerCookieConsentService>();
|
||||
services.AddScoped<ITimeZoneService, TimeZoneService>();
|
||||
|
||||
// providers
|
||||
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
|
||||
|
@ -22,6 +22,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
@ -1,4 +1,3 @@
|
||||
using Oqtane.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@ -7,7 +6,14 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using NodaTime;
|
||||
using NodaTime.Extensions;
|
||||
|
||||
using Oqtane.Models;
|
||||
|
||||
using File = Oqtane.Models.File;
|
||||
using TimeZone = Oqtane.Models.TimeZone;
|
||||
|
||||
namespace Oqtane.Shared
|
||||
{
|
||||
@ -505,6 +511,7 @@ namespace Oqtane.Shared
|
||||
return $"[{@class.GetType()}] {message}";
|
||||
}
|
||||
|
||||
//Time conversions with TimeZoneInfo
|
||||
public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, string time, TimeZoneInfo localTimeZone = null)
|
||||
{
|
||||
if (date != null && !string.IsNullOrEmpty(time) && TimeSpan.TryParse(time, out TimeSpan timespan))
|
||||
@ -581,6 +588,120 @@ namespace Oqtane.Shared
|
||||
|
||||
return (localDateTime?.Date, localTime);
|
||||
}
|
||||
|
||||
//Time conversions with NodaTime (IANA) timezoneId
|
||||
public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, string time, string localTimeZoneId)
|
||||
{
|
||||
if (date != null && !string.IsNullOrEmpty(time) && TimeSpan.TryParse(time, out TimeSpan timespan))
|
||||
{
|
||||
return LocalDateAndTimeAsUtc(date.Value.Date.Add(timespan), localTimeZoneId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, DateTime? time, string localTimeZoneId)
|
||||
{
|
||||
if (date != null)
|
||||
{
|
||||
if (time != null)
|
||||
{
|
||||
return LocalDateAndTimeAsUtc(date.Value.Date.Add(time.Value.TimeOfDay), localTimeZoneId);
|
||||
}
|
||||
return LocalDateAndTimeAsUtc(date.Value.Date, localTimeZoneId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, string localTimeZoneId)
|
||||
{
|
||||
if (date != null)
|
||||
{
|
||||
DateTimeZone localTimeZone;
|
||||
|
||||
if (!string.IsNullOrEmpty(localTimeZoneId))
|
||||
{
|
||||
localTimeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(localTimeZoneId) ?? DateTimeZoneProviders.Tzdb.GetSystemDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
localTimeZone = DateTimeZoneProviders.Tzdb.GetSystemDefault();
|
||||
}
|
||||
|
||||
var localDateTime = LocalDateTime.FromDateTime(date.Value);
|
||||
return localTimeZone.AtLeniently(localDateTime).ToDateTimeUtc();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static DateTime? UtcAsLocalDate(DateTime? dateTime, string timeZoneId)
|
||||
{
|
||||
return UtcAsLocalDateAndTime(dateTime, timeZoneId).date;
|
||||
}
|
||||
|
||||
public static DateTime? UtcAsLocalDateTime(DateTime? dateTime, string timeZoneId)
|
||||
{
|
||||
var result = UtcAsLocalDateAndTime(dateTime, timeZoneId);
|
||||
if (result.date != null && !string.IsNullOrEmpty(result.time) && TimeSpan.TryParse(result.time, out TimeSpan timespan))
|
||||
{
|
||||
result.date = result.date.Value.Add(timespan);
|
||||
}
|
||||
return result.date;
|
||||
}
|
||||
|
||||
public static (DateTime? date, string time) UtcAsLocalDateAndTime(DateTime? dateTime, string timeZoneId)
|
||||
{
|
||||
DateTimeZone localTimeZone;
|
||||
|
||||
if (!string.IsNullOrEmpty(timeZoneId))
|
||||
{
|
||||
localTimeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(timeZoneId) ?? DateTimeZoneProviders.Tzdb.GetSystemDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
localTimeZone = DateTimeZoneProviders.Tzdb.GetSystemDefault();
|
||||
}
|
||||
|
||||
DateTime? localDateTime = null;
|
||||
string localTime = string.Empty;
|
||||
|
||||
if (dateTime.HasValue && dateTime?.Kind != DateTimeKind.Local)
|
||||
{
|
||||
Instant instant;
|
||||
|
||||
if (dateTime?.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
// Treat Unspecified as Utc not Local. This is due to EF Core, on some databases, after retrieval will have DateTimeKind as Unspecified.
|
||||
// All values in database should be UTC.
|
||||
// Normal .net conversion treats Unspecified as local.
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.converttime?view=net-6.0
|
||||
instant = Instant.FromDateTimeUtc(new DateTime(dateTime.Value.Ticks, DateTimeKind.Utc));
|
||||
}
|
||||
else
|
||||
{
|
||||
instant = Instant.FromDateTimeUtc(dateTime.Value);
|
||||
}
|
||||
|
||||
localDateTime = instant.InZone(localTimeZone).ToDateTimeOffset().DateTime;
|
||||
}
|
||||
|
||||
if (localDateTime != null && localDateTime.Value.TimeOfDay.TotalSeconds != 0)
|
||||
{
|
||||
localTime = localDateTime.Value.ToString("HH:mm");
|
||||
}
|
||||
|
||||
return (localDateTime?.Date, localTime);
|
||||
}
|
||||
|
||||
public static List<TimeZone> GetTimeZones()
|
||||
{
|
||||
return [.. DateTimeZoneProviders.Tzdb.GetAllZones()
|
||||
.Select(tz => new TimeZone()
|
||||
{
|
||||
Id = tz.Id,
|
||||
DisplayName = tz.ToString()
|
||||
})];
|
||||
}
|
||||
|
||||
public static bool IsEffectiveAndNotExpired(DateTime? effectiveDate, DateTime? expiryDate)
|
||||
{
|
||||
DateTime currentUtcTime = DateTime.UtcNow;
|
||||
|
Reference in New Issue
Block a user