Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
5 changed files with 336 additions and 547 deletions
Showing only changes of commit 54e46fdeb6 - Show all commits

View File

@ -73,82 +73,6 @@ type DisplayCategory = {
}
/// A display version of an episode chapter
type DisplayChapter = {
/// The start time of the chapter (H:mm:ss.FF format)
StartTime: string
/// The title of the chapter
Title: string
/// An image to display for this chapter
ImageUrl: string
/// A URL with information about this chapter
Url: string
/// Whether this chapter should be displayed in podcast players
IsHidden: bool
/// The end time of the chapter (H:mm:ss.FF format)
EndTime: string
/// The name of a location
LocationName: string
/// The geographic coordinates of the location
LocationGeo: string
/// An OpenStreetMap query for this location
LocationOsm: string
} with
/// Create a display chapter from a chapter
static member FromChapter (chapter: Chapter) =
let pattern = DurationPattern.CreateWithInvariantCulture "H:mm:ss.FF"
{ StartTime = pattern.Format chapter.StartTime
Title = defaultArg chapter.Title ""
ImageUrl = defaultArg chapter.ImageUrl ""
Url = defaultArg chapter.Url ""
IsHidden = defaultArg chapter.IsHidden false
EndTime = chapter.EndTime |> Option.map pattern.Format |> Option.defaultValue ""
LocationName = chapter.Location |> Option.map _.Name |> Option.defaultValue ""
LocationGeo = chapter.Location |> Option.map _.Geo |> Option.defaultValue ""
LocationOsm = chapter.Location |> Option.map _.Osm |> Option.flatten |> Option.defaultValue "" }
/// A display version of a custom feed definition
type DisplayCustomFeed = {
/// The ID of the custom feed
Id: string
/// The source of the custom feed
Source: string
/// The relative path at which the custom feed is served
Path: string
/// Whether this custom feed is for a podcast
IsPodcast: bool
} with
/// Create a display version from a custom feed
static member FromFeed (cats: DisplayCategory array) (feed: CustomFeed) =
let source =
match feed.Source with
| Category (CategoryId catId) ->
cats
|> Array.tryFind (fun cat -> cat.Id = catId)
|> Option.map _.Name
|> Option.defaultValue "--INVALID; DELETE THIS FEED--"
|> sprintf "Category: %s"
| Tag tag -> $"Tag: {tag}"
{ Id = string feed.Id
Source = source
Path = string feed.Path
IsPodcast = Option.isSome feed.Podcast }
/// Details about a page used to display page lists
[<NoComparison; NoEquality>]
type DisplayPage = {
@ -269,50 +193,6 @@ type DisplayUpload = {
Source = string source }
/// View model to display a user's information
[<NoComparison; NoEquality>]
type DisplayUser = {
/// The ID of the user
Id: string
/// The user name (e-mail address)
Email: string
/// The user's first name
FirstName: string
/// The user's last name
LastName: string
/// The user's preferred name
PreferredName: string
/// The URL of the user's personal site
Url: string
/// The user's access level
AccessLevel: string
/// When the user was created
CreatedOn: DateTime
/// When the user last logged on
LastSeenOn: Nullable<DateTime>
} with
/// Construct a displayed user from a web log user
static member FromUser (webLog: WebLog) (user: WebLogUser) =
{ Id = string user.Id
Email = user.Email
FirstName = user.FirstName
LastName = user.LastName
PreferredName = user.PreferredName
Url = defaultArg user.Url ""
AccessLevel = string user.AccessLevel
CreatedOn = webLog.LocalTime user.CreatedOn
LastSeenOn = user.LastSeenOn |> Option.map webLog.LocalTime |> Option.toNullable }
/// View model for editing categories
[<CLIMutable; NoComparison; NoEquality>]
type EditCategoryModel = {
@ -386,19 +266,19 @@ type EditChapterModel = {
} with
/// Create a display chapter from a chapter
static member FromChapter (postId: PostId) idx chapter =
let it = DisplayChapter.FromChapter chapter
static member FromChapter (postId: PostId) idx (chapter: Chapter) =
let pattern = DurationPattern.CreateWithInvariantCulture "H:mm:ss.FF"
{ PostId = string postId
Index = idx
StartTime = it.StartTime
Title = it.Title
ImageUrl = it.ImageUrl
Url = it.Url
IsHidden = it.IsHidden
EndTime = it.EndTime
LocationName = it.LocationName
LocationGeo = it.LocationGeo
LocationOsm = it.LocationOsm
StartTime = pattern.Format chapter.StartTime
Title = defaultArg chapter.Title ""
ImageUrl = defaultArg chapter.ImageUrl ""
Url = defaultArg chapter.Url ""
IsHidden = defaultArg chapter.IsHidden false
EndTime = chapter.EndTime |> Option.map pattern.Format |> Option.defaultValue ""
LocationName = chapter.Location |> Option.map _.Name |> Option.defaultValue ""
LocationGeo = chapter.Location |> Option.map _.Geo |> Option.defaultValue ""
LocationOsm = chapter.Location |> Option.map _.Osm |> Option.flatten |> Option.defaultValue ""
AddAnother = false }
/// Create a chapter from the values in this model
@ -427,6 +307,76 @@ type EditChapterModel = {
Location = location }
/// View model common to page and post edits
type EditCommonModel() =
/// Find the latest revision within a list of revisions
let findLatestRevision (revs: Revision list) =
match revs |> List.sortByDescending _.AsOf |> List.tryHead with Some rev -> rev | None -> Revision.Empty
/// The ID of the page or post
member val Id = "" with get, set
/// The title of the page or post
member val Title = "" with get, set
/// The permalink for the page or post
member val Permalink = "" with get, set
/// The entity to which this model applies ("page" or "post")
member val Entity = "" with get, set
/// Whether to provide a link to manage chapters
member val IncludeChapterLink = false with get, set
/// The template to use to display the page
member val Template = "" with get, set
/// The source type ("HTML" or "Markdown")
member val Source = "" with get, set
/// The text of the page or post
member val Text = "" with get, set
/// Names of metadata items
member val MetaNames: string array = [||] with get, set
/// Values of metadata items
member val MetaValues: string array = [||] with get, set
/// Whether this is a new page or post
member this.IsNew with get () = this.Id = "new"
/// Fill the properties of this object from a page
member this.PopulateFromPage (page: Page) =
let latest = findLatestRevision page.Revisions
let metaItems = if page.Metadata.Length = 0 then [ MetaItem.Empty ] else page.Metadata
this.Id <- string page.Id
this.Title <- page.Title
this.Permalink <- string page.Permalink
this.Entity <- "page"
this.Template <- defaultArg page.Template ""
this.Source <- latest.Text.SourceType
this.Text <- latest.Text.Text
this.MetaNames <- metaItems |> List.map _.Name |> Array.ofList
this.MetaValues <- metaItems |> List.map _.Value |> Array.ofList
/// Fill the properties of this object from a post
member this.PopulateFromPost (post: Post) =
let latest = findLatestRevision post.Revisions
let metaItems = if post.Metadata.Length = 0 then [ MetaItem.Empty ] else post.Metadata
this.Id <- string post.Id
this.Title <- post.Title
this.Permalink <- string post.Permalink
this.Entity <- "post"
this.IncludeChapterLink <- Option.isSome post.Episode && Option.isSome post.Episode.Value.Chapters
this.Template <- defaultArg post.Template ""
this.Source <- latest.Text.SourceType
this.Text <- latest.Text.Text
this.MetaNames <- metaItems |> List.map _.Name |> Array.ofList
this.MetaValues <- metaItems |> List.map _.Value |> Array.ofList
/// View model to edit a custom RSS feed
[<CLIMutable; NoComparison; NoEquality>]
type EditCustomFeedModel = {
@ -604,74 +554,6 @@ type EditMyInfoModel = {
NewPasswordConfirm = "" }
/// View model common to page and post edits
type EditCommonModel() =
/// Find the latest revision within a list of revisions
let findLatestRevision (revs: Revision list) =
match revs |> List.sortByDescending _.AsOf |> List.tryHead with Some rev -> rev | None -> Revision.Empty
/// The ID of the page or post
member val Id = "" with get, set
/// The title of the page or post
member val Title = "" with get, set
/// The permalink for the page or post
member val Permalink = "" with get, set
/// The entity to which this model applies ("page" or "post")
member val Entity = "" with get, set
/// Whether to provide a link to manage chapters
member val IncludeChapterLink = false with get, set
/// The template to use to display the page
member val Template = "" with get, set
/// The source type ("HTML" or "Markdown")
member val Source = "" with get, set
/// The text of the page or post
member val Text = "" with get, set
/// Names of metadata items
member val MetaNames: string array = [||] with get, set
/// Values of metadata items
member val MetaValues: string array = [||] with get, set
/// Whether this is a new page or post
member this.IsNew with get () = this.Id = "new"
/// Fill the properties of this object from a page
member this.PopulateFromPage (page: Page) =
let latest = findLatestRevision page.Revisions
this.Id <- string page.Id
this.Title <- page.Title
this.Permalink <- string page.Permalink
this.Entity <- "page"
this.Template <- defaultArg page.Template ""
this.Source <- latest.Text.SourceType
this.Text <- latest.Text.Text
this.MetaNames <- page.Metadata |> List.map _.Name |> Array.ofList
this.MetaValues <- page.Metadata |> List.map _.Value |> Array.ofList
/// Fill the properties of this object from a post
member this.PopulateFromPost (post: Post) =
let latest = findLatestRevision post.Revisions
this.Id <- string post.Id
this.Title <- post.Title
this.Permalink <- string post.Permalink
this.Entity <- "post"
this.IncludeChapterLink <- Option.isSome post.Episode && Option.isSome post.Episode.Value.Chapters
this.Template <- defaultArg post.Template ""
this.Source <- latest.Text.SourceType
this.Text <- latest.Text.Text
this.MetaNames <- post.Metadata |> List.map _.Name |> Array.ofList
this.MetaValues <- post.Metadata |> List.map _.Value |> Array.ofList
/// View model to edit a page
type EditPageModel() =
inherit EditCommonModel()
@ -801,7 +683,6 @@ type EditPostModel() =
/// Create an edit model from an existing past
static member FromPost (webLog: WebLog) (post: Post) =
let model = EditPostModel()
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.Empty ] } else post
model.PopulateFromPost post
let episode = defaultArg post.Episode Episode.Empty
model.Tags <- post.Tags |> String.concat ", "

View File

@ -33,88 +33,6 @@ let addBaseToRelativeUrlsTests = testList "PublicHelpers.addBaseToRelativeUrls"
}
]
/// Unit tests for the DisplayChapter type
let displayChapterTests = testList "DisplayChapter.FromChapter" [
test "succeeds for a minimally-filled chapter" {
let chapter = DisplayChapter.FromChapter { Chapter.Empty with StartTime = Duration.FromSeconds 322L }
Expect.equal chapter.StartTime "0:05:22" "Start time not filled/formatted properly"
Expect.equal chapter.Title "" "Title not filled properly"
Expect.equal chapter.ImageUrl "" "Image URL not filled properly"
Expect.isFalse chapter.IsHidden "Is hidden flag not filled properly"
Expect.equal chapter.EndTime "" "End time not filled properly"
Expect.equal chapter.LocationName "" "Location name not filled properly"
Expect.equal chapter.LocationGeo "" "Location geo URL not filled properly"
Expect.equal chapter.LocationOsm "" "Location OSM query not filled properly"
}
test "succeeds for a fully-filled chapter" {
let chapter =
DisplayChapter.FromChapter
{ StartTime = Duration.FromSeconds 7201.43242
Title = Some "My Test Chapter"
ImageUrl = Some "two-hours-in.jpg"
Url = Some "https://example.com/about"
IsHidden = Some true
EndTime = Some (Duration.FromSeconds 7313.788)
Location = Some { Name = "Over Here"; Geo = "geo:23432"; Osm = Some "SF98fFSu-8" } }
Expect.equal chapter.StartTime "2:00:01.43" "Start time not filled/formatted properly"
Expect.equal chapter.Title "My Test Chapter" "Title not filled properly"
Expect.equal chapter.ImageUrl "two-hours-in.jpg" "Image URL not filled properly"
Expect.equal chapter.Url "https://example.com/about" "URL not filled properly"
Expect.isTrue chapter.IsHidden "Is hidden flag not filled properly"
Expect.equal chapter.EndTime "2:01:53.78" "End time not filled/formatted properly"
Expect.equal chapter.LocationName "Over Here" "Location name not filled properly"
Expect.equal chapter.LocationGeo "geo:23432" "Location geo URL not filled properly"
Expect.equal chapter.LocationOsm "SF98fFSu-8" "Location OSM query not filled properly"
}
]
/// Unit tests for the DisplayCustomFeed type
let displayCustomFeedTests = testList "DisplayCustomFeed.FromFeed" [
test "succeeds for a feed for an existing category" {
let cats =
[| { DisplayCategory.Id = "abc"
Slug = "a-b-c"
Name = "My Lovely Category"
Description = None
ParentNames = [||]
PostCount = 3 } |]
let feed =
{ CustomFeed.Empty with
Id = CustomFeedId "test-feed"
Source = Category (CategoryId "abc")
Path = Permalink "test-feed.xml" }
let model = DisplayCustomFeed.FromFeed cats feed
Expect.equal model.Id "test-feed" "Id not filled properly"
Expect.equal model.Source "Category: My Lovely Category" "Source not filled properly"
Expect.equal model.Path "test-feed.xml" "Path not filled properly"
Expect.isFalse model.IsPodcast "IsPodcast not filled properly"
}
test "succeeds for a feed for a non-existing category" {
let feed =
{ CustomFeed.Empty with
Id = CustomFeedId "bad-feed"
Source = Category (CategoryId "xyz")
Path = Permalink "trouble.xml" }
let model = DisplayCustomFeed.FromFeed [||] feed
Expect.equal model.Id "bad-feed" "Id not filled properly"
Expect.equal model.Source "Category: --INVALID; DELETE THIS FEED--" "Source not filled properly"
Expect.equal model.Path "trouble.xml" "Path not filled properly"
Expect.isFalse model.IsPodcast "IsPodcast not filled properly"
}
test "succeeds for a feed for a tag" {
let feed =
{ Id = CustomFeedId "tag-feed"
Source = Tag "testing"
Path = Permalink "testing-posts.xml"
Podcast = Some PodcastOptions.Empty }
let model = DisplayCustomFeed.FromFeed [||] feed
Expect.equal model.Id "tag-feed" "Id not filled properly"
Expect.equal model.Source "Tag: testing" "Source not filled properly"
Expect.equal model.Path "testing-posts.xml" "Path not filled properly"
Expect.isTrue model.IsPodcast "IsPodcast not filled properly"
}
]
/// Unit tests for the DisplayPage type
let displayPageTests = testList "DisplayPage" [
let page =
@ -242,47 +160,6 @@ let displayUploadTests = test "DisplayUpload.FromUpload succeeds" {
model.UpdatedOn.Value ((Noda.epoch + Duration.FromHours 1).ToDateTimeUtc()) "UpdatedOn not filled properly"
}
/// Unit tests for the DisplayUser type
let displayUserTests = testList "DisplayUser.FromUser" [
let minimalUser =
{ WebLogUser.Empty with
Id = WebLogUserId "test-user"
Email = "jim.james@example.com"
FirstName = "Jim"
LastName = "James"
PreferredName = "John"
AccessLevel = Editor
CreatedOn = Noda.epoch }
test "succeeds when the user has minimal information" {
let model = DisplayUser.FromUser WebLog.Empty minimalUser
Expect.equal model.Id "test-user" "Id not filled properly"
Expect.equal model.Email "jim.james@example.com" "Email not filled properly"
Expect.equal model.FirstName "Jim" "FirstName not filled properly"
Expect.equal model.LastName "James" "LastName not filled properly"
Expect.equal model.PreferredName "John" "PreferredName not filled properly"
Expect.equal model.Url "" "Url not filled properly"
Expect.equal model.AccessLevel "Editor" "AccessLevel not filled properly"
Expect.equal model.CreatedOn (Noda.epoch.ToDateTimeUtc()) "CreatedOn not filled properly"
Expect.isFalse model.LastSeenOn.HasValue "LastSeenOn should have been null"
}
test "succeeds when the user has all information" {
let model =
DisplayUser.FromUser
{ WebLog.Empty with TimeZone = "Etc/GMT-1" }
{ minimalUser with
Url = Some "https://my.site"
LastSeenOn = Some (Noda.epoch + Duration.FromDays 4) }
Expect.equal model.Url "https://my.site" "Url not filled properly"
Expect.equal
model.CreatedOn ((Noda.epoch + Duration.FromHours 1).ToDateTimeUtc()) "CreatedOn not filled properly"
Expect.isTrue model.LastSeenOn.HasValue "LastSeenOn should not have been null"
Expect.equal
model.LastSeenOn.Value
((Noda.epoch + Duration.FromDays 4 + Duration.FromHours 1).ToDateTimeUtc())
"LastSeenOn not filled properly"
}
]
/// Unit tests for the EditCategoryModel type
let editCategoryModelTests = testList "EditCategoryModel" [
testList "FromCategory" [
@ -315,6 +192,131 @@ let editCategoryModelTests = testList "EditCategoryModel" [
]
]
/// A full page used to test various models
let private testFullPage =
{ Page.Empty with
Id = PageId "the-page"
Title = "Test Page"
Permalink = Permalink "blog/page.html"
Template = Some "bork"
IsInPageList = true
Revisions =
[ { AsOf = Noda.epoch + Duration.FromHours 1; Text = Markdown "# Howdy!" }
{ AsOf = Noda.epoch; Text = Html "<h1>howdy</h1>" } ]
Metadata = [ { Name = "Test"; Value = "me" }; { Name = "Two"; Value = "2" } ] }
/// A full post used to test various models
let testFullPost =
{ Post.Empty with
Id = PostId "a-post"
Status = Published
Title = "A Post"
Permalink = Permalink "1970/01/a-post.html"
PublishedOn = Some (Noda.epoch + Duration.FromDays 7)
UpdatedOn = Noda.epoch + Duration.FromDays 365
Template = Some "demo"
Text = "<p>A post!</p>"
CategoryIds = [ CategoryId "cat-a"; CategoryId "cat-b"; CategoryId "cat-n" ]
Tags = [ "demo"; "post" ]
Metadata = [ { Name = "A Meta"; Value = "A Value" } ]
Revisions =
[ { AsOf = Noda.epoch + Duration.FromDays 365; Text = Html "<p>A post!</p>" }
{ AsOf = Noda.epoch + Duration.FromDays 7; Text = Markdown "A post!" } ]
Episode =
Some { Media = "a-post-ep.mp3"
Length = 15555L
Duration = Some (Duration.FromMinutes 15L + Duration.FromSeconds 22L)
MediaType = Some "audio/mpeg3"
ImageUrl = Some "uploads/podcast-cover.jpg"
Subtitle = Some "Narration"
Explicit = Some Clean
Chapters = None
ChapterFile = Some "uploads/1970/01/chapters.txt"
ChapterType = Some "chapters"
ChapterWaypoints = Some true
TranscriptUrl = Some "uploads/1970/01/transcript.txt"
TranscriptType = Some "transcript"
TranscriptLang = Some "EN-us"
TranscriptCaptions = Some true
SeasonNumber = Some 3
SeasonDescription = Some "Season Three"
EpisodeNumber = Some 322.
EpisodeDescription = Some "Episode 322" } }
/// Unit tests for the EditCommonModel type
let editCommonModelTests = testList "EditCommonModel" [
testList "IsNew" [
test "succeeds for a new page or post" {
Expect.isTrue (EditCommonModel(Id = "new")).IsNew "IsNew should have been set"
}
test "succeeds for an existing page or post" {
Expect.isFalse (EditCommonModel(Id = string (PageId.Create ()))).IsNew "IsNew should not have been set"
}
]
testList "PopulateFromPage" [
test "succeeds for empty page" {
let model = EditCommonModel()
model.PopulateFromPage { Page.Empty with Id = PageId "abc" }
Expect.equal model.Id "abc" "PageId not filled properly"
Expect.equal model.Title "" "Title not filled properly"
Expect.equal model.Permalink "" "Permalink not filled properly"
Expect.equal model.Template "" "Template not filled properly"
Expect.equal model.Source "HTML" "Source not filled properly"
Expect.equal model.Text "" "Text not set properly"
Expect.equal model.MetaNames.Length 1 "MetaNames should have one entry"
Expect.equal model.MetaNames[0] "" "Meta name not set properly"
Expect.equal model.MetaValues.Length 1 "MetaValues should have one entry"
Expect.equal model.MetaValues[0] "" "Meta value not set properly"
}
test "succeeds for filled page" {
let model = EditCommonModel()
model.PopulateFromPage testFullPage
Expect.equal model.Id "the-page" "PageId not filled properly"
Expect.equal model.Title "Test Page" "Title not filled properly"
Expect.equal model.Permalink "blog/page.html" "Permalink not filled properly"
Expect.equal model.Template "bork" "Template not filled properly"
Expect.equal model.Source "Markdown" "Source not filled properly"
Expect.equal model.Text "# Howdy!" "Text not filled properly"
Expect.equal model.MetaNames.Length 2 "MetaNames should have two entries"
Expect.equal model.MetaNames[0] "Test" "Meta name 0 not set properly"
Expect.equal model.MetaNames[1] "Two" "Meta name 1 not set properly"
Expect.equal model.MetaValues.Length 2 "MetaValues should have two entries"
Expect.equal model.MetaValues[0] "me" "Meta value 0 not set properly"
Expect.equal model.MetaValues[1] "2" "Meta value 1 not set properly"
}
]
testList "PopulateFromPost" [
test "succeeds for empty post" {
let model = EditCommonModel()
model.PopulateFromPost { Post.Empty with Id = PostId "la-la-la" }
Expect.equal model.Id "la-la-la" "PostId not filled properly"
Expect.equal model.Title "" "Title not filled properly"
Expect.equal model.Permalink "" "Permalink not filled properly"
Expect.equal model.Source "HTML" "Source not filled properly"
Expect.equal model.Text "" "Text not filled properly"
Expect.equal model.Template "" "Template not filled properly"
Expect.equal model.MetaNames.Length 1 "MetaNames not filled properly"
Expect.equal model.MetaNames[0] "" "Meta name 0 not filled properly"
Expect.equal model.MetaValues.Length 1 "MetaValues not filled properly"
Expect.equal model.MetaValues[0] "" "Meta value 0 not filled properly"
}
test "succeeds for full post with external chapters" {
let model = EditCommonModel()
model.PopulateFromPost testFullPost
Expect.equal model.Id "a-post" "PostId not filled properly"
Expect.equal model.Title "A Post" "Title not filled properly"
Expect.equal model.Permalink "1970/01/a-post.html" "Permalink not filled properly"
Expect.equal model.Source "HTML" "Source not filled properly"
Expect.equal model.Text "<p>A post!</p>" "Text not filled properly"
Expect.equal model.Template "demo" "Template not filled properly"
Expect.equal model.MetaNames.Length 1 "MetaNames not filled properly"
Expect.equal model.MetaNames[0] "A Meta" "Meta name 0 not filled properly"
Expect.equal model.MetaValues.Length 1 "MetaValues not filled properly"
Expect.equal model.MetaValues[0] "A Value" "Meta value 0 not filled properly"
}
]
]
/// Unit tests for the EditCustomFeedModel type
let editCustomFeedModelTests = testList "EditCustomFeedModel" [
let minimalPodcast =
@ -502,63 +504,26 @@ let editMyInfoModelTests = test "EditMyInfoModel.FromUser succeeds" {
Expect.equal model.NewPasswordConfirm "" "NewPasswordConfirm not filled properly"
}
/// Unit tests for the EditPageModel type
let editPageModelTests = testList "EditPageModel" [
let fullPage =
{ Page.Empty with
Id = PageId "the-page"
Title = "Test Page"
Permalink = Permalink "blog/page.html"
Template = Some "bork"
IsInPageList = true
Revisions =
[ { AsOf = Noda.epoch + Duration.FromHours 1; Text = Markdown "# Howdy!" }
{ AsOf = Noda.epoch; Text = Html "<h1>howdy</h1>" } ]
Metadata = [ { Name = "Test"; Value = "me" }; { Name = "Two"; Value = "2" } ] }
testList "FromPage" [
test "succeeds for empty page" {
let model = EditPageModel.FromPage { Page.Empty with Id = PageId "abc" }
Expect.equal model.PageId "abc" "PageId not filled properly"
Expect.equal model.Title "" "Title not filled properly"
Expect.equal model.Permalink "" "Permalink not filled properly"
Expect.equal model.Template "" "Template not filled properly"
Expect.equal model.Id "abc" "Parent fields not filled properly"
Expect.isFalse model.IsShownInPageList "IsShownInPageList should not have been set"
Expect.equal model.Source "HTML" "Source not filled properly"
Expect.equal model.Text "" "Text not set properly"
Expect.equal model.MetaNames.Length 1 "MetaNames should have one entry"
Expect.equal model.MetaNames[0] "" "Meta name not set properly"
Expect.equal model.MetaValues.Length 1 "MetaValues should have one entry"
Expect.equal model.MetaValues[0] "" "Meta value not set properly"
}
test "succeeds for filled page" {
let model = EditPageModel.FromPage fullPage
Expect.equal model.PageId "the-page" "PageId not filled properly"
Expect.equal model.Title "Test Page" "Title not filled properly"
Expect.equal model.Permalink "blog/page.html" "Permalink not filled properly"
Expect.equal model.Template "bork" "Template not filled properly"
let model = EditPageModel.FromPage testFullPage
Expect.equal model.Id "the-page" "Parent fields not filled properly"
Expect.isTrue model.IsShownInPageList "IsShownInPageList should have been set"
Expect.equal model.Source "Markdown" "Source not filled properly"
Expect.equal model.Text "# Howdy!" "Text not filled properly"
Expect.equal model.MetaNames.Length 2 "MetaNames should have two entries"
Expect.equal model.MetaNames[0] "Test" "Meta name 0 not set properly"
Expect.equal model.MetaNames[1] "Two" "Meta name 1 not set properly"
Expect.equal model.MetaValues.Length 2 "MetaValues should have two entries"
Expect.equal model.MetaValues[0] "me" "Meta value 0 not set properly"
Expect.equal model.MetaValues[1] "2" "Meta value 1 not set properly"
}
]
testList "IsNew" [
test "succeeds for a new page" {
Expect.isTrue
(EditPageModel.FromPage { Page.Empty with Id = PageId "new" }).IsNew "IsNew should have been set"
}
test "succeeds for an existing page" {
Expect.isFalse (EditPageModel.FromPage Page.Empty).IsNew "IsNew should not have been set"
}
]
testList "UpdatePage" [
test "succeeds with minimal changes" {
let model = { EditPageModel.FromPage fullPage with Title = "Updated Page"; IsShownInPageList = false }
let page = model.UpdatePage fullPage (Noda.epoch + Duration.FromHours 4)
let model = EditPageModel.FromPage testFullPage
model.Title <- "Updated Page"
model.IsShownInPageList <- false
let page = model.UpdatePage testFullPage (Noda.epoch + Duration.FromHours 4)
Expect.equal page.Title "Updated Page" "Title not filled properly"
Expect.equal page.Permalink (Permalink "blog/page.html") "Permalink not filled properly"
Expect.isEmpty page.PriorPermalinks "PriorPermalinks should be empty"
@ -582,18 +547,18 @@ let editPageModelTests = testList "EditPageModel" [
Expect.equal rev2.Text (Html "<h1>howdy</h1>") "Revision 1 text not filled properly"
}
test "succeeds with all changes" {
let model =
{ PageId = "this-page"
Title = "My Updated Page"
Permalink = "blog/updated.html"
Template = ""
IsShownInPageList = false
Source = "HTML"
Text = "<h1>Howdy, partners!</h1>"
MetaNames = [| "banana"; "apple"; "grape" |]
MetaValues = [| "monkey"; "zebra"; "ape" |] }
let model = EditPageModel()
model.Id <- "this-page"
model.Title <- "My Updated Page"
model.Permalink <- "blog/updated.html"
model.Template <- ""
model.IsShownInPageList <- false
model.Source <- "HTML"
model.Text <- "<h1>Howdy, partners!</h1>"
model.MetaNames <- [| "banana"; "apple"; "grape" |]
model.MetaValues <- [| "monkey"; "zebra"; "ape" |]
let now = Noda.epoch + Duration.FromDays 7
let page = model.UpdatePage fullPage now
let page = model.UpdatePage testFullPage now
Expect.equal page.Title "My Updated Page" "Title not filled properly"
Expect.equal page.Permalink (Permalink "blog/updated.html") "Permalink not filled properly"
Expect.equal page.PriorPermalinks [ Permalink "blog/page.html" ] "PriorPermalinks not filled properly"
@ -621,59 +586,14 @@ let editPageModelTests = testList "EditPageModel" [
/// Unit tests for the EditPostModel type
let editPostModelTests = testList "EditPostModel" [
let fullPost =
{ Post.Empty with
Id = PostId "a-post"
Status = Published
Title = "A Post"
Permalink = Permalink "1970/01/a-post.html"
PublishedOn = Some (Noda.epoch + Duration.FromDays 7)
UpdatedOn = Noda.epoch + Duration.FromDays 365
Template = Some "demo"
Text = "<p>A post!</p>"
CategoryIds = [ CategoryId "cat-a"; CategoryId "cat-b"; CategoryId "cat-n" ]
Tags = [ "demo"; "post" ]
Metadata = [ { Name = "A Meta"; Value = "A Value" } ]
Revisions =
[ { AsOf = Noda.epoch + Duration.FromDays 365; Text = Html "<p>A post!</p>" }
{ AsOf = Noda.epoch + Duration.FromDays 7; Text = Markdown "A post!" } ]
Episode =
Some { Media = "a-post-ep.mp3"
Length = 15555L
Duration = Some (Duration.FromMinutes 15L + Duration.FromSeconds 22L)
MediaType = Some "audio/mpeg3"
ImageUrl = Some "uploads/podcast-cover.jpg"
Subtitle = Some "Narration"
Explicit = Some Clean
Chapters = None
ChapterFile = Some "uploads/1970/01/chapters.txt"
ChapterType = Some "chapters"
ChapterWaypoints = Some true
TranscriptUrl = Some "uploads/1970/01/transcript.txt"
TranscriptType = Some "transcript"
TranscriptLang = Some "EN-us"
TranscriptCaptions = Some true
SeasonNumber = Some 3
SeasonDescription = Some "Season Three"
EpisodeNumber = Some 322.
EpisodeDescription = Some "Episode 322" } }
testList "FromPost" [
test "succeeds for empty post" {
let model = EditPostModel.FromPost WebLog.Empty { Post.Empty with Id = PostId "la-la-la" }
Expect.equal model.PostId "la-la-la" "PostId not filled properly"
Expect.equal model.Title "" "Title not filled properly"
Expect.equal model.Permalink "" "Permalink not filled properly"
Expect.equal model.Source "HTML" "Source not filled properly"
Expect.equal model.Text "" "Text not filled properly"
Expect.equal model.Id "la-la-la" "Parent fields not filled properly"
Expect.equal model.Tags "" "Tags not filled properly"
Expect.equal model.Template "" "Template not filled properly"
Expect.isEmpty model.CategoryIds "CategoryIds not filled properly"
Expect.equal model.Status (string Draft) "Status not filled properly"
Expect.isFalse model.DoPublish "DoPublish should not have been set"
Expect.equal model.MetaNames.Length 1 "MetaNames not filled properly"
Expect.equal model.MetaNames[0] "" "Meta name 0 not filled properly"
Expect.equal model.MetaValues.Length 1 "MetaValues not filled properly"
Expect.equal model.MetaValues[0] "" "Meta value 0 not filled properly"
Expect.isFalse model.SetPublished "SetPublished should not have been set"
Expect.isFalse model.PubOverride.HasValue "PubOverride not filled properly"
Expect.isFalse model.SetUpdated "SetUpdated should not have been set"
@ -699,21 +619,12 @@ let editPostModelTests = testList "EditPostModel" [
Expect.equal model.EpisodeDescription "" "EpisodeDescription not filled properly"
}
test "succeeds for full post with external chapters" {
let model = EditPostModel.FromPost { WebLog.Empty with TimeZone = "Etc/GMT+1" } fullPost
Expect.equal model.PostId "a-post" "PostId not filled properly"
Expect.equal model.Title "A Post" "Title not filled properly"
Expect.equal model.Permalink "1970/01/a-post.html" "Permalink not filled properly"
Expect.equal model.Source "HTML" "Source not filled properly"
Expect.equal model.Text "<p>A post!</p>" "Text not filled properly"
let model = EditPostModel.FromPost { WebLog.Empty with TimeZone = "Etc/GMT+1" } testFullPost
Expect.equal model.Id "a-post" "Parent fields not filled properly"
Expect.equal model.Tags "demo, post" "Tags not filled properly"
Expect.equal model.Template "demo" "Template not filled properly"
Expect.equal model.CategoryIds [| "cat-a"; "cat-b"; "cat-n" |] "CategoryIds not filled properly"
Expect.equal model.Status (string Published) "Status not filled properly"
Expect.isFalse model.DoPublish "DoPublish should not have been set"
Expect.equal model.MetaNames.Length 1 "MetaNames not filled properly"
Expect.equal model.MetaNames[0] "A Meta" "Meta name 0 not filled properly"
Expect.equal model.MetaValues.Length 1 "MetaValues not filled properly"
Expect.equal model.MetaValues[0] "A Value" "Meta value 0 not filled properly"
Expect.isFalse model.SetPublished "SetPublished should not have been set"
Expect.isTrue model.PubOverride.HasValue "PubOverride should not have been null"
Expect.equal
@ -746,63 +657,52 @@ let editPostModelTests = testList "EditPostModel" [
let model =
EditPostModel.FromPost
{ WebLog.Empty with TimeZone = "Etc/GMT+1" }
{ fullPost with
{ testFullPost with
Episode =
Some
{ fullPost.Episode.Value with
{ testFullPost.Episode.Value with
Chapters = Some []
ChapterFile = None
ChapterType = None } }
Expect.equal model.ChapterSource "internal" "ChapterSource not filled properly"
}
]
testList "IsNew" [
test "succeeds for a new post" {
Expect.isTrue
(EditPostModel.FromPost WebLog.Empty { Post.Empty with Id = PostId "new" }).IsNew
"IsNew should be set for new post"
}
test "succeeds for a not-new post" {
Expect.isFalse
(EditPostModel.FromPost WebLog.Empty { Post.Empty with Id = PostId "nu" }).IsNew
"IsNew should not be set for not-new post"
}
]
let updatedModel =
{ EditPostModel.FromPost WebLog.Empty fullPost with
Title = "An Updated Post"
Permalink = "1970/01/updated-post.html"
Source = "HTML"
Text = "<p>An updated post!</p>"
Tags = "Zebras, Aardvarks, , Turkeys"
Template = "updated"
CategoryIds = [| "cat-x"; "cat-y" |]
MetaNames = [| "Zed Meta"; "A Meta" |]
MetaValues = [| "A Value"; "Zed Value" |]
Media = "an-updated-ep.mp3"
Length = 14444L
Duration = "0:14:42"
MediaType = "audio/mp3"
ImageUrl = "updated-cover.png"
Subtitle = "Talking"
Explicit = "no"
ChapterSource = "external"
ChapterFile = "updated-chapters.txt"
ChapterType = "indexes"
TranscriptUrl = "updated-transcript.txt"
TranscriptType = "subtitles"
TranscriptLang = "ES-mx"
SeasonNumber = 4
SeasonDescription = "Season Fo"
EpisodeNumber = "432.1"
EpisodeDescription = "Four Three Two pt One" }
let updatedModel () =
let model = EditPostModel.FromPost WebLog.Empty testFullPost
model.Title <- "An Updated Post"
model.Permalink <- "1970/01/updated-post.html"
model.Source <- "HTML"
model.Text <- "<p>An updated post!</p>"
model.Tags <- "Zebras, Aardvarks, , Turkeys"
model.Template <- "updated"
model.CategoryIds <- [| "cat-x"; "cat-y" |]
model.MetaNames <- [| "Zed Meta"; "A Meta" |]
model.MetaValues <- [| "A Value"; "Zed Value" |]
model.Media <- "an-updated-ep.mp3"
model.Length <- 14444L
model.Duration <- "0:14:42"
model.MediaType <- "audio/mp3"
model.ImageUrl <- "updated-cover.png"
model.Subtitle <- "Talking"
model.Explicit <- "no"
model.ChapterSource <- "external"
model.ChapterFile <- "updated-chapters.txt"
model.ChapterType <- "indexes"
model.TranscriptUrl <- "updated-transcript.txt"
model.TranscriptType <- "subtitles"
model.TranscriptLang <- "ES-mx"
model.SeasonNumber <- 4
model.SeasonDescription <- "Season Fo"
model.EpisodeNumber <- "432.1"
model.EpisodeDescription <- "Four Three Two pt One"
model
testList "UpdatePost" [
test "succeeds for a full podcast episode" {
let post = updatedModel.UpdatePost fullPost (Noda.epoch + Duration.FromDays 400)
let post = (updatedModel ()).UpdatePost testFullPost (Noda.epoch + Duration.FromDays 400)
Expect.equal post.Title "An Updated Post" "Title not filled properly"
Expect.equal post.Permalink (Permalink "1970/01/updated-post.html") "Permalink not filled properly"
Expect.equal post.PriorPermalinks [ Permalink "1970/01/a-post.html" ] "PriorPermalinks not filled properly"
Expect.equal post.PublishedOn fullPost.PublishedOn "PublishedOn should not have changed"
Expect.equal post.PublishedOn testFullPost.PublishedOn "PublishedOn should not have changed"
Expect.equal post.UpdatedOn (Noda.epoch + Duration.FromDays 400) "UpdatedOn not filled properly"
Expect.equal post.Text "<p>An updated post!</p>" "Text not filled properly"
Expect.equal post.Tags [ "aardvarks"; "turkeys"; "zebras" ] "Tags not filled properly"
@ -841,25 +741,24 @@ let editPostModelTests = testList "EditPostModel" [
Expect.equal ep.EpisodeDescription (Some "Four Three Two pt One") "EpisodeDescription not filled properly"
}
test "succeeds for a minimal podcast episode" {
let minModel =
{ updatedModel with
Duration = ""
MediaType = ""
ImageUrl = ""
Subtitle = ""
Explicit = ""
ChapterFile = ""
ChapterType = ""
ContainsWaypoints = false
TranscriptUrl = ""
TranscriptType = ""
TranscriptLang = ""
TranscriptCaptions = false
SeasonNumber = 0
SeasonDescription = ""
EpisodeNumber = ""
EpisodeDescription = "" }
let post = minModel.UpdatePost fullPost (Noda.epoch + Duration.FromDays 500)
let minModel = updatedModel ()
minModel.Duration <- ""
minModel.MediaType <- ""
minModel.ImageUrl <- ""
minModel.Subtitle <- ""
minModel.Explicit <- ""
minModel.ChapterFile <- ""
minModel.ChapterType <- ""
minModel.ContainsWaypoints <- false
minModel.TranscriptUrl <- ""
minModel.TranscriptType <- ""
minModel.TranscriptLang <- ""
minModel.TranscriptCaptions <- false
minModel.SeasonNumber <- 0
minModel.SeasonDescription <- ""
minModel.EpisodeNumber <- ""
minModel.EpisodeDescription <- ""
let post = minModel.UpdatePost testFullPost (Noda.epoch + Duration.FromDays 500)
Expect.isSome post.Episode "There should have been a podcast episode"
let ep = post.Episode.Value
Expect.equal ep.Media "an-updated-ep.mp3" "Media not filled properly"
@ -882,12 +781,11 @@ let editPostModelTests = testList "EditPostModel" [
Expect.isNone ep.EpisodeDescription "EpisodeDescription not filled properly"
}
test "succeeds for a podcast episode with internal chapters" {
let minModel =
{ updatedModel with
ChapterSource = "internal"
ChapterFile = ""
ChapterType = "" }
let post = minModel.UpdatePost fullPost (Noda.epoch + Duration.FromDays 500)
let minModel = updatedModel ()
minModel.ChapterSource <- "internal"
minModel.ChapterFile <- ""
minModel.ChapterType <- ""
let post = minModel.UpdatePost testFullPost (Noda.epoch + Duration.FromDays 500)
Expect.isSome post.Episode "There should have been a podcast episode"
let ep = post.Episode.Value
Expect.equal ep.Chapters (Some []) "Chapters not filled properly"
@ -895,10 +793,11 @@ let editPostModelTests = testList "EditPostModel" [
Expect.isNone ep.ChapterType "ChapterType not filled properly"
}
test "succeeds for a podcast episode with no chapters" {
let minModel = { updatedModel with ChapterSource = "none" }
let minModel = updatedModel ()
minModel.ChapterSource <- "none"
let post =
minModel.UpdatePost
{ fullPost with Episode = Some { fullPost.Episode.Value with Chapters = Some [] } }
{ testFullPost with Episode = Some { testFullPost.Episode.Value with Chapters = Some [] } }
(Noda.epoch + Duration.FromDays 500)
Expect.isSome post.Episode "There should have been a podcast episode"
let ep = post.Episode.Value
@ -908,14 +807,17 @@ let editPostModelTests = testList "EditPostModel" [
Expect.isNone ep.ChapterWaypoints "ChapterWaypoints not filled properly"
}
test "succeeds for no podcast episode and no template" {
let post = { updatedModel with IsEpisode = false; Template = "" }.UpdatePost fullPost Noda.epoch
let model = updatedModel ()
model.IsEpisode <- false
model.Template <- ""
let post = model.UpdatePost testFullPost Noda.epoch
Expect.isNone post.Template "Template not filled properly"
Expect.isNone post.Episode "Episode not filled properly"
}
test "succeeds when publishing a draft" {
let post =
{ updatedModel with DoPublish = true }.UpdatePost
{ fullPost with Status = Draft } (Noda.epoch + Duration.FromDays 375)
let model = updatedModel ()
model.DoPublish <- true
let post = model.UpdatePost { testFullPost with Status = Draft } (Noda.epoch + Duration.FromDays 375)
Expect.equal post.Status Published "Status not set properly"
Expect.equal post.PublishedOn (Some (Noda.epoch + Duration.FromDays 375)) "PublishedOn not set properly"
}
@ -1322,13 +1224,11 @@ let userMessageTests = testList "UserMessage" [
/// All tests in the Domain.ViewModels file
let all = testList "ViewModels" [
addBaseToRelativeUrlsTests
displayChapterTests
displayCustomFeedTests
displayPageTests
displayThemeTests
displayUploadTests
displayUserTests
editCategoryModelTests
editCommonModelTests
editCustomFeedModelTests
editMyInfoModelTests
editPageModelTests

View File

@ -225,12 +225,11 @@ let register () =
Template.RegisterTag<UserLinksTag> "user_links"
[ // Domain types
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>
typeof<TagMap>; typeof<WebLog>
// View models
typeof<AppViewContext>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
typeof<DisplayTheme>; typeof<DisplayUpload>; typeof<DisplayUser>; typeof<EditPageModel>
typeof<EditPostModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<UserMessage>
typeof<AppViewContext>; typeof<DisplayCategory>; typeof<DisplayPage>; typeof<EditPageModel>; typeof<PostDisplay>
typeof<PostListItem>; typeof<UserMessage>
// Framework types
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>

View File

@ -455,11 +455,9 @@ module WebLog =
|> List.append [ { Page.Empty with Id = PageId "posts"; Title = "- First Page of Posts -" } ]
let! themes = data.Theme.All()
let uploads = [ Database; Disk ]
let feeds = ctx.WebLog.Rss.CustomFeeds |> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx))
return!
Views.WebLog.webLogSettings
(SettingsModel.FromWebLog ctx.WebLog) themes pages uploads (EditRssModel.FromRssOptions ctx.WebLog.Rss)
feeds
|> adminPage "Web Log Settings" true next ctx
}

View File

@ -699,7 +699,40 @@ let uploadNew app = [
/// Web log settings page
let webLogSettings
(model: SettingsModel) (themes: Theme list) (pages: Page list) (uploads: UploadDestination list)
(rss: EditRssModel) (feeds: DisplayCustomFeed list) app = [
(rss: EditRssModel) (app: AppViewContext) = [
let feedDetail (feed: CustomFeed) =
let source =
match feed.Source with
| Category (CategoryId catId) ->
app.Categories
|> Array.tryFind (fun cat -> cat.Id = catId)
|> Option.map _.Name
|> Option.defaultValue "--INVALID; DELETE THIS FEED--"
|> sprintf "Category: %s"
| Tag tag -> $"Tag: {tag}"
div [ _class "row mwl-table-detail" ] [
div [ _class "col-12 col-md-6" ] [
txt source
if Option.isSome feed.Podcast then
raw " &nbsp; "; span [ _class "badge bg-primary" ] [ raw "PODCAST" ]
br []
small [] [
let feedUrl = relUrl app $"admin/settings/rss/{feed.Id}"
a [ _href (relUrl app (string feed.Path)); _target "_blank" ] [ raw "View Feed" ]
actionSpacer
a [ _href $"{feedUrl}/edit" ] [ raw "Edit" ]; actionSpacer
a [ _href feedUrl; _hxDelete feedUrl; _class "text-danger"
_hxConfirm $"Are you sure you want to delete the custom RSS feed based on {feed.Source}? This action cannot be undone." ] [
raw "Delete"
]
]
]
div [ _class "col-12 col-md-6" ] [
small [ _class "d-md-none" ] [ raw "Served at "; txt (string feed.Path) ]
span [ _class "d-none d-md-inline" ] [ txt (string feed.Path) ]
]
]
h2 [ _class "my-3" ] [ txt app.WebLog.Name; raw " Settings" ]
article [] [
p [ _class "text-muted" ] [
@ -824,7 +857,7 @@ let webLogSettings
a [ _class "btn btn-sm btn-secondary"; _href (relUrl app "admin/settings/rss/new/edit") ] [
raw "Add a New Custom Feed"
]
if feeds.Length = 0 then
if app.WebLog.Rss.CustomFeeds.Length = 0 then
p [ _class "text-muted fst-italic text-center" ] [ raw "No custom feeds defined" ]
else
form [ _method "post"; _class "container g-0"; _hxTarget "body" ] [
@ -834,31 +867,9 @@ let webLogSettings
span [ _class "d-md-none" ] [ raw "Feed" ]
span [ _class "d-none d-md-inline" ] [ raw "Source" ]
]
div [ _class $"col-12 col-md-6 d-none d-md-inline-block" ] [ raw "Relative Path" ]
div [ _class "col-12 col-md-6 d-none d-md-inline-block" ] [ raw "Relative Path" ]
]
for feed in feeds do
div [ _class "row mwl-table-detail" ] [
div [ _class "col-12 col-md-6" ] [
txt feed.Source
if feed.IsPodcast then
raw " &nbsp; "; span [ _class "badge bg-primary" ] [ raw "PODCAST" ]
br []
small [] [
let feedUrl = relUrl app $"admin/settings/rss/{feed.Id}"
a [ _href (relUrl app feed.Path); _target "_blank" ] [ raw "View Feed" ]
actionSpacer
a [ _href $"{feedUrl}/edit" ] [ raw "Edit" ]; actionSpacer
a [ _href feedUrl; _hxDelete feedUrl; _class "text-danger"
_hxConfirm $"Are you sure you want to delete the custom RSS feed based on {feed.Source}? This action cannot be undone." ] [
raw "Delete"
]
]
]
div [ _class "col-12 col-md-6" ] [
small [ _class "d-md-none" ] [ raw "Served at "; txt feed.Path ]
span [ _class "d-none d-md-inline" ] [ txt feed.Path ]
]
]
yield! app.WebLog.Rss.CustomFeeds |> List.map feedDetail
]
]
]