From d290e6e8a61fffc77e6df0414cf848764766203c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 16 Jul 2022 12:33:34 -0400 Subject: [PATCH] Complete page / post revision maint (#13) - Fix log on redirection - Move page handlers to its own file - Add version to admin area footer - Move generator to HttpContext extension property --- src/MyWebLog/Caches.fs | 16 +++ src/MyWebLog/Handlers/Admin.fs | 211 +--------------------------- src/MyWebLog/Handlers/Feed.fs | 2 +- src/MyWebLog/Handlers/Helpers.fs | 21 +-- src/MyWebLog/Handlers/Page.fs | 221 ++++++++++++++++++++++++++++++ src/MyWebLog/Handlers/Post.fs | 89 +++++++++++- src/MyWebLog/Handlers/Routes.fs | 59 ++++---- src/MyWebLog/Handlers/User.fs | 9 +- src/MyWebLog/MyWebLog.fsproj | 1 + src/admin-theme/_layout.liquid | 2 + src/admin-theme/post-edit.liquid | 11 +- src/admin-theme/revisions.liquid | 19 ++- src/admin-theme/wwwroot/admin.css | 4 + 13 files changed, 383 insertions(+), 282 deletions(-) create mode 100644 src/MyWebLog/Handlers/Page.fs diff --git a/src/MyWebLog/Caches.fs b/src/MyWebLog/Caches.fs index 0324ab8..ff50f08 100644 --- a/src/MyWebLog/Caches.fs +++ b/src/MyWebLog/Caches.fs @@ -9,8 +9,12 @@ module Extensions = open System.Security.Claims open Microsoft.AspNetCore.Antiforgery + open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection + /// Hold variable for the configured generator string + let mutable private generatorString : string option = None + type HttpContext with /// The anti-CSRF service @@ -22,6 +26,18 @@ module Extensions = /// The data implementation member this.Data = this.RequestServices.GetRequiredService () + /// The generator string + member this.Generator = + match generatorString with + | Some gen -> gen + | None -> + let cfg = this.RequestServices.GetRequiredService () + generatorString <- + match Option.ofObj cfg["Generator"] with + | Some gen -> Some gen + | None -> Some "generator not configured" + generatorString.Value + /// The user ID for the current request member this.UserId = WebLogUserId (this.User.Claims |> Seq.find (fun c -> c.Type = ClaimTypes.NameIdentifier)).Value diff --git a/src/MyWebLog/Handlers/Admin.fs b/src/MyWebLog/Handlers/Admin.fs index 9d1c2a6..1d4de67 100644 --- a/src/MyWebLog/Handlers/Admin.fs +++ b/src/MyWebLog/Handlers/Admin.fs @@ -118,218 +118,8 @@ let deleteCategory catId : HttpHandler = fun next ctx -> task { return! listCategoriesBare next ctx } -// -- PAGES -- - -// GET /admin/pages -// GET /admin/pages/page/{pageNbr} -let listPages pageNbr : HttpHandler = fun next ctx -> task { - let webLog = ctx.WebLog - let! pages = ctx.Data.Page.findPageOfPages webLog.id pageNbr - return! - Hash.FromAnonymousObject {| - page_title = "Pages" - csrf = ctx.CsrfTokenSet - pages = pages |> List.map (DisplayPage.fromPageMinimal webLog) - page_nbr = pageNbr - prev_page = if pageNbr = 2 then "" else $"/page/{pageNbr - 1}" - next_page = $"/page/{pageNbr + 1}" - |} - |> viewForTheme "admin" "page-list" next ctx -} - -// GET /admin/page/{id}/edit -let editPage pgId : HttpHandler = fun next ctx -> task { - let! result = task { - match pgId with - | "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new" }) - | _ -> - match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with - | Some page -> return Some ("Edit Page", page) - | None -> return None - } - match result with - | Some (title, page) -> - let model = EditPageModel.fromPage page - let! templates = templatesForTheme ctx "page" - return! - Hash.FromAnonymousObject {| - page_title = title - csrf = ctx.CsrfTokenSet - model = model - metadata = Array.zip model.metaNames model.metaValues - |> Array.mapi (fun idx (name, value) -> [| string idx; name; value |]) - templates = templates - |} - |> viewForTheme "admin" "page-edit" next ctx - | None -> return! Error.notFound next ctx -} - -// POST /admin/page/{id}/delete -let deletePage pgId : HttpHandler = fun next ctx -> task { - let webLog = ctx.WebLog - match! ctx.Data.Page.delete (PageId pgId) webLog.id with - | true -> - do! PageListCache.update ctx - do! addMessage ctx { UserMessage.success with message = "Page deleted successfully" } - | false -> do! addMessage ctx { UserMessage.error with message = "Page not found; nothing deleted" } - return! redirectToGet "admin/pages" next ctx -} - -// GET /admin/page/{id}/permalinks -let editPagePermalinks pgId : HttpHandler = fun next ctx -> task { - match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with - | Some pg -> - return! - Hash.FromAnonymousObject {| - page_title = "Manage Prior Permalinks" - csrf = ctx.CsrfTokenSet - model = ManagePermalinksModel.fromPage pg - |} - |> viewForTheme "admin" "permalinks" next ctx - | None -> return! Error.notFound next ctx -} - -// POST /admin/page/permalinks -let savePagePermalinks : HttpHandler = fun next ctx -> task { - let webLog = ctx.WebLog - let! model = ctx.BindFormAsync () - let links = model.prior |> Array.map Permalink |> List.ofArray - match! ctx.Data.Page.updatePriorPermalinks (PageId model.id) webLog.id links with - | true -> - do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" } - return! redirectToGet $"admin/page/{model.id}/permalinks" next ctx - | false -> return! Error.notFound next ctx -} - -// GET /admin/page/{id}/revisions -let editPageRevisions pgId : HttpHandler = fun next ctx -> task { - let webLog = ctx.WebLog - match! ctx.Data.Page.findFullById (PageId pgId) webLog.id with - | Some pg -> - return! - Hash.FromAnonymousObject {| - page_title = "Manage Page Revisions" - csrf = ctx.CsrfTokenSet - model = ManageRevisionsModel.fromPage webLog pg - |} - |> viewForTheme "admin" "revisions" next ctx - | None -> return! Error.notFound next ctx -} - -// GET /admin/page/{id}/revisions/purge -let purgePageRevisions pgId : HttpHandler = fun next ctx -> task { - let webLog = ctx.WebLog - let data = ctx.Data - match! data.Page.findFullById (PageId pgId) webLog.id with - | Some pg -> - do! data.Page.update { pg with revisions = [ List.head pg.revisions ] } - do! addMessage ctx { UserMessage.success with message = "Prior revisions purged successfully" } - return! redirectToGet $"admin/page/{pgId}/revisions" next ctx - | None -> return! Error.notFound next ctx -} - open Microsoft.AspNetCore.Http -/// Find the page and the requested revision -let private findPageRevision pgId revDate (ctx : HttpContext) = task { - match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with - | Some pg -> - let asOf = parseToUtc revDate - return Some pg, pg.revisions |> List.tryFind (fun r -> r.asOf = asOf) - | None -> return None, None -} - -// GET /admin/page/{id}/revision/{revision-date}/preview -let previewPageRevision (pgId, revDate) : HttpHandler = fun next ctx -> task { - match! findPageRevision pgId revDate ctx with - | Some _, Some rev -> - return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = MarkupText.toHtml rev.text |}) - | None, _ - | _, None -> return! Error.notFound next ctx -} - -open System - -// POST /admin/page/{id}/revision/{revision-date}/restore -let restorePageRevision (pgId, revDate) : HttpHandler = fun next ctx -> task { - match! findPageRevision pgId revDate ctx with - | Some pg, Some rev -> - do! ctx.Data.Page.update - { pg with - revisions = { rev with asOf = DateTime.UtcNow } - :: (pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf)) - } - do! addMessage ctx { UserMessage.success with message = "Revision restored successfully" } - return! redirectToGet $"admin/page/{pgId}/revisions" next ctx - | None, _ - | _, None -> return! Error.notFound next ctx -} - -// POST /admin/page/{id}/revision/{revision-date}/delete -let deletePageRevision (pgId, revDate) : HttpHandler = fun next ctx -> task { - match! findPageRevision pgId revDate ctx with - | Some pg, Some rev -> - do! ctx.Data.Page.update { pg with revisions = pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) } - do! addMessage ctx { UserMessage.success with message = "Revision deleted successfully" } - return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |}) - | None, _ - | _, None -> return! Error.notFound next ctx -} - -#nowarn "3511" - -// POST /admin/page/save -let savePage : HttpHandler = fun next ctx -> task { - let! model = ctx.BindFormAsync () - let webLog = ctx.WebLog - let data = ctx.Data - let now = DateTime.UtcNow - let! pg = task { - match model.pageId with - | "new" -> - return Some - { Page.empty with - id = PageId.create () - webLogId = webLog.id - authorId = ctx.UserId - publishedOn = now - } - | pgId -> return! data.Page.findFullById (PageId pgId) webLog.id - } - match pg with - | Some page -> - let updateList = page.showInPageList <> model.isShownInPageList - let revision = { asOf = now; text = MarkupText.parse $"{model.source}: {model.text}" } - // Detect a permalink change, and add the prior one to the prior list - let page = - match Permalink.toString page.permalink with - | "" -> page - | link when link = model.permalink -> page - | _ -> { page with priorPermalinks = page.permalink :: page.priorPermalinks } - let page = - { page with - title = model.title - permalink = Permalink model.permalink - updatedOn = now - showInPageList = model.isShownInPageList - template = match model.template with "" -> None | tmpl -> Some tmpl - text = MarkupText.toHtml revision.text - metadata = Seq.zip model.metaNames model.metaValues - |> Seq.filter (fun it -> fst it > "") - |> Seq.map (fun it -> { name = fst it; value = snd it }) - |> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}") - |> List.ofSeq - revisions = match page.revisions |> List.tryHead with - | Some r when r.text = revision.text -> page.revisions - | _ -> revision :: page.revisions - } - do! (if model.pageId = "new" then data.Page.add else data.Page.update) page - if updateList then do! PageListCache.update ctx - do! addMessage ctx { UserMessage.success with message = "Page saved successfully" } - return! redirectToGet $"admin/page/{PageId.toString page.id}/edit" next ctx - | None -> return! Error.notFound next ctx -} - // -- TAG MAPPINGS -- /// Get the hash necessary to render the tag mapping list @@ -407,6 +197,7 @@ let deleteMapping tagMapId : HttpHandler = fun next ctx -> task { // -- THEMES -- +open System open System.IO open System.IO.Compression open System.Text.RegularExpressions diff --git a/src/MyWebLog/Handlers/Feed.fs b/src/MyWebLog/Handlers/Feed.fs index c102d64..e6fd088 100644 --- a/src/MyWebLog/Handlers/Feed.fs +++ b/src/MyWebLog/Handlers/Feed.fs @@ -382,7 +382,7 @@ let createFeed (feedType : FeedType) posts : HttpHandler = fun next ctx -> backg setTitleAndDescription feedType webLog cats feed feed.LastUpdatedTime <- (List.head posts).updatedOn |> DateTimeOffset - feed.Generator <- generator ctx + feed.Generator <- ctx.Generator feed.Items <- posts |> Seq.ofList |> Seq.map toItem feed.Language <- "en" feed.Id <- WebLog.absoluteUrl webLog link diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index 60591c9..6cd6115 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -52,24 +52,6 @@ let messages (ctx : HttpContext) = task { | None -> return [||] } -/// Hold variable for the configured generator string -let mutable private generatorString : string option = None - -open Microsoft.Extensions.Configuration -open Microsoft.Extensions.DependencyInjection - -/// Get the generator string -let generator (ctx : HttpContext) = - match generatorString with - | Some gen -> gen - | None -> - let cfg = ctx.RequestServices.GetRequiredService () - generatorString <- - match Option.ofObj cfg["Generator"] with - | Some gen -> Some gen - | None -> Some "generator not configured" - generatorString.Value - open MyWebLog open DotLiquid @@ -94,7 +76,7 @@ let private populateHash hash ctx = task { hash.Add ("page_list", PageListCache.get ctx) hash.Add ("current_page", ctx.Request.Path.Value.Substring 1) hash.Add ("messages", messages) - hash.Add ("generator", generator ctx) + hash.Add ("generator", ctx.Generator) hash.Add ("htmx_script", htmxScript) do! commitSession ctx @@ -219,6 +201,7 @@ open System.Globalization let parseToUtc (date : string) = DateTime.Parse (date, null, DateTimeStyles.AdjustToUniversal) +open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging /// Log level for debugging diff --git a/src/MyWebLog/Handlers/Page.fs b/src/MyWebLog/Handlers/Page.fs new file mode 100644 index 0000000..867a86e --- /dev/null +++ b/src/MyWebLog/Handlers/Page.fs @@ -0,0 +1,221 @@ +/// Handlers to manipulate pages +module MyWebLog.Handlers.Page + +open DotLiquid +open Giraffe +open MyWebLog +open MyWebLog.ViewModels + +// GET /admin/pages +// GET /admin/pages/page/{pageNbr} +let all pageNbr : HttpHandler = fun next ctx -> task { + let webLog = ctx.WebLog + let! pages = ctx.Data.Page.findPageOfPages webLog.id pageNbr + return! + Hash.FromAnonymousObject {| + page_title = "Pages" + csrf = ctx.CsrfTokenSet + pages = pages |> List.map (DisplayPage.fromPageMinimal webLog) + page_nbr = pageNbr + prev_page = if pageNbr = 2 then "" else $"/page/{pageNbr - 1}" + next_page = $"/page/{pageNbr + 1}" + |} + |> viewForTheme "admin" "page-list" next ctx +} + +// GET /admin/page/{id}/edit +let edit pgId : HttpHandler = fun next ctx -> task { + let! result = task { + match pgId with + | "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new" }) + | _ -> + match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with + | Some page -> return Some ("Edit Page", page) + | None -> return None + } + match result with + | Some (title, page) -> + let model = EditPageModel.fromPage page + let! templates = templatesForTheme ctx "page" + return! + Hash.FromAnonymousObject {| + page_title = title + csrf = ctx.CsrfTokenSet + model = model + metadata = Array.zip model.metaNames model.metaValues + |> Array.mapi (fun idx (name, value) -> [| string idx; name; value |]) + templates = templates + |} + |> viewForTheme "admin" "page-edit" next ctx + | None -> return! Error.notFound next ctx +} + +// POST /admin/page/{id}/delete +let delete pgId : HttpHandler = fun next ctx -> task { + let webLog = ctx.WebLog + match! ctx.Data.Page.delete (PageId pgId) webLog.id with + | true -> + do! PageListCache.update ctx + do! addMessage ctx { UserMessage.success with message = "Page deleted successfully" } + | false -> do! addMessage ctx { UserMessage.error with message = "Page not found; nothing deleted" } + return! redirectToGet "admin/pages" next ctx +} + +// GET /admin/page/{id}/permalinks +let editPermalinks pgId : HttpHandler = fun next ctx -> task { + match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with + | Some pg -> + return! + Hash.FromAnonymousObject {| + page_title = "Manage Prior Permalinks" + csrf = ctx.CsrfTokenSet + model = ManagePermalinksModel.fromPage pg + |} + |> viewForTheme "admin" "permalinks" next ctx + | None -> return! Error.notFound next ctx +} + +// POST /admin/page/permalinks +let savePermalinks : HttpHandler = fun next ctx -> task { + let webLog = ctx.WebLog + let! model = ctx.BindFormAsync () + let links = model.prior |> Array.map Permalink |> List.ofArray + match! ctx.Data.Page.updatePriorPermalinks (PageId model.id) webLog.id links with + | true -> + do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" } + return! redirectToGet $"admin/page/{model.id}/permalinks" next ctx + | false -> return! Error.notFound next ctx +} + +// GET /admin/page/{id}/revisions +let editRevisions pgId : HttpHandler = fun next ctx -> task { + let webLog = ctx.WebLog + match! ctx.Data.Page.findFullById (PageId pgId) webLog.id with + | Some pg -> + return! + Hash.FromAnonymousObject {| + page_title = "Manage Page Revisions" + csrf = ctx.CsrfTokenSet + model = ManageRevisionsModel.fromPage webLog pg + |} + |> viewForTheme "admin" "revisions" next ctx + | None -> return! Error.notFound next ctx +} + +// GET /admin/page/{id}/revisions/purge +let purgeRevisions pgId : HttpHandler = fun next ctx -> task { + let webLog = ctx.WebLog + let data = ctx.Data + match! data.Page.findFullById (PageId pgId) webLog.id with + | Some pg -> + do! data.Page.update { pg with revisions = [ List.head pg.revisions ] } + do! addMessage ctx { UserMessage.success with message = "Prior revisions purged successfully" } + return! redirectToGet $"admin/page/{pgId}/revisions" next ctx + | None -> return! Error.notFound next ctx +} + +open Microsoft.AspNetCore.Http + +/// Find the page and the requested revision +let private findPageRevision pgId revDate (ctx : HttpContext) = task { + match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with + | Some pg -> + let asOf = parseToUtc revDate + return Some pg, pg.revisions |> List.tryFind (fun r -> r.asOf = asOf) + | None -> return None, None +} + +// GET /admin/page/{id}/revision/{revision-date}/preview +let previewRevision (pgId, revDate) : HttpHandler = fun next ctx -> task { + match! findPageRevision pgId revDate ctx with + | Some _, Some rev -> + return! + Hash.FromAnonymousObject {| + content = $"""
{MarkupText.toHtml rev.text}
""" + |} + |> bareForTheme "admin" "" next ctx + | None, _ + | _, None -> return! Error.notFound next ctx +} + +open System + +// POST /admin/page/{id}/revision/{revision-date}/restore +let restoreRevision (pgId, revDate) : HttpHandler = fun next ctx -> task { + match! findPageRevision pgId revDate ctx with + | Some pg, Some rev -> + do! ctx.Data.Page.update + { pg with + revisions = { rev with asOf = DateTime.UtcNow } + :: (pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf)) + } + do! addMessage ctx { UserMessage.success with message = "Revision restored successfully" } + return! redirectToGet $"admin/page/{pgId}/revisions" next ctx + | None, _ + | _, None -> return! Error.notFound next ctx +} + +// POST /admin/page/{id}/revision/{revision-date}/delete +let deleteRevision (pgId, revDate) : HttpHandler = fun next ctx -> task { + match! findPageRevision pgId revDate ctx with + | Some pg, Some rev -> + do! ctx.Data.Page.update { pg with revisions = pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) } + do! addMessage ctx { UserMessage.success with message = "Revision deleted successfully" } + return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |}) + | None, _ + | _, None -> return! Error.notFound next ctx +} + +#nowarn "3511" + +// POST /admin/page/save +let save : HttpHandler = fun next ctx -> task { + let! model = ctx.BindFormAsync () + let webLog = ctx.WebLog + let data = ctx.Data + let now = DateTime.UtcNow + let! pg = task { + match model.pageId with + | "new" -> + return Some + { Page.empty with + id = PageId.create () + webLogId = webLog.id + authorId = ctx.UserId + publishedOn = now + } + | pgId -> return! data.Page.findFullById (PageId pgId) webLog.id + } + match pg with + | Some page -> + let updateList = page.showInPageList <> model.isShownInPageList + let revision = { asOf = now; text = MarkupText.parse $"{model.source}: {model.text}" } + // Detect a permalink change, and add the prior one to the prior list + let page = + match Permalink.toString page.permalink with + | "" -> page + | link when link = model.permalink -> page + | _ -> { page with priorPermalinks = page.permalink :: page.priorPermalinks } + let page = + { page with + title = model.title + permalink = Permalink model.permalink + updatedOn = now + showInPageList = model.isShownInPageList + template = match model.template with "" -> None | tmpl -> Some tmpl + text = MarkupText.toHtml revision.text + metadata = Seq.zip model.metaNames model.metaValues + |> Seq.filter (fun it -> fst it > "") + |> Seq.map (fun it -> { name = fst it; value = snd it }) + |> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}") + |> List.ofSeq + revisions = match page.revisions |> List.tryHead with + | Some r when r.text = revision.text -> page.revisions + | _ -> revision :: page.revisions + } + do! (if model.pageId = "new" then data.Page.add else data.Page.update) page + if updateList then do! PageListCache.update ctx + do! addMessage ctx { UserMessage.success with message = "Page saved successfully" } + return! redirectToGet $"admin/page/{PageId.toString page.id}/edit" next ctx + | None -> return! Error.notFound next ctx +} diff --git a/src/MyWebLog/Handlers/Post.fs b/src/MyWebLog/Handlers/Post.fs index b16d992..2465e7e 100644 --- a/src/MyWebLog/Handlers/Post.fs +++ b/src/MyWebLog/Handlers/Post.fs @@ -256,6 +256,15 @@ let edit postId : HttpHandler = fun next ctx -> task { | None -> return! Error.notFound next ctx } +// POST /admin/post/{id}/delete +let delete postId : HttpHandler = fun next ctx -> task { + let webLog = ctx.WebLog + match! ctx.Data.Post.delete (PostId postId) webLog.id with + | true -> do! addMessage ctx { UserMessage.success with message = "Post deleted successfully" } + | false -> do! addMessage ctx { UserMessage.error with message = "Post not found; nothing deleted" } + return! redirectToGet "admin/posts" next ctx +} + // GET /admin/post/{id}/permalinks let editPermalinks postId : HttpHandler = fun next ctx -> task { match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with @@ -282,13 +291,79 @@ let savePermalinks : HttpHandler = fun next ctx -> task { | false -> return! Error.notFound next ctx } -// POST /admin/post/{id}/delete -let delete postId : HttpHandler = fun next ctx -> task { - let webLog = ctx.WebLog - match! ctx.Data.Post.delete (PostId postId) webLog.id with - | true -> do! addMessage ctx { UserMessage.success with message = "Post deleted successfully" } - | false -> do! addMessage ctx { UserMessage.error with message = "Post not found; nothing deleted" } - return! redirectToGet "admin/posts" next ctx +// GET /admin/post/{id}/revisions +let editRevisions postId : HttpHandler = fun next ctx -> task { + match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with + | Some post -> + return! + Hash.FromAnonymousObject {| + page_title = "Manage Post Revisions" + csrf = ctx.CsrfTokenSet + model = ManageRevisionsModel.fromPost ctx.WebLog post + |} + |> viewForTheme "admin" "revisions" next ctx + | None -> return! Error.notFound next ctx +} + +// GET /admin/post/{id}/revisions/purge +let purgeRevisions postId : HttpHandler = fun next ctx -> task { + let data = ctx.Data + match! data.Post.findFullById (PostId postId) ctx.WebLog.id with + | Some post -> + do! data.Post.update { post with revisions = [ List.head post.revisions ] } + do! addMessage ctx { UserMessage.success with message = "Prior revisions purged successfully" } + return! redirectToGet $"admin/post/{postId}/revisions" next ctx + | None -> return! Error.notFound next ctx +} + +open Microsoft.AspNetCore.Http + +/// Find the post and the requested revision +let private findPostRevision postId revDate (ctx : HttpContext) = task { + match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with + | Some post -> + let asOf = parseToUtc revDate + return Some post, post.revisions |> List.tryFind (fun r -> r.asOf = asOf) + | None -> return None, None +} + +// GET /admin/post/{id}/revision/{revision-date}/preview +let previewRevision (postId, revDate) : HttpHandler = fun next ctx -> task { + match! findPostRevision postId revDate ctx with + | Some _, Some rev -> + return! + Hash.FromAnonymousObject {| + content = $"""
{MarkupText.toHtml rev.text}
""" + |} + |> bareForTheme "admin" "" next ctx + | None, _ + | _, None -> return! Error.notFound next ctx +} + +// POST /admin/post/{id}/revision/{revision-date}/restore +let restoreRevision (postId, revDate) : HttpHandler = fun next ctx -> task { + match! findPostRevision postId revDate ctx with + | Some post, Some rev -> + do! ctx.Data.Post.update + { post with + revisions = { rev with asOf = DateTime.UtcNow } + :: (post.revisions |> List.filter (fun r -> r.asOf <> rev.asOf)) + } + do! addMessage ctx { UserMessage.success with message = "Revision restored successfully" } + return! redirectToGet $"admin/post/{postId}/revisions" next ctx + | None, _ + | _, None -> return! Error.notFound next ctx +} + +// POST /admin/post/{id}/revision/{revision-date}/delete +let deleteRevision (postId, revDate) : HttpHandler = fun next ctx -> task { + match! findPostRevision postId revDate ctx with + | Some post, Some rev -> + do! ctx.Data.Post.update { post with revisions = post.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) } + do! addMessage ctx { UserMessage.success with message = "Revision deleted successfully" } + return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |}) + | None, _ + | _, None -> return! Error.notFound next ctx } #nowarn "3511" diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs index 159d870..a746563 100644 --- a/src/MyWebLog/Handlers/Routes.fs +++ b/src/MyWebLog/Handlers/Routes.fs @@ -29,7 +29,7 @@ module CatchAll = // Current post match data.Post.findByPermalink permalink webLog.id |> await with | Some post -> - debug (fun () -> $"Found post by permalink") + debug (fun () -> "Found post by permalink") let model = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 ctx data |> await model.Add ("page_title", post.title) yield fun next ctx -> themedView (defaultArg post.template "single-post") next ctx model @@ -37,7 +37,7 @@ module CatchAll = // Current page match data.Page.findByPermalink permalink webLog.id |> await with | Some page -> - debug (fun () -> $"Found page by permalink") + debug (fun () -> "Found page by permalink") yield fun next ctx -> Hash.FromAnonymousObject {| page_title = page.title @@ -50,7 +50,7 @@ module CatchAll = // RSS feed match Feed.deriveFeedType ctx textLink with | Some (feedType, postCount) -> - debug (fun () -> $"Found RSS feed") + debug (fun () -> "Found RSS feed") yield Feed.generate feedType postCount | None -> () // Post differing only by trailing slash @@ -58,28 +58,28 @@ module CatchAll = Permalink (if textLink.EndsWith "/" then textLink[1..textLink.Length - 2] else $"{textLink[1..]}/") match data.Post.findByPermalink altLink webLog.id |> await with | Some post -> - debug (fun () -> $"Found post by trailing-slash-agnostic permalink") + debug (fun () -> "Found post by trailing-slash-agnostic permalink") yield redirectTo true (WebLog.relativeUrl webLog post.permalink) | None -> () // Page differing only by trailing slash match data.Page.findByPermalink altLink webLog.id |> await with | Some page -> - debug (fun () -> $"Found page by trailing-slash-agnostic permalink") + debug (fun () -> "Found page by trailing-slash-agnostic permalink") yield redirectTo true (WebLog.relativeUrl webLog page.permalink) | None -> () // Prior post match data.Post.findCurrentPermalink [ permalink; altLink ] webLog.id |> await with | Some link -> - debug (fun () -> $"Found post by prior permalink") + debug (fun () -> "Found post by prior permalink") yield redirectTo true (WebLog.relativeUrl webLog link) | None -> () // Prior page match data.Page.findCurrentPermalink [ permalink; altLink ] webLog.id |> await with | Some link -> - debug (fun () -> $"Found page by prior permalink") + debug (fun () -> "Found page by prior permalink") yield redirectTo true (WebLog.relativeUrl webLog link) | None -> () - debug (fun () -> $"No content found") + debug (fun () -> "No content found") } // GET {all-of-the-above} @@ -119,18 +119,20 @@ let router : HttpHandler = choose [ ]) route "/dashboard" >=> Admin.dashboard subRoute "/page" (choose [ - route "s" >=> Admin.listPages 1 - routef "s/page/%i" Admin.listPages - routef "/%s/edit" Admin.editPage - routef "/%s/permalinks" Admin.editPagePermalinks - routef "/%s/revision/%s/preview" Admin.previewPageRevision - routef "/%s/revisions" Admin.editPageRevisions + route "s" >=> Page.all 1 + routef "s/page/%i" Page.all + routef "/%s/edit" Page.edit + routef "/%s/permalinks" Page.editPermalinks + routef "/%s/revision/%s/preview" Page.previewRevision + routef "/%s/revisions" Page.editRevisions ]) subRoute "/post" (choose [ - route "s" >=> Post.all 1 - routef "s/page/%i" Post.all - routef "/%s/edit" Post.edit - routef "/%s/permalinks" Post.editPermalinks + route "s" >=> Post.all 1 + routef "s/page/%i" Post.all + routef "/%s/edit" Post.edit + routef "/%s/permalinks" Post.editPermalinks + routef "/%s/revision/%s/preview" Post.previewRevision + routef "/%s/revisions" Post.editRevisions ]) subRoute "/settings" (choose [ route "" >=> Admin.settings @@ -157,17 +159,20 @@ let router : HttpHandler = choose [ routef "/%s/delete" Admin.deleteCategory ]) subRoute "/page" (choose [ - route "/save" >=> Admin.savePage - route "/permalinks" >=> Admin.savePagePermalinks - routef "/%s/delete" Admin.deletePage - routef "/%s/revision/%s/delete" Admin.deletePageRevision - routef "/%s/revision/%s/restore" Admin.restorePageRevision - routef "/%s/revisions/purge" Admin.purgePageRevisions + route "/save" >=> Page.save + route "/permalinks" >=> Page.savePermalinks + routef "/%s/delete" Page.delete + routef "/%s/revision/%s/delete" Page.deleteRevision + routef "/%s/revision/%s/restore" Page.restoreRevision + routef "/%s/revisions/purge" Page.purgeRevisions ]) subRoute "/post" (choose [ - route "/save" >=> Post.save - route "/permalinks" >=> Post.savePermalinks - routef "/%s/delete" Post.delete + route "/save" >=> Post.save + route "/permalinks" >=> Post.savePermalinks + routef "/%s/delete" Post.delete + routef "/%s/revision/%s/delete" Post.deleteRevision + routef "/%s/revision/%s/restore" Post.restoreRevision + routef "/%s/revisions/purge" Post.purgeRevisions ]) subRoute "/settings" (choose [ route "" >=> Admin.saveSettings diff --git a/src/MyWebLog/Handlers/User.fs b/src/MyWebLog/Handlers/User.fs index b0f176c..b22129a 100644 --- a/src/MyWebLog/Handlers/User.fs +++ b/src/MyWebLog/Handlers/User.fs @@ -40,9 +40,8 @@ open Microsoft.AspNetCore.Authentication.Cookies // POST /user/log-on let doLogOn : HttpHandler = fun next ctx -> task { - let! model = ctx.BindFormAsync () - let webLog = ctx.WebLog - match! ctx.Data.WebLogUser.findByEmail model.emailAddress webLog.id with + let! model = ctx.BindFormAsync () + match! ctx.Data.WebLogUser.findByEmail model.emailAddress ctx.WebLog.id with | Some user when user.passwordHash = hashedPassword model.password user.userName user.salt -> let claims = seq { Claim (ClaimTypes.NameIdentifier, WebLogUserId.toString user.id) @@ -55,8 +54,8 @@ let doLogOn : HttpHandler = fun next ctx -> task { do! ctx.SignInAsync (identity.AuthenticationType, ClaimsPrincipal identity, AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow)) do! addMessage ctx - { UserMessage.success with message = $"Logged on successfully | Welcome to {webLog.name}!" } - return! redirectToGet (defaultArg model.returnTo "admin/dashboard") next ctx + { UserMessage.success with message = $"Logged on successfully | Welcome to {ctx.WebLog.name}!" } + return! redirectToGet (defaultArg (model.returnTo |> Option.map (fun it -> it[1..])) "admin/dashboard") next ctx | _ -> do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" } return! logOn model.returnTo next ctx diff --git a/src/MyWebLog/MyWebLog.fsproj b/src/MyWebLog/MyWebLog.fsproj index 2883e6a..78689b7 100644 --- a/src/MyWebLog/MyWebLog.fsproj +++ b/src/MyWebLog/MyWebLog.fsproj @@ -16,6 +16,7 @@ + diff --git a/src/admin-theme/_layout.liquid b/src/admin-theme/_layout.liquid index 856af96..75859ba 100644 --- a/src/admin-theme/_layout.liquid +++ b/src/admin-theme/_layout.liquid @@ -48,6 +48,8 @@
+ {%- assign version = generator | split: " " -%} + v{{ version[1] }} myWebLog
diff --git a/src/admin-theme/post-edit.liquid b/src/admin-theme/post-edit.liquid index f669821..9c70588 100644 --- a/src/admin-theme/post-edit.liquid +++ b/src/admin-theme/post-edit.liquid @@ -16,8 +16,15 @@ value="{{ model.permalink }}"> {%- if model.post_id != "new" %} - {%- capture perm_edit %}admin/post/{{ model.post_id }}/permalinks{% endcapture -%} - Manage Permalinks + + + Manage Permalinks + + + + Manage Revisions + + {% endif -%}
diff --git a/src/admin-theme/revisions.liquid b/src/admin-theme/revisions.liquid index 0b01409..6341c65 100644 --- a/src/admin-theme/revisions.liquid +++ b/src/admin-theme/revisions.liquid @@ -3,7 +3,7 @@
-
+

@@ -29,11 +29,14 @@

{%- endif %} +
+
Revision
+
{% for rev in model.revisions %} {%- assign as_of_string = rev.as_of | date: "o" -%} {%- assign as_of_id = "rev_" | append: as_of_string | replace: "\.", "_" | replace: ":", "-" -%} -
-
+
+
{{ rev.as_of_local | date: "MMMM d, yyyy" }} at {{ rev.as_of_local | date: "h:mmtt" | downcase }} {{ rev.format }} {%- if forloop.first %} @@ -50,20 +53,14 @@ Restore as Current - Delete {% endunless %}
- {% unless forloop.first %} -
-
- preview not loaded -
-
- {% endunless %} + {% unless forloop.first %}
{% endunless %}
{% endfor %}
diff --git a/src/admin-theme/wwwroot/admin.css b/src/admin-theme/wwwroot/admin.css index 2e9592c..b8ab082 100644 --- a/src/admin-theme/wwwroot/admin.css +++ b/src/admin-theme/wwwroot/admin.css @@ -87,4 +87,8 @@ a.text-danger:link:hover, a.text-danger:visited:hover { } .mwl-revision-preview { max-height: 90vh; + overflow: auto; + border: solid 1px black; + border-radius: .5rem; + padding: .5rem; }