* 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:
2022-08-21 18:56:18 -04:00
committed by GitHub
parent 1ec664ad24
commit 5f3daa1de9
45 changed files with 3820 additions and 1306 deletions

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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 ""
}

View File

@@ -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