v2 RC2 (#33)
* Add PostgreSQL back end (#30) * Upgrade password storage (#32) * Change podcast/episode storage for SQLite (#29) * Move date/time handling to NodaTime (#31)
This commit was merged in pull request #33.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
open System
|
||||
open MyWebLog
|
||||
open NodaTime
|
||||
|
||||
/// A category under which a post may be identified
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
@@ -64,7 +65,7 @@ type Comment =
|
||||
Status : CommentStatus
|
||||
|
||||
/// When the comment was posted
|
||||
PostedOn : DateTime
|
||||
PostedOn : Instant
|
||||
|
||||
/// The text of the comment
|
||||
Text : string
|
||||
@@ -82,7 +83,7 @@ module Comment =
|
||||
Email = ""
|
||||
Url = None
|
||||
Status = Pending
|
||||
PostedOn = DateTime.UtcNow
|
||||
PostedOn = Noda.epoch
|
||||
Text = ""
|
||||
}
|
||||
|
||||
@@ -106,10 +107,10 @@ type Page =
|
||||
Permalink : Permalink
|
||||
|
||||
/// When this page was published
|
||||
PublishedOn : DateTime
|
||||
PublishedOn : Instant
|
||||
|
||||
/// When this page was last updated
|
||||
UpdatedOn : DateTime
|
||||
UpdatedOn : Instant
|
||||
|
||||
/// Whether this page shows as part of the web log's navigation
|
||||
IsInPageList : bool
|
||||
@@ -140,8 +141,8 @@ module Page =
|
||||
AuthorId = WebLogUserId.empty
|
||||
Title = ""
|
||||
Permalink = Permalink.empty
|
||||
PublishedOn = DateTime.MinValue
|
||||
UpdatedOn = DateTime.MinValue
|
||||
PublishedOn = Noda.epoch
|
||||
UpdatedOn = Noda.epoch
|
||||
IsInPageList = false
|
||||
Template = None
|
||||
Text = ""
|
||||
@@ -173,10 +174,10 @@ type Post =
|
||||
Permalink : Permalink
|
||||
|
||||
/// The instant on which the post was originally published
|
||||
PublishedOn : DateTime option
|
||||
PublishedOn : Instant option
|
||||
|
||||
/// The instant on which the post was last updated
|
||||
UpdatedOn : DateTime
|
||||
UpdatedOn : Instant
|
||||
|
||||
/// The template to use in displaying the post
|
||||
Template : string option
|
||||
@@ -215,7 +216,7 @@ module Post =
|
||||
Title = ""
|
||||
Permalink = Permalink.empty
|
||||
PublishedOn = None
|
||||
UpdatedOn = DateTime.MinValue
|
||||
UpdatedOn = Noda.epoch
|
||||
Text = ""
|
||||
Template = None
|
||||
CategoryIds = []
|
||||
@@ -288,7 +289,7 @@ type ThemeAsset =
|
||||
Id : ThemeAssetId
|
||||
|
||||
/// The updated date (set from the file date from the ZIP archive)
|
||||
UpdatedOn : DateTime
|
||||
UpdatedOn : Instant
|
||||
|
||||
/// The data for the asset
|
||||
Data : byte[]
|
||||
@@ -300,7 +301,7 @@ module ThemeAsset =
|
||||
/// An empty theme asset
|
||||
let empty =
|
||||
{ Id = ThemeAssetId (ThemeId "", "")
|
||||
UpdatedOn = DateTime.MinValue
|
||||
UpdatedOn = Noda.epoch
|
||||
Data = [||]
|
||||
}
|
||||
|
||||
@@ -317,7 +318,7 @@ type Upload =
|
||||
Path : Permalink
|
||||
|
||||
/// The updated date/time for this upload
|
||||
UpdatedOn : DateTime
|
||||
UpdatedOn : Instant
|
||||
|
||||
/// The data for the upload
|
||||
Data : byte[]
|
||||
@@ -331,7 +332,7 @@ module Upload =
|
||||
{ Id = UploadId.empty
|
||||
WebLogId = WebLogId.empty
|
||||
Path = Permalink.empty
|
||||
UpdatedOn = DateTime.MinValue
|
||||
UpdatedOn = Noda.epoch
|
||||
Data = [||]
|
||||
}
|
||||
|
||||
@@ -410,10 +411,11 @@ module WebLog =
|
||||
let _, leadPath = hostAndPath webLog
|
||||
$"{leadPath}/{Permalink.toString permalink}"
|
||||
|
||||
/// Convert a UTC date/time to the web log's local date/time
|
||||
let localTime webLog (date : DateTime) =
|
||||
TimeZoneInfo.ConvertTimeFromUtc
|
||||
(DateTime (date.Ticks, DateTimeKind.Utc), TimeZoneInfo.FindSystemTimeZoneById webLog.TimeZone)
|
||||
/// Convert an Instant (UTC reference) to the web log's local date/time
|
||||
let localTime webLog (date : Instant) =
|
||||
match DateTimeZoneProviders.Tzdb[webLog.TimeZone] with
|
||||
| null -> date.ToDateTimeUtc ()
|
||||
| tz -> date.InZone(tz).ToDateTimeUnspecified ()
|
||||
|
||||
|
||||
/// A user of the web log
|
||||
@@ -440,9 +442,6 @@ type WebLogUser =
|
||||
/// The hash of the user's password
|
||||
PasswordHash : string
|
||||
|
||||
/// Salt used to calculate the user's password hash
|
||||
Salt : Guid
|
||||
|
||||
/// The URL of the user's personal site
|
||||
Url : string option
|
||||
|
||||
@@ -450,10 +449,10 @@ type WebLogUser =
|
||||
AccessLevel : AccessLevel
|
||||
|
||||
/// When the user was created
|
||||
CreatedOn : DateTime
|
||||
CreatedOn : Instant
|
||||
|
||||
/// When the user last logged on
|
||||
LastSeenOn : DateTime option
|
||||
LastSeenOn : Instant option
|
||||
}
|
||||
|
||||
/// Functions to support web log users
|
||||
@@ -468,10 +467,9 @@ module WebLogUser =
|
||||
LastName = ""
|
||||
PreferredName = ""
|
||||
PasswordHash = ""
|
||||
Salt = Guid.Empty
|
||||
Url = None
|
||||
AccessLevel = Author
|
||||
CreatedOn = DateTime.UnixEpoch
|
||||
CreatedOn = Noda.epoch
|
||||
LastSeenOn = None
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Markdig" Version="0.30.2" />
|
||||
<PackageReference Include="Markdig" Version="0.30.3" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
<PackageReference Include="Markdown.ColorCode" Version="1.0.1" />
|
||||
<PackageReference Include="NodaTime" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
namespace MyWebLog
|
||||
|
||||
open System
|
||||
open NodaTime
|
||||
|
||||
/// Support functions for domain definition
|
||||
[<AutoOpen>]
|
||||
@@ -12,6 +13,29 @@ module private Helpers =
|
||||
Convert.ToBase64String(Guid.NewGuid().ToByteArray ()).Replace('/', '_').Replace('+', '-').Substring (0, 22)
|
||||
|
||||
|
||||
/// Functions to support NodaTime manipulation
|
||||
module Noda =
|
||||
|
||||
/// The clock to use when getting "now" (will make mutable for testing)
|
||||
let clock : IClock = SystemClock.Instance
|
||||
|
||||
/// The Unix epoch
|
||||
let epoch = Instant.FromUnixTimeSeconds 0L
|
||||
|
||||
|
||||
/// Truncate an instant to remove fractional seconds
|
||||
let toSecondsPrecision (value : Instant) =
|
||||
Instant.FromUnixTimeSeconds (value.ToUnixTimeSeconds ())
|
||||
|
||||
/// The current Instant, with fractional seconds truncated
|
||||
let now () =
|
||||
toSecondsPrecision (clock.GetCurrentInstant ())
|
||||
|
||||
/// Convert a date/time to an Instant with whole seconds
|
||||
let fromDateTime (dt : DateTime) =
|
||||
toSecondsPrecision (Instant.FromDateTimeUtc (DateTime (dt.Ticks, DateTimeKind.Utc)))
|
||||
|
||||
|
||||
/// A user's access level
|
||||
type AccessLevel =
|
||||
/// The user may create and publish posts and edit the ones they have created
|
||||
@@ -137,6 +161,8 @@ module ExplicitRating =
|
||||
| x -> raise (invalidArg "rating" $"{x} is not a valid explicit rating")
|
||||
|
||||
|
||||
open NodaTime.Text
|
||||
|
||||
/// A podcast episode
|
||||
type Episode =
|
||||
{ /// The URL to the media file for the episode (may be permalink)
|
||||
@@ -146,7 +172,7 @@ type Episode =
|
||||
Length : int64
|
||||
|
||||
/// The duration of the episode
|
||||
Duration : TimeSpan option
|
||||
Duration : Duration option
|
||||
|
||||
/// The media type of the file (overrides podcast default if present)
|
||||
MediaType : string option
|
||||
@@ -214,6 +240,10 @@ module Episode =
|
||||
EpisodeNumber = None
|
||||
EpisodeDescription = None
|
||||
}
|
||||
|
||||
/// Format a duration for an episode
|
||||
let formatDuration ep =
|
||||
ep.Duration |> Option.map (DurationPattern.CreateWithInvariantCulture("H:mm:ss").Format)
|
||||
|
||||
|
||||
open Markdig
|
||||
@@ -269,12 +299,11 @@ module MetaItem =
|
||||
let empty =
|
||||
{ Name = ""; Value = "" }
|
||||
|
||||
|
||||
/// A revision of a page or post
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Revision =
|
||||
{ /// When this revision was saved
|
||||
AsOf : DateTime
|
||||
AsOf : Instant
|
||||
|
||||
/// The text of the revision
|
||||
Text : MarkupText
|
||||
@@ -285,7 +314,7 @@ module Revision =
|
||||
|
||||
/// An empty revision
|
||||
let empty =
|
||||
{ AsOf = DateTime.UtcNow
|
||||
{ AsOf = Noda.epoch
|
||||
Text = Html ""
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
open System
|
||||
open MyWebLog
|
||||
open NodaTime
|
||||
|
||||
/// Helper functions for view models
|
||||
[<AutoOpen>]
|
||||
@@ -138,8 +139,8 @@ type DisplayPage =
|
||||
AuthorId = WebLogUserId.toString page.AuthorId
|
||||
Title = page.Title
|
||||
Permalink = Permalink.toString page.Permalink
|
||||
PublishedOn = page.PublishedOn
|
||||
UpdatedOn = page.UpdatedOn
|
||||
PublishedOn = WebLog.localTime webLog page.PublishedOn
|
||||
UpdatedOn = WebLog.localTime webLog page.UpdatedOn
|
||||
IsInPageList = page.IsInPageList
|
||||
IsDefault = pageId = webLog.DefaultPage
|
||||
Text = ""
|
||||
@@ -154,8 +155,8 @@ type DisplayPage =
|
||||
AuthorId = WebLogUserId.toString page.AuthorId
|
||||
Title = page.Title
|
||||
Permalink = Permalink.toString page.Permalink
|
||||
PublishedOn = page.PublishedOn
|
||||
UpdatedOn = page.UpdatedOn
|
||||
PublishedOn = WebLog.localTime webLog page.PublishedOn
|
||||
UpdatedOn = WebLog.localTime webLog page.UpdatedOn
|
||||
IsInPageList = page.IsInPageList
|
||||
IsDefault = pageId = webLog.DefaultPage
|
||||
Text = addBaseToRelativeUrls extra page.Text
|
||||
@@ -179,7 +180,7 @@ with
|
||||
|
||||
/// Create a display revision from an actual revision
|
||||
static member fromRevision webLog (rev : Revision) =
|
||||
{ AsOf = rev.AsOf
|
||||
{ AsOf = rev.AsOf.ToDateTimeUtc ()
|
||||
AsOfLocal = WebLog.localTime webLog rev.AsOf
|
||||
Format = MarkupText.sourceType rev.Text
|
||||
}
|
||||
@@ -703,7 +704,7 @@ type EditPostModel =
|
||||
match post.Revisions |> List.sortByDescending (fun r -> r.AsOf) |> List.tryHead with
|
||||
| Some rev -> rev
|
||||
| None -> Revision.empty
|
||||
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.empty ] } else post
|
||||
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.empty ] } else post
|
||||
let episode = defaultArg post.Episode Episode.empty
|
||||
{ PostId = PostId.toString post.Id
|
||||
Title = post.Title
|
||||
@@ -723,7 +724,7 @@ type EditPostModel =
|
||||
IsEpisode = Option.isSome post.Episode
|
||||
Media = episode.Media
|
||||
Length = episode.Length
|
||||
Duration = defaultArg (episode.Duration |> Option.map (fun it -> it.ToString """hh\:mm\:ss""")) ""
|
||||
Duration = defaultArg (Episode.formatDuration episode) ""
|
||||
MediaType = defaultArg episode.MediaType ""
|
||||
ImageUrl = defaultArg episode.ImageUrl ""
|
||||
Subtitle = defaultArg episode.Subtitle ""
|
||||
@@ -781,7 +782,8 @@ type EditPostModel =
|
||||
Some {
|
||||
Media = this.Media
|
||||
Length = this.Length
|
||||
Duration = noneIfBlank this.Duration |> Option.map TimeSpan.Parse
|
||||
Duration = noneIfBlank this.Duration
|
||||
|> Option.map (TimeSpan.Parse >> Duration.FromTimeSpan)
|
||||
MediaType = noneIfBlank this.MediaType
|
||||
ImageUrl = noneIfBlank this.ImageUrl
|
||||
Subtitle = noneIfBlank this.Subtitle
|
||||
|
||||
Reference in New Issue
Block a user