From 54e46fdeb6c51e129247fdce7b167640f8e419a1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 16 Mar 2024 12:20:08 -0400 Subject: [PATCH] Remove unneeded types --- src/MyWebLog.Domain/ViewModels.fs | 281 +++------- src/MyWebLog.Tests/Domain/ViewModelsTests.fs | 528 ++++++++----------- src/MyWebLog/DotLiquidBespoke.fs | 9 +- src/MyWebLog/Handlers/Admin.fs | 2 - src/MyWebLog/Views/WebLog.fs | 63 ++- 5 files changed, 336 insertions(+), 547 deletions(-) diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index 8c69c2c..d52cd10 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -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 [] type DisplayPage = { @@ -269,50 +193,6 @@ type DisplayUpload = { Source = string source } -/// View model to display a user's information -[] -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 -} 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 [] 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 [] 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 ", " diff --git a/src/MyWebLog.Tests/Domain/ViewModelsTests.fs b/src/MyWebLog.Tests/Domain/ViewModelsTests.fs index e55e488..da29a2b 100644 --- a/src/MyWebLog.Tests/Domain/ViewModelsTests.fs +++ b/src/MyWebLog.Tests/Domain/ViewModelsTests.fs @@ -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 "

howdy

" } ] + 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 = "

A post!

" + 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 "

A post!

" } + { 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 "

A post!

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

howdy

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

howdy

") "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 = "

Howdy, partners!

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

Howdy, partners!

" + 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 = "

A post!

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

A post!

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

A post!

" "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 = "

An updated post!

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

An updated post!

" + 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 "

An updated post!

" "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 diff --git a/src/MyWebLog/DotLiquidBespoke.fs b/src/MyWebLog/DotLiquidBespoke.fs index 872486b..e68f448 100644 --- a/src/MyWebLog/DotLiquidBespoke.fs +++ b/src/MyWebLog/DotLiquidBespoke.fs @@ -225,12 +225,11 @@ let register () = Template.RegisterTag "user_links" [ // Domain types - typeof; typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof; typeof; typeof + typeof; typeof // View models - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof; typeof + typeof; typeof // Framework types typeof; typeof; typeof; typeof typeof; typeof; typeof; typeof diff --git a/src/MyWebLog/Handlers/Admin.fs b/src/MyWebLog/Handlers/Admin.fs index fefd983..6bca839 100644 --- a/src/MyWebLog/Handlers/Admin.fs +++ b/src/MyWebLog/Handlers/Admin.fs @@ -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 } diff --git a/src/MyWebLog/Views/WebLog.fs b/src/MyWebLog/Views/WebLog.fs index 2aeff71..a0f2cb6 100644 --- a/src/MyWebLog/Views/WebLog.fs +++ b/src/MyWebLog/Views/WebLog.fs @@ -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 "   "; 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 "   "; 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 ] ] ]