Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
4 changed files with 288 additions and 174 deletions
Showing only changes of commit d50056cd66 - Show all commits

View File

@ -26,14 +26,13 @@ type Category = {
} with } with
/// An empty category /// An empty category
static member Empty = { static member Empty =
Id = CategoryId.Empty { Id = CategoryId.Empty
WebLogId = WebLogId.Empty WebLogId = WebLogId.Empty
Name = "" Name = ""
Slug = "" Slug = ""
Description = None Description = None
ParentId = None ParentId = None }
}
/// A comment on a post /// A comment on a post
@ -68,17 +67,16 @@ type Comment = {
} with } with
/// An empty comment /// An empty comment
static member Empty = { static member Empty =
Id = CommentId.Empty { Id = CommentId.Empty
PostId = PostId.Empty PostId = PostId.Empty
InReplyToId = None InReplyToId = None
Name = "" Name = ""
Email = "" Email = ""
Url = None Url = None
Status = Pending Status = Pending
PostedOn = Noda.epoch PostedOn = Noda.epoch
Text = "" Text = "" }
}
/// A page (text not associated with a date/time) /// A page (text not associated with a date/time)
@ -125,21 +123,20 @@ type Page = {
} with } with
/// An empty page /// An empty page
static member Empty = { static member Empty =
Id = PageId.Empty { Id = PageId.Empty
WebLogId = WebLogId.Empty WebLogId = WebLogId.Empty
AuthorId = WebLogUserId.Empty AuthorId = WebLogUserId.Empty
Title = "" Title = ""
Permalink = Permalink.Empty Permalink = Permalink.Empty
PublishedOn = Noda.epoch PublishedOn = Noda.epoch
UpdatedOn = Noda.epoch UpdatedOn = Noda.epoch
IsInPageList = false IsInPageList = false
Template = None Template = None
Text = "" Text = ""
Metadata = [] Metadata = []
PriorPermalinks = [] PriorPermalinks = []
Revisions = [] Revisions = [] }
}
/// A web log post /// A web log post
@ -195,24 +192,23 @@ type Post = {
} with } with
/// An empty post /// An empty post
static member Empty = { static member Empty =
Id = PostId.Empty { Id = PostId.Empty
WebLogId = WebLogId.Empty WebLogId = WebLogId.Empty
AuthorId = WebLogUserId.Empty AuthorId = WebLogUserId.Empty
Status = Draft Status = Draft
Title = "" Title = ""
Permalink = Permalink.Empty Permalink = Permalink.Empty
PublishedOn = None PublishedOn = None
UpdatedOn = Noda.epoch UpdatedOn = Noda.epoch
Text = "" Text = ""
Template = None Template = None
CategoryIds = [] CategoryIds = []
Tags = [] Tags = []
Episode = None Episode = None
Metadata = [] Metadata = []
PriorPermalinks = [] PriorPermalinks = []
Revisions = [] Revisions = [] }
}
/// A mapping between a tag and its URL value, used to translate restricted characters (ex. "#1" -> "number-1") /// A mapping between a tag and its URL value, used to translate restricted characters (ex. "#1" -> "number-1")
@ -232,12 +228,8 @@ type TagMap = {
} with } with
/// An empty tag mapping /// An empty tag mapping
static member Empty = { static member Empty =
Id = TagMapId.Empty { Id = TagMapId.Empty; WebLogId = WebLogId.Empty; Tag = ""; UrlValue = "" }
WebLogId = WebLogId.Empty
Tag = ""
UrlValue = ""
}
/// A theme /// A theme
@ -257,12 +249,8 @@ type Theme = {
} with } with
/// An empty theme /// An empty theme
static member Empty = { static member Empty =
Id = ThemeId.Empty { Id = ThemeId.Empty; Name = ""; Version = ""; Templates = [] }
Name = ""
Version = ""
Templates = []
}
/// A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path]) /// A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path])
@ -279,11 +267,8 @@ type ThemeAsset = {
} with } with
/// An empty theme asset /// An empty theme asset
static member Empty = { static member Empty =
Id = ThemeAssetId.Empty { Id = ThemeAssetId.Empty; UpdatedOn = Noda.epoch; Data = [||] }
UpdatedOn = Noda.epoch
Data = [||]
}
/// An uploaded file /// An uploaded file
@ -306,13 +291,8 @@ type Upload = {
} with } with
/// An empty upload /// An empty upload
static member Empty = { static member Empty =
Id = UploadId.Empty { Id = UploadId.Empty; WebLogId = WebLogId.Empty; Path = Permalink.Empty; UpdatedOn = Noda.epoch; Data = [||] }
WebLogId = WebLogId.Empty
Path = Permalink.Empty
UpdatedOn = Noda.epoch
Data = [||]
}
open Newtonsoft.Json open Newtonsoft.Json
@ -361,21 +341,20 @@ type WebLog = {
} with } with
/// An empty web log /// An empty web log
static member Empty = { static member Empty =
Id = WebLogId.Empty { Id = WebLogId.Empty
Name = "" Name = ""
Slug = "" Slug = ""
Subtitle = None Subtitle = None
DefaultPage = "" DefaultPage = ""
PostsPerPage = 10 PostsPerPage = 10
ThemeId = ThemeId "default" ThemeId = ThemeId "default"
UrlBase = "" UrlBase = ""
TimeZone = "" TimeZone = ""
Rss = RssOptions.Empty Rss = RssOptions.Empty
AutoHtmx = false AutoHtmx = false
Uploads = Database Uploads = Database
RedirectRules = [] RedirectRules = [] }
}
/// Any extra path where this web log is hosted (blank if web log is hosted at the root of the domain) /// Any extra path where this web log is hosted (blank if web log is hosted at the root of the domain)
[<JsonIgnore>] [<JsonIgnore>]
@ -441,19 +420,18 @@ type WebLogUser = {
} with } with
/// An empty web log user /// An empty web log user
static member Empty = { static member Empty =
Id = WebLogUserId.Empty { Id = WebLogUserId.Empty
WebLogId = WebLogId.Empty WebLogId = WebLogId.Empty
Email = "" Email = ""
FirstName = "" FirstName = ""
LastName = "" LastName = ""
PreferredName = "" PreferredName = ""
PasswordHash = "" PasswordHash = ""
Url = None Url = None
AccessLevel = Author AccessLevel = Author
CreatedOn = Noda.epoch CreatedOn = Noda.epoch
LastSeenOn = None LastSeenOn = None }
}
/// Get the user's displayed name /// Get the user's displayed name
[<JsonIgnore>] [<JsonIgnore>]

View File

@ -256,27 +256,26 @@ type Episode = {
} with } with
/// An empty episode /// An empty episode
static member Empty = { static member Empty =
Media = "" { Media = ""
Length = 0L Length = 0L
Duration = None Duration = None
MediaType = None MediaType = None
ImageUrl = None ImageUrl = None
Subtitle = None Subtitle = None
Explicit = None Explicit = None
Chapters = None Chapters = None
ChapterFile = None ChapterFile = None
ChapterType = None ChapterType = None
ChapterWaypoints = None ChapterWaypoints = None
TranscriptUrl = None TranscriptUrl = None
TranscriptType = None TranscriptType = None
TranscriptLang = None TranscriptLang = None
TranscriptCaptions = None TranscriptCaptions = None
SeasonNumber = None SeasonNumber = None
SeasonDescription = None SeasonDescription = None
EpisodeNumber = None EpisodeNumber = None
EpisodeDescription = None EpisodeDescription = None }
}
/// Format a duration for an episode /// Format a duration for an episode
member this.FormatDuration() = member this.FormatDuration() =
@ -460,11 +459,8 @@ type RedirectRule = {
} with } with
/// An empty redirect rule /// An empty redirect rule
static member Empty = { static member Empty =
From = "" { From = ""; To = ""; IsRegex = false }
To = ""
IsRegex = false
}
/// An identifier for a custom feed /// An identifier for a custom feed
@ -557,24 +553,23 @@ type PodcastOptions = {
} with } with
/// A default set of podcast options /// A default set of podcast options
static member Empty = { static member Empty =
Title = "" { Title = ""
Subtitle = None Subtitle = None
ItemsInFeed = 0 ItemsInFeed = 0
Summary = "" Summary = ""
DisplayedAuthor = "" DisplayedAuthor = ""
Email = "" Email = ""
ImageUrl = Permalink.Empty ImageUrl = Permalink.Empty
AppleCategory = "" AppleCategory = ""
AppleSubcategory = None AppleSubcategory = None
Explicit = No Explicit = No
DefaultMediaType = None DefaultMediaType = None
MediaBaseUrl = None MediaBaseUrl = None
PodcastGuid = None PodcastGuid = None
FundingUrl = None FundingUrl = None
FundingText = None FundingText = None
Medium = None Medium = None }
}
/// A custom feed /// A custom feed
@ -594,12 +589,11 @@ type CustomFeed = {
} with } with
/// An empty custom feed /// An empty custom feed
static member Empty = { static member Empty =
Id = CustomFeedId.Empty { Id = CustomFeedId.Empty
Source = Category CategoryId.Empty Source = Category CategoryId.Empty
Path = Permalink.Empty Path = Permalink.Empty
Podcast = None Podcast = None }
}
/// Really Simple Syndication (RSS) options for this web log /// Really Simple Syndication (RSS) options for this web log
@ -628,15 +622,14 @@ type RssOptions = {
} with } with
/// An empty set of RSS options /// An empty set of RSS options
static member Empty = { static member Empty =
IsFeedEnabled = true { IsFeedEnabled = true
FeedName = "feed.xml" FeedName = "feed.xml"
ItemsInFeed = None ItemsInFeed = None
IsCategoryEnabled = true IsCategoryEnabled = true
IsTagEnabled = true IsTagEnabled = true
Copyright = None Copyright = None
CustomFeeds = [] CustomFeeds = [] }
}
/// An identifier for a tag mapping /// An identifier for a tag mapping

View File

@ -1146,17 +1146,16 @@ type SettingsModel = {
} with } with
/// Create a settings model from a web log /// Create a settings model from a web log
static member FromWebLog(webLog: WebLog) = { static member FromWebLog(webLog: WebLog) =
Name = webLog.Name { Name = webLog.Name
Slug = webLog.Slug Slug = webLog.Slug
Subtitle = defaultArg webLog.Subtitle "" Subtitle = defaultArg webLog.Subtitle ""
DefaultPage = webLog.DefaultPage DefaultPage = webLog.DefaultPage
PostsPerPage = webLog.PostsPerPage PostsPerPage = webLog.PostsPerPage
TimeZone = webLog.TimeZone TimeZone = webLog.TimeZone
ThemeId = string webLog.ThemeId ThemeId = string webLog.ThemeId
AutoHtmx = webLog.AutoHtmx AutoHtmx = webLog.AutoHtmx
Uploads = string webLog.Uploads Uploads = string webLog.Uploads }
}
/// Update a web log with settings from the form /// Update a web log with settings from the form
member this.Update(webLog: WebLog) = member this.Update(webLog: WebLog) =

View File

@ -1074,6 +1074,147 @@ let manageRevisionsModelTests = testList "ManageRevisionsModel" [
} }
] ]
/// Unit tests for the PostListItem type
let postListItemTests = testList "PostListItem" [
testList "FromPost" [
test "succeeds for a draft post" {
let post =
{ Post.Empty with
Id = PostId "draft-post"
AuthorId = WebLogUserId "myself"
Title = "Not Ready for Prime Time"
Permalink = Permalink "2021/draft.html"
UpdatedOn = Noda.epoch + Duration.FromHours 8
Text = "<h1>WIP</h1>" }
let model = PostListItem.FromPost { WebLog.Empty with TimeZone = "Etc/GMT-1" } post
Expect.equal model.Id "draft-post" "Id not filled properly"
Expect.equal model.AuthorId "myself" "AuthorId not filled properly"
Expect.equal model.Status "Draft" "Status not filled properly"
Expect.equal model.Title "Not Ready for Prime Time" "Title not filled properly"
Expect.equal model.Permalink "2021/draft.html" "Permalink not filled properly"
Expect.isFalse model.PublishedOn.HasValue "PublishedOn should not have had a value"
Expect.equal
model.UpdatedOn ((Noda.epoch + Duration.FromHours 9).ToDateTimeUtc()) "UpdatedOn not filled properly"
Expect.equal model.Text "<h1>WIP</h1>" "Text not filled properly"
Expect.isEmpty model.CategoryIds "There should have been no category IDs"
Expect.isEmpty model.Tags "There should have been no tags"
Expect.isNone model.Episode "There should not have been an episode"
Expect.isEmpty model.Metadata "There should have been no metadata"
}
test "succeeds for a published post in a non-root domain" {
let post =
{ Post.Empty with
Id = PostId "full-post"
AuthorId = WebLogUserId "me"
Status = Published
Title = "Finished Product"
Permalink = Permalink "2021/post.html"
PublishedOn = Some (Noda.epoch + Duration.FromHours 12)
UpdatedOn = Noda.epoch + Duration.FromHours 13
Text = """<a href="/other-post.html">Click</a>"""
CategoryIds = [ CategoryId "z"; CategoryId "y" ]
Tags = [ "test"; "unit" ]
Episode = Some { Episode.Empty with Media = "test.mp3" }
Metadata = [ { Name = "MyMeta"; Value = "MyValue" } ] }
let model =
PostListItem.FromPost { WebLog.Empty with UrlBase = "https://u.t/w"; TimeZone = "Etc/GMT+1" } post
Expect.equal model.Id "full-post" "Id not filled properly"
Expect.equal model.AuthorId "me" "AuthorId not filled properly"
Expect.equal model.Status "Published" "Status not filled properly"
Expect.equal model.Title "Finished Product" "Title not filled properly"
Expect.equal model.Permalink "2021/post.html" "Permalink not filled properly"
Expect.isTrue model.PublishedOn.HasValue "PublishedOn should not have had a value"
Expect.equal
model.PublishedOn.Value
((Noda.epoch + Duration.FromHours 11).ToDateTimeUtc())
"PublishedOn not filled properly"
Expect.equal
model.UpdatedOn ((Noda.epoch + Duration.FromHours 12).ToDateTimeUtc()) "UpdatedOn not filled properly"
Expect.equal model.Text """<a href="/w/other-post.html">Click</a>""" "Text not filled properly"
Expect.equal model.CategoryIds [ "z"; "y" ] "CategoryIds not filled properly"
Expect.equal model.Tags [ "test"; "unit" ] "Tags not filled properly"
Expect.isSome model.Episode "There should have been an episode"
Expect.equal model.Episode.Value.Media "test.mp3" "Episode not filled properly"
Expect.equal model.Metadata.Length 1 "There should have been 1 metadata item"
Expect.equal model.Metadata[0].Name "MyMeta" "Metadata not filled properly"
}
]
]
/// Unit tests for the SettingModel type
let settingsModelTests = testList "SettingsModel" [
testList "FromWebLog" [
test "succeeds with no subtitle" {
let model =
SettingsModel.FromWebLog
{ WebLog.Empty with
Name = "The Web Log"
Slug = "the-web-log"
DefaultPage = "this-one"
PostsPerPage = 18
TimeZone = "America/Denver"
ThemeId = ThemeId "my-theme"
AutoHtmx = true }
Expect.equal model.Name "The Web Log" "Name not filled properly"
Expect.equal model.Slug "the-web-log" "Slug not filled properly"
Expect.equal model.Subtitle "" "Subtitle not filled properly"
Expect.equal model.DefaultPage "this-one" "DefaultPage not filled properly"
Expect.equal model.PostsPerPage 18 "PostsPerPage not filled properly"
Expect.equal model.TimeZone "America/Denver" "TimeZone not filled properly"
Expect.equal model.ThemeId "my-theme" "ThemeId not filled properly"
Expect.isTrue model.AutoHtmx "AutoHtmx should have been set"
Expect.equal model.Uploads "Database" "Uploads not filled properly"
}
test "succeeds with a subtitle" {
let model = SettingsModel.FromWebLog { WebLog.Empty with Subtitle = Some "sub here!" }
Expect.equal model.Subtitle "sub here!" "Subtitle not filled properly"
}
]
testList "Update" [
test "succeeds with no subtitle" {
let webLog =
{ Name = "Interesting"
Slug = "some-stuff"
Subtitle = ""
DefaultPage = "that-one"
PostsPerPage = 8
TimeZone = "America/Chicago"
ThemeId = "test-theme"
AutoHtmx = true
Uploads = "Disk" }.Update WebLog.Empty
Expect.equal webLog.Name "Interesting" "Name not filled properly"
Expect.equal webLog.Slug "some-stuff" "Slug not filled properly"
Expect.isNone webLog.Subtitle "Subtitle should not have had a value"
Expect.equal webLog.DefaultPage "that-one" "DefaultPage not filled properly"
Expect.equal webLog.PostsPerPage 8 "PostsPerPage not filled properly"
Expect.equal webLog.TimeZone "America/Chicago" "TimeZone not filled properly"
Expect.equal webLog.ThemeId (ThemeId "test-theme") "ThemeId not filled properly"
Expect.isTrue webLog.AutoHtmx "AutoHtmx should have been set"
Expect.equal webLog.Uploads Disk "Uploads not filled properly"
}
test "succeeds with a subtitle" {
let webLog = { SettingsModel.FromWebLog WebLog.Empty with Subtitle = "Sub" }.Update WebLog.Empty
Expect.equal webLog.Subtitle (Some "Sub") "Subtitle should have had a value"
}
]
]
/// Unit tests for the UserMessage type
let userMessageTests = testList "UserMessage" [
test "Success succeeds" {
Expect.equal UserMessage.Success.Level "success" "Level incorrect"
}
test "Info succeeds" {
Expect.equal UserMessage.Info.Level "primary" "Level incorrect"
}
test "Warning succeeds" {
Expect.equal UserMessage.Warning.Level "warning" "Level incorrect"
}
test "Error succeeds" {
Expect.equal UserMessage.Error.Level "danger" "Level incorrect"
}
]
/// All tests in the Domain.ViewModels file /// All tests in the Domain.ViewModels file
let all = testList "ViewModels" [ let all = testList "ViewModels" [
addBaseToRelativeUrlsTests addBaseToRelativeUrlsTests
@ -1094,4 +1235,7 @@ let all = testList "ViewModels" [
editUserModelTests editUserModelTests
managePermalinksModelTests managePermalinksModelTests
manageRevisionsModelTests manageRevisionsModelTests
postListItemTests
settingsModelTests
userMessageTests
] ]