From cbf87f5b49783f78c9f6e6d9ca3480a9cc5c76d0 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 21 May 2022 22:55:13 -0400 Subject: [PATCH] Support directory installations - Add web log to HttpContext on first retrieval per req - Add several DotLiquid filters - Use int for page numbers - Update all themes to use rel/abs link and other filters --- src/MyWebLog.Data/Data.fs | 21 +- src/MyWebLog.Domain/DataTypes.fs | 16 +- src/MyWebLog.Domain/ViewModels.fs | 5 +- src/MyWebLog/Caches.fs | 48 +++-- src/MyWebLog/DotLiquidBespoke.fs | 123 +++++++++++ src/MyWebLog/Handlers/Admin.fs | 118 ++++++----- src/MyWebLog/Handlers/Error.fs | 16 +- src/MyWebLog/Handlers/Helpers.fs | 20 +- src/MyWebLog/Handlers/Post.fs | 191 ++++++++++-------- src/MyWebLog/Handlers/Routes.fs | 154 +++++++------- src/MyWebLog/Handlers/User.fs | 8 +- src/MyWebLog/MyWebLog.fsproj | 1 + src/MyWebLog/Program.fs | 111 ++-------- .../themes/admin/category-edit.liquid | 2 +- .../themes/admin/category-list.liquid | 23 ++- src/MyWebLog/themes/admin/dashboard.liquid | 14 +- src/MyWebLog/themes/admin/layout.liquid | 2 +- src/MyWebLog/themes/admin/log-on.liquid | 2 +- src/MyWebLog/themes/admin/page-edit.liquid | 5 +- src/MyWebLog/themes/admin/page-list.liquid | 13 +- src/MyWebLog/themes/admin/permalinks.liquid | 6 +- src/MyWebLog/themes/admin/post-edit.liquid | 5 +- src/MyWebLog/themes/admin/post-list.liquid | 16 +- src/MyWebLog/themes/admin/settings.liquid | 2 +- .../themes/admin/tag-mapping-edit.liquid | 4 +- .../themes/admin/tag-mapping-list.liquid | 13 +- src/MyWebLog/themes/admin/user-edit.liquid | 2 +- .../themes/bit-badger/home-page.liquid | 49 ++++- src/MyWebLog/themes/bit-badger/layout.liquid | 11 +- .../themes/bit-badger/single-page.liquid | 4 +- .../themes/bit-badger/solution-page.liquid | 4 +- .../themes/daniel-j-summers/index.liquid | 11 +- .../themes/daniel-j-summers/layout.liquid | 14 +- .../daniel-j-summers/single-post.liquid | 12 +- src/MyWebLog/themes/default/index.liquid | 2 +- src/MyWebLog/themes/default/layout.liquid | 2 +- src/MyWebLog/themes/tech-blog/index.liquid | 9 +- src/MyWebLog/themes/tech-blog/layout.liquid | 20 +- .../themes/tech-blog/single-page.liquid | 2 +- .../themes/tech-blog/single-post.liquid | 8 +- src/MyWebLog/wwwroot/themes/admin/admin.js | 54 +++-- 41 files changed, 658 insertions(+), 485 deletions(-) create mode 100644 src/MyWebLog/DotLiquidBespoke.fs diff --git a/src/MyWebLog.Data/Data.fs b/src/MyWebLog.Data/Data.fs index 4ca164f..daa94ad 100644 --- a/src/MyWebLog.Data/Data.fs +++ b/src/MyWebLog.Data/Data.fs @@ -540,35 +540,32 @@ module Post = } /// Find posts to be displayed on an admin page - let findPageOfPosts (webLogId : WebLogId) (pageNbr : int64) postsPerPage = - let pg = int pageNbr + let findPageOfPosts (webLogId : WebLogId) (pageNbr : int) postsPerPage = rethink { withTable Table.Post getAll [ webLogId ] (nameof webLogId) without [ "priorPermalinks"; "revisions" ] orderByFuncDescending (fun row -> row["publishedOn"].Default_ "updatedOn" :> obj) - skip ((pg - 1) * postsPerPage) + skip ((pageNbr - 1) * postsPerPage) limit (postsPerPage + 1) result; withRetryDefault } /// Find posts to be displayed on a page - let findPageOfPublishedPosts (webLogId : WebLogId) (pageNbr : int64) postsPerPage = - let pg = int pageNbr + let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage = rethink { withTable Table.Post getAll [ webLogId ] (nameof webLogId) filter "status" Published without [ "priorPermalinks"; "revisions" ] orderByDescending "publishedOn" - skip ((pg - 1) * postsPerPage) + skip ((pageNbr - 1) * postsPerPage) limit (postsPerPage + 1) result; withRetryDefault } /// Find posts to be displayed on a tag list page - let findPageOfTaggedPosts (webLogId : WebLogId) (tag : string) (pageNbr : int64) postsPerPage = - let pg = int pageNbr + let findPageOfTaggedPosts (webLogId : WebLogId) (tag : string) pageNbr postsPerPage = rethink { withTable Table.Post getAll [ tag ] "tags" @@ -576,7 +573,7 @@ module Post = filter "status" Published without [ "priorPermalinks"; "revisions" ] orderByDescending "publishedOn" - skip ((pg - 1) * postsPerPage) + skip ((pageNbr - 1) * postsPerPage) limit (postsPerPage + 1) result; withRetryDefault } @@ -711,6 +708,12 @@ module WebLog = write; withRetryOnce; ignoreResult } + /// Get all web logs + let all = rethink { + withTable Table.WebLog + result; withRetryDefault + } + /// Retrieve a web log by the URL base let findByHost (url : string) = rethink { diff --git a/src/MyWebLog.Domain/DataTypes.fs b/src/MyWebLog.Domain/DataTypes.fs index 4624b7a..60c4b72 100644 --- a/src/MyWebLog.Domain/DataTypes.fs +++ b/src/MyWebLog.Domain/DataTypes.fs @@ -289,8 +289,20 @@ module WebLog = timeZone = "" } - /// Convert a permalink to an absolute URL - let absoluteUrl webLog = function Permalink link -> $"{webLog.urlBase}{link}" + /// Get the host (including scheme) and extra path from the URL base + let hostAndPath webLog = + let scheme = webLog.urlBase.Split "://" + let host = scheme[1].Split "/" + $"{scheme[0]}://{host[0]}", if host.Length > 1 then $"""/{String.Join ("/", host |> Array.skip 1)}""" else "" + + /// Generate an absolute URL for the given link + let absoluteUrl webLog permalink = + $"{webLog.urlBase}/{Permalink.toString permalink}" + + /// Generate a relative URL for the given link + let relativeUrl webLog permalink = + let _, leadPath = hostAndPath webLog + $"{leadPath}/{Permalink.toString permalink}" /// Convert a date/time to the web log's local date/time let localTime webLog (date : DateTime) = diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index b923065..022c46f 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -400,7 +400,8 @@ type PostListItem = /// Create a post list item from a post static member fromPost (webLog : WebLog) (post : Post) = - let inTZ = WebLog.localTime webLog + let _, extra = WebLog.hostAndPath webLog + let inTZ = WebLog.localTime webLog { id = PostId.toString post.id authorId = WebLogUserId.toString post.authorId status = PostStatus.toString post.status @@ -408,7 +409,7 @@ type PostListItem = permalink = Permalink.toString post.permalink publishedOn = post.publishedOn |> Option.map inTZ |> Option.toNullable updatedOn = inTZ post.updatedOn - text = post.text + text = if extra = "" then post.text else post.text.Replace ("href=\"/", $"href=\"{extra}/") categoryIds = post.categoryIds |> List.map CategoryId.toString tags = post.tags meta = post.metadata diff --git a/src/MyWebLog/Caches.fs b/src/MyWebLog/Caches.fs index 23fd6dc..e85850b 100644 --- a/src/MyWebLog/Caches.fs +++ b/src/MyWebLog/Caches.fs @@ -6,10 +6,12 @@ open Microsoft.AspNetCore.Http module Cache = /// Create the cache key for the web log for the current request - let makeKey (ctx : HttpContext) = ctx.Request.Host.ToUriComponent () + let makeKey (ctx : HttpContext) = (ctx.Items["webLog"] :?> WebLog).urlBase open System.Collections.Concurrent +open Microsoft.Extensions.DependencyInjection +open RethinkDb.Driver.Net /// /// In-memory cache of web log details @@ -17,23 +19,35 @@ open System.Collections.Concurrent /// This is filled by the middleware via the first request for each host, and can be updated via the web log /// settings update page module WebLogCache = - + + /// Create the full path of the request + let private fullPath (ctx : HttpContext) = + $"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{ctx.Request.Path.Value}" + /// The cache of web log details - let private _cache = ConcurrentDictionary () + let mutable private _cache : WebLog list = [] /// Does a host exist in the cache? - let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) + let exists ctx = + let path = fullPath ctx + _cache |> List.exists (fun wl -> path.StartsWith wl.urlBase) /// Get the web log for the current request - let get ctx = _cache[Cache.makeKey ctx] + let get ctx = + let path = fullPath ctx + _cache |> List.find (fun wl -> path.StartsWith wl.urlBase) /// Cache the web log for a particular host - let set ctx webLog = _cache[Cache.makeKey ctx] <- webLog + let set webLog = + _cache <- webLog :: (_cache |> List.filter (fun wl -> wl.id <> webLog.id)) + + /// Fill the web log cache from the database + let fill conn = backgroundTask { + let! webLogs = Data.WebLog.all conn + _cache <- webLogs + } -open Microsoft.Extensions.DependencyInjection -open RethinkDb.Driver.Net - /// A cache of page information needed to display the page list in templates module PageListCache = @@ -42,12 +56,15 @@ module PageListCache = /// Cache of displayed pages let private _cache = ConcurrentDictionary () + /// Are there pages cached for this web log? + let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) + /// Get the pages for the web log for this request let get ctx = _cache[Cache.makeKey ctx] /// Update the pages for the current web log - let update ctx = task { - let webLog = WebLogCache.get ctx + let update (ctx : HttpContext) = backgroundTask { + let webLog = ctx.Items["webLog"] :?> WebLog let conn = ctx.RequestServices.GetRequiredService () let! pages = Data.Page.findListed webLog.id conn _cache[Cache.makeKey ctx] <- pages |> List.map (DisplayPage.fromPage webLog) |> Array.ofList @@ -62,12 +79,15 @@ module CategoryCache = /// The cache itself let private _cache = ConcurrentDictionary () + /// Are there categories cached for this web log? + let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) + /// Get the categories for the web log for this request let get ctx = _cache[Cache.makeKey ctx] /// Update the cache with fresh data - let update ctx = backgroundTask { - let webLog = WebLogCache.get ctx + let update (ctx : HttpContext) = backgroundTask { + let webLog = ctx.Items["webLog"] :?> WebLog let conn = ctx.RequestServices.GetRequiredService () let! cats = Data.Category.findAllForView webLog.id conn _cache[Cache.makeKey ctx] <- cats @@ -84,7 +104,7 @@ module TemplateCache = let private _cache = ConcurrentDictionary () /// Get a template for the given theme and template nate - let get (theme : string) (templateName : string) = task { + let get (theme : string) (templateName : string) = backgroundTask { let templatePath = $"themes/{theme}/{templateName}" match _cache.ContainsKey templatePath with | true -> () diff --git a/src/MyWebLog/DotLiquidBespoke.fs b/src/MyWebLog/DotLiquidBespoke.fs new file mode 100644 index 0000000..3b29deb --- /dev/null +++ b/src/MyWebLog/DotLiquidBespoke.fs @@ -0,0 +1,123 @@ +/// Custom DotLiquid filters and tags +module MyWebLog.DotLiquidBespoke + +open System +open System.IO +open DotLiquid +open MyWebLog.ViewModels + +/// Get the current web log from the DotLiquid context +let webLog (ctx : Context) = + ctx.Environments[0].["web_log"] :?> WebLog + +/// Obtain the link from known types +let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) = + match item with + | :? String as link -> Some link + | :? DisplayPage as page -> Some page.permalink + | :? PostListItem as post -> Some post.permalink + | :? DropProxy as proxy -> Option.ofObj proxy["permalink"] |> Option.map string + | _ -> None + |> function + | Some link -> linkFunc (webLog ctx) (Permalink link) + | None -> $"alert('unknown item type {item.GetType().Name}')" + +/// A filter to generate an absolute link +type AbsoluteLinkFilter () = + static member AbsoluteLink (ctx : Context, item : obj) = + permalink ctx item WebLog.absoluteUrl + +/// A filter to generate a link with posts categorized under the given category +type CategoryLinkFilter () = + static member CategoryLink (ctx : Context, catObj : obj) = + match catObj with + | :? DisplayCategory as cat -> Some cat.slug + | :? DropProxy as proxy -> Option.ofObj proxy["slug"] |> Option.map string + | _ -> None + |> function + | Some slug -> WebLog.relativeUrl (webLog ctx) (Permalink $"category/{slug}/") + | None -> $"alert('unknown category object type {catObj.GetType().Name}')" + + +/// A filter to generate a link that will edit a page +type EditPageLinkFilter () = + static member EditPageLink (ctx : Context, pageObj : obj) = + match pageObj with + | :? DisplayPage as page -> Some page.id + | :? DropProxy as proxy -> Option.ofObj proxy["id"] |> Option.map string + | :? String as theId -> Some theId + | _ -> None + |> function + | Some pageId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/page/{pageId}/edit") + | None -> $"alert('unknown page object type {pageObj.GetType().Name}')" + +/// A filter to generate a link that will edit a post +type EditPostLinkFilter () = + static member EditPostLink (ctx : Context, postObj : obj) = + match postObj with + | :? PostListItem as post -> Some post.id + | :? DropProxy as proxy -> Option.ofObj proxy["id"] |> Option.map string + | :? String as theId -> Some theId + | _ -> None + |> function + | Some postId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/post/{postId}/edit") + | None -> $"alert('unknown post object type {postObj.GetType().Name}')" + +/// A filter to generate nav links, highlighting the active link (exact match) +type NavLinkFilter () = + static member NavLink (ctx : Context, url : string, text : string) = + let webLog = webLog ctx + seq { + "
  • " + text + "
  • " + } + |> Seq.fold (+) "" + +/// A filter to generate a relative link +type RelativeLinkFilter () = + static member RelativeLink (ctx : Context, item : obj) = + permalink ctx item WebLog.relativeUrl + +/// A filter to generate a link with posts tagged with the given tag +type TagLinkFilter () = + static member TagLink (ctx : Context, tag : string) = + ctx.Environments[0].["tag_mappings"] :?> TagMap list + |> List.tryFind (fun it -> it.tag = tag) + |> function + | Some tagMap -> tagMap.urlValue + | None -> tag.Replace (" ", "+") + |> function tagUrl -> WebLog.relativeUrl (webLog ctx) (Permalink $"tag/{tagUrl}/") + +/// Create links for a user to log on or off, and a dashboard link if they are logged off +type UserLinksTag () = + inherit Tag () + + override this.Render (context : Context, result : TextWriter) = + let webLog = webLog context + let link it = WebLog.relativeUrl webLog (Permalink it) + seq { + """" + } + |> Seq.iter result.WriteLine + +/// A filter to retrieve the value of a meta item from a list +// (shorter than `{% assign item = list | where: "name", [name] | first %}{{ item.value }}`) +type ValueFilter () = + static member Value (_ : Context, items : MetaItem list, name : string) = + match items |> List.tryFind (fun it -> it.name = name) with + | Some item -> item.value + | None -> $"-- {name} not found --" + + diff --git a/src/MyWebLog/Handlers/Admin.fs b/src/MyWebLog/Handlers/Admin.fs index 9afd336..4940fe1 100644 --- a/src/MyWebLog/Handlers/Admin.fs +++ b/src/MyWebLog/Handlers/Admin.fs @@ -1,6 +1,8 @@ /// Handlers to manipulate admin functions module MyWebLog.Handlers.Admin +// TODO: remove requireUser, as this is applied in the router + open System.Collections.Generic open System.IO @@ -21,9 +23,9 @@ open RethinkDb.Driver.Net // GET /admin let dashboard : HttpHandler = requireUser >=> fun next ctx -> task { - let webLogId = webLogId ctx - let conn = conn ctx - let getCount (f : WebLogId -> IConnection -> Task) = f webLogId conn + let webLog = webLog ctx + let conn = conn ctx + let getCount (f : WebLogId -> IConnection -> Task) = f webLog.id conn let! posts = Data.Post.countByStatus Published |> getCount let! drafts = Data.Post.countByStatus Draft |> getCount let! pages = Data.Page.countAll |> getCount @@ -60,13 +62,13 @@ let listCategories : HttpHandler = requireUser >=> fun next ctx -> task { // GET /admin/category/{id}/edit let editCategory catId : HttpHandler = requireUser >=> fun next ctx -> task { - let webLogId = webLogId ctx - let conn = conn ctx - let! result = task { + let webLog = webLog ctx + let conn = conn ctx + let! result = task { match catId with | "new" -> return Some ("Add a New Category", { Category.empty with id = CategoryId "new" }) | _ -> - match! Data.Category.findById (CategoryId catId) webLogId conn with + match! Data.Category.findById (CategoryId catId) webLog.id conn with | Some cat -> return Some ("Edit Category", cat) | None -> return None } @@ -86,12 +88,12 @@ let editCategory catId : HttpHandler = requireUser >=> fun next ctx -> task { // POST /admin/category/save let saveCategory : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { let! model = ctx.BindFormAsync () - let webLogId = webLogId ctx - let conn = conn ctx + let webLog = webLog ctx + let conn = conn ctx let! category = task { match model.categoryId with - | "new" -> return Some { Category.empty with id = CategoryId.create (); webLogId = webLogId } - | catId -> return! Data.Category.findById (CategoryId catId) webLogId conn + | "new" -> return Some { Category.empty with id = CategoryId.create (); webLogId = webLog.id } + | catId -> return! Data.Category.findById (CategoryId catId) webLog.id conn } match category with | Some cat -> @@ -105,20 +107,22 @@ let saveCategory : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - do! (match model.categoryId with "new" -> Data.Category.add | _ -> Data.Category.update) cat conn do! CategoryCache.update ctx do! addMessage ctx { UserMessage.success with message = "Category saved successfully" } - return! redirectToGet $"/admin/category/{CategoryId.toString cat.id}/edit" next ctx + return! + redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/category/{CategoryId.toString cat.id}/edit")) + next ctx | None -> return! Error.notFound next ctx } // POST /admin/category/{id}/delete let deleteCategory catId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - let webLogId = webLogId ctx - let conn = conn ctx - match! Data.Category.delete (CategoryId catId) webLogId conn with + let webLog = webLog ctx + let conn = conn ctx + match! Data.Category.delete (CategoryId catId) webLog.id conn with | true -> do! CategoryCache.update ctx do! addMessage ctx { UserMessage.success with message = "Category deleted successfully" } | false -> do! addMessage ctx { UserMessage.error with message = "Category not found; cannot delete" } - return! redirectToGet "/admin/categories" next ctx + return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/categories")) next ctx } // -- PAGES -- @@ -126,7 +130,7 @@ let deleteCategory catId : HttpHandler = requireUser >=> validateCsrf >=> fun ne // GET /admin/pages // GET /admin/pages/page/{pageNbr} let listPages pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { - let webLog = WebLogCache.get ctx + let webLog = webLog ctx let! pages = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx) return! Hash.FromAnonymousObject @@ -142,7 +146,7 @@ let editPage pgId : HttpHandler = requireUser >=> fun next ctx -> task { match pgId with | "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new" }) | _ -> - match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with + match! Data.Page.findByFullId (PageId pgId) (webLog ctx).id (conn ctx) with | Some page -> return Some ("Edit Page", page) | None -> return None } @@ -164,7 +168,7 @@ let editPage pgId : HttpHandler = requireUser >=> fun next ctx -> task { // GET /admin/page/{id}/permalinks let editPagePermalinks pgId : HttpHandler = requireUser >=> fun next ctx -> task { - match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with + match! Data.Page.findByFullId (PageId pgId) (webLog ctx).id (conn ctx) with | Some pg -> return! Hash.FromAnonymousObject {| @@ -178,44 +182,46 @@ let editPagePermalinks pgId : HttpHandler = requireUser >=> fun next ctx -> task // POST /admin/page/permalinks let savePagePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - let! model = ctx.BindFormAsync () - let links = model.prior |> Array.map Permalink |> List.ofArray - match! Data.Page.updatePriorPermalinks (PageId model.id) (webLogId ctx) links (conn ctx) with + let webLog = webLog ctx + let! model = ctx.BindFormAsync () + let links = model.prior |> Array.map Permalink |> List.ofArray + match! Data.Page.updatePriorPermalinks (PageId model.id) webLog.id links (conn ctx) with | true -> do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" } - return! redirectToGet $"/admin/page/{model.id}/permalinks" next ctx + return! redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/page/{model.id}/permalinks")) next ctx | false -> return! Error.notFound next ctx } // POST /admin/page/{id}/delete let deletePage pgId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - match! Data.Page.delete (PageId pgId) (webLogId ctx) (conn ctx) with + let webLog = webLog ctx + match! Data.Page.delete (PageId pgId) webLog.id (conn ctx) with | true -> 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 + return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/pages")) next ctx } open System #nowarn "3511" -// POST /page/save +// POST /admin/page/save let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - let! model = ctx.BindFormAsync () - let webLogId = webLogId ctx - let conn = conn ctx - let now = DateTime.UtcNow - let! pg = task { + let! model = ctx.BindFormAsync () + let webLog = webLog ctx + let conn = conn ctx + let now = DateTime.UtcNow + let! pg = task { match model.pageId with | "new" -> return Some { Page.empty with id = PageId.create () - webLogId = webLogId + webLogId = webLog.id authorId = userId ctx publishedOn = now } - | pgId -> return! Data.Page.findByFullId (PageId pgId) webLogId conn + | pgId -> return! Data.Page.findByFullId (PageId pgId) webLog.id conn } match pg with | Some page -> @@ -247,7 +253,8 @@ let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> ta do! (if model.pageId = "new" then Data.Page.add else Data.Page.update) page conn 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 + return! + redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/page/{PageId.toString page.id}/edit")) next ctx | None -> return! Error.notFound next ctx } @@ -255,7 +262,7 @@ let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> ta // GET /admin/settings let settings : HttpHandler = requireUser >=> fun next ctx -> task { - let webLog = WebLogCache.get ctx + let webLog = webLog ctx let! allPages = Data.Page.findAll webLog.id (conn ctx) return! Hash.FromAnonymousObject @@ -278,9 +285,10 @@ let settings : HttpHandler = requireUser >=> fun next ctx -> task { // POST /admin/settings let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - let conn = conn ctx - let! model = ctx.BindFormAsync () - match! Data.WebLog.findById (WebLogCache.get ctx).id conn with + let webLog = webLog ctx + let conn = conn ctx + let! model = ctx.BindFormAsync () + match! Data.WebLog.findById webLog.id conn with | Some webLog -> let updated = { webLog with @@ -294,10 +302,10 @@ let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - do! Data.WebLog.updateSettings updated conn // Update cache - WebLogCache.set ctx updated + WebLogCache.set updated do! addMessage ctx { UserMessage.success with message = "Web log settings saved successfully" } - return! redirectToGet "/admin" next ctx + return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin")) next ctx | None -> return! Error.notFound next ctx } @@ -305,7 +313,7 @@ let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - // GET /admin/tag-mappings let tagMappings : HttpHandler = requireUser >=> fun next ctx -> task { - let! mappings = Data.TagMap.findByWebLogId (webLogId ctx) (conn ctx) + let! mappings = Data.TagMap.findByWebLogId (webLog ctx).id (conn ctx) return! Hash.FromAnonymousObject {| csrf = csrfToken ctx @@ -318,13 +326,12 @@ let tagMappings : HttpHandler = requireUser >=> fun next ctx -> task { // GET /admin/tag-mapping/{id}/edit let editMapping tagMapId : HttpHandler = requireUser >=> fun next ctx -> task { - let webLogId = webLogId ctx - let isNew = tagMapId = "new" - let tagMap = + let isNew = tagMapId = "new" + let tagMap = if isNew then Task.FromResult (Some { TagMap.empty with id = TagMapId "new" }) else - Data.TagMap.findById (TagMapId tagMapId) webLogId (conn ctx) + Data.TagMap.findById (TagMapId tagMapId) (webLog ctx).id (conn ctx) match! tagMap with | Some tm -> return! @@ -339,26 +346,29 @@ let editMapping tagMapId : HttpHandler = requireUser >=> fun next ctx -> task { // POST /admin/tag-mapping/save let saveMapping : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - let webLogId = webLogId ctx - let conn = conn ctx - let! model = ctx.BindFormAsync () - let tagMap = + let webLog = webLog ctx + let conn = conn ctx + let! model = ctx.BindFormAsync () + let tagMap = if model.id = "new" then - Task.FromResult (Some { TagMap.empty with id = TagMapId.create (); webLogId = webLogId }) + Task.FromResult (Some { TagMap.empty with id = TagMapId.create (); webLogId = webLog.id }) else - Data.TagMap.findById (TagMapId model.id) webLogId conn + Data.TagMap.findById (TagMapId model.id) webLog.id conn match! tagMap with | Some tm -> do! Data.TagMap.save { tm with tag = model.tag.ToLower (); urlValue = model.urlValue.ToLower () } conn do! addMessage ctx { UserMessage.success with message = "Tag mapping saved successfully" } - return! redirectToGet $"/admin/tag-mapping/{TagMapId.toString tm.id}/edit" next ctx + return! + redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/tag-mapping/{TagMapId.toString tm.id}/edit")) + next ctx | None -> return! Error.notFound next ctx } // POST /admin/tag-mapping/{id}/delete let deleteMapping tagMapId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - match! Data.TagMap.delete (TagMapId tagMapId) (webLogId ctx) (conn ctx) with + let webLog = webLog ctx + match! Data.TagMap.delete (TagMapId tagMapId) webLog.id (conn ctx) with | true -> do! addMessage ctx { UserMessage.success with message = "Tag mapping deleted successfully" } | false -> do! addMessage ctx { UserMessage.error with message = "Tag mapping not found; nothing deleted" } - return! redirectToGet "/admin/tag-mappings" next ctx + return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/tag-mappings")) next ctx } diff --git a/src/MyWebLog/Handlers/Error.fs b/src/MyWebLog/Handlers/Error.fs index 794e6bd..dfb00ee 100644 --- a/src/MyWebLog/Handlers/Error.fs +++ b/src/MyWebLog/Handlers/Error.fs @@ -3,15 +3,19 @@ module MyWebLog.Handlers.Error open System.Net open System.Threading.Tasks -open Microsoft.AspNetCore.Http open Giraffe +open Microsoft.AspNetCore.Http +open MyWebLog /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response -let notAuthorized : HttpHandler = fun next ctx -> - (next, ctx) - ||> match ctx.Request.Method with - | "GET" -> redirectTo false $"/user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}" - | _ -> setStatusCode 401 >=> fun _ _ -> Task.FromResult None +let notAuthorized : HttpHandler = fun next ctx -> task { + let webLog = ctx.Items["webLog"] :?> WebLog + if ctx.Request.Method = "GET" then + let returnUrl = WebUtility.UrlEncode ctx.Request.Path + return! redirectTo false (WebLog.relativeUrl webLog (Permalink $"user/log-on?returnUrl={returnUrl}")) next ctx + else + return! (setStatusCode 401 >=> fun _ _ -> Task.FromResult None) next ctx +} /// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there let notFound : HttpHandler = diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index 3fc78b8..eb9644e 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -67,17 +67,18 @@ let generator (ctx : HttpContext) = generatorString <- Option.ofObj cfg["Generator"] defaultArg generatorString "generator not configured" -open DotLiquid open MyWebLog +/// Get the web log for the request from the context (established by middleware) +let webLog (ctx : HttpContext) = + ctx.Items["webLog"] :?> WebLog + +open DotLiquid + /// Either get the web log from the hash, or get it from the cache and add it to the hash let private deriveWebLogFromHash (hash : Hash) ctx = - match hash.ContainsKey "web_log" with - | true -> hash["web_log"] :?> WebLog - | false -> - let wl = WebLogCache.get ctx - hash.Add ("web_log", wl) - wl + if hash.ContainsKey "web_log" then () else hash.Add ("web_log", webLog ctx) + hash["web_log"] :?> WebLog open Giraffe @@ -118,9 +119,6 @@ let redirectToGet url : HttpHandler = fun next ctx -> task { return! redirectTo false url next ctx } -/// Get the web log ID for the current request -let webLogId ctx = (WebLogCache.get ctx).id - open System.Security.Claims /// Get the user ID for the current request @@ -159,7 +157,7 @@ let templatesForTheme ctx (typ : string) = seq { KeyValuePair.Create ("", $"- Default (single-{typ}) -") yield! - Path.Combine ("themes", (WebLogCache.get ctx).themePath) + Path.Combine ("themes", (webLog ctx).themePath) |> Directory.EnumerateFiles |> Seq.filter (fun it -> it.EndsWith $"{typ}.liquid") |> Seq.map (fun it -> diff --git a/src/MyWebLog/Handlers/Post.fs b/src/MyWebLog/Handlers/Post.fs index afdf646..2978b74 100644 --- a/src/MyWebLog/Handlers/Post.fs +++ b/src/MyWebLog/Handlers/Post.fs @@ -2,21 +2,19 @@ module MyWebLog.Handlers.Post open System -open Giraffe -open Microsoft.AspNetCore.Http -/// Split the "rest" capture for categories and tags into the page number and category/tag URL parts -let private pathAndPageNumber (ctx : HttpContext) = - let slugs = (string ctx.Request.RouteValues["slug"]).Split "/" |> Array.filter (fun it -> it <> "") - let pageIdx = Array.IndexOf (slugs, "page") - let pageNbr = +/// Parse a slug and page number from an "everything else" URL +let private parseSlugAndPage (slugAndPage : string seq) = + let slugs = (slugAndPage |> Seq.skip 1 |> Seq.head).Split "/" |> Array.filter (fun it -> it <> "") + let pageIdx = Array.IndexOf (slugs, "page") + let pageNbr = match pageIdx with - | -1 -> Some 1L - | idx when idx + 2 = slugs.Length -> Some (int64 slugs[pageIdx + 1]) + | -1 -> Some 1 + | idx when idx + 2 = slugs.Length -> Some (int slugs[pageIdx + 1]) | _ -> None let slugParts = if pageIdx > 0 then Array.truncate pageIdx slugs else slugs pageNbr, String.Join ("/", slugParts) - + /// The type of post list being prepared type ListType = | AdminList @@ -47,10 +45,11 @@ open DotLiquid open MyWebLog.ViewModels /// Convert a list of posts into items ready to be displayed -let private preparePostList webLog posts listType url pageNbr perPage ctx conn = task { +let private preparePostList webLog posts listType (url : string) pageNbr perPage ctx conn = task { let! authors = getAuthors webLog posts conn let! tagMappings = getTagMappings webLog posts conn - let postItems = + let relUrl it = Some <| WebLog.relativeUrl webLog (Permalink it) + let postItems = posts |> Seq.ofList |> Seq.truncate perPage @@ -65,24 +64,24 @@ let private preparePostList webLog posts listType url pageNbr perPage ctx conn = | _ -> Task.FromResult (None, None) let newerLink = match listType, pageNbr with - | SinglePost, _ -> newerPost |> Option.map (fun p -> Permalink.toString p.permalink) - | _, 1L -> None - | PostList, 2L when webLog.defaultPage = "posts" -> Some "" - | PostList, _ -> Some $"page/{pageNbr - 1L}" - | CategoryList, 2L -> Some $"category/{url}/" - | CategoryList, _ -> Some $"category/{url}/page/{pageNbr - 1L}" - | TagList, 2L -> Some $"tag/{url}/" - | TagList, _ -> Some $"tag/{url}/page/{pageNbr - 1L}" - | AdminList, 2L -> Some "admin/posts" - | AdminList, _ -> Some $"admin/posts/page/{pageNbr - 1L}" + | SinglePost, _ -> newerPost |> Option.map (fun p -> Permalink.toString p.permalink) + | _, 1 -> None + | PostList, 2 when webLog.defaultPage = "posts" -> Some "" + | PostList, _ -> relUrl $"page/{pageNbr - 1}" + | CategoryList, 2 -> relUrl $"category/{url}/" + | CategoryList, _ -> relUrl $"category/{url}/page/{pageNbr - 1}" + | TagList, 2 -> relUrl $"tag/{url}/" + | TagList, _ -> relUrl $"tag/{url}/page/{pageNbr - 1}" + | AdminList, 2 -> relUrl "admin/posts" + | AdminList, _ -> relUrl $"admin/posts/page/{pageNbr - 1}" let olderLink = match listType, List.length posts > perPage with | SinglePost, _ -> olderPost |> Option.map (fun p -> Permalink.toString p.permalink) | _, false -> None - | PostList, true -> Some $"page/{pageNbr + 1L}" - | CategoryList, true -> Some $"category/{url}/page/{pageNbr + 1L}" - | TagList, true -> Some $"tag/{url}/page/{pageNbr + 1L}" - | AdminList, true -> Some $"admin/posts/page/{pageNbr + 1L}" + | PostList, true -> relUrl $"page/{pageNbr + 1}" + | CategoryList, true -> relUrl $"category/{url}/page/{pageNbr + 1}" + | TagList, true -> relUrl $"tag/{url}/page/{pageNbr + 1}" + | AdminList, true -> relUrl $"admin/posts/page/{pageNbr + 1}" let model = { posts = postItems authors = authors @@ -95,28 +94,30 @@ let private preparePostList webLog posts listType url pageNbr perPage ctx conn = return Hash.FromAnonymousObject {| model = model; categories = CategoryCache.get ctx; tag_mappings = tagMappings |} } +open Giraffe + // GET /page/{pageNbr} let pageOfPosts pageNbr : HttpHandler = fun next ctx -> task { - let webLog = WebLogCache.get ctx - let conn = conn ctx + let webLog = webLog ctx + let conn = conn ctx let! posts = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage conn let! hash = preparePostList webLog posts PostList "" pageNbr webLog.postsPerPage ctx conn let title = match pageNbr, webLog.defaultPage with - | 1L, "posts" -> None - | _, "posts" -> Some $"Page {pageNbr}" - | _, _ -> Some $"Page {pageNbr} « Posts" + | 1, "posts" -> None + | _, "posts" -> Some $"Page {pageNbr}" + | _, _ -> Some $"Page {pageNbr} « Posts" match title with Some ttl -> hash.Add ("page_title", ttl) | None -> () - if pageNbr = 1L && webLog.defaultPage = "posts" then hash.Add ("is_home", true) + if pageNbr = 1 && webLog.defaultPage = "posts" then hash.Add ("is_home", true) return! themedView "index" next ctx hash } // GET /category/{slug}/ // GET /category/{slug}/page/{pageNbr} -let pageOfCategorizedPosts : HttpHandler = fun next ctx -> task { - let webLog = WebLogCache.get ctx - let conn = conn ctx - match pathAndPageNumber ctx with +let pageOfCategorizedPosts slugAndPage : HttpHandler = fun next ctx -> task { + let webLog = webLog ctx + let conn = conn ctx + match parseSlugAndPage slugAndPage with | Some pageNbr, slug -> let allCats = CategoryCache.get ctx let cat = allCats |> Array.find (fun cat -> cat.slug = slug) @@ -130,7 +131,7 @@ let pageOfCategorizedPosts : HttpHandler = fun next ctx -> task { match! Data.Post.findPageOfCategorizedPosts webLog.id catIds pageNbr webLog.postsPerPage conn with | posts when List.length posts > 0 -> let! hash = preparePostList webLog posts CategoryList cat.slug pageNbr webLog.postsPerPage ctx conn - let pgTitle = if pageNbr = 1L then "" else $""" (Page {pageNbr})""" + let pgTitle = if pageNbr = 1 then "" else $""" (Page {pageNbr})""" hash.Add ("page_title", $"{cat.name}: Category Archive{pgTitle}") hash.Add ("subtitle", cat.description.Value) hash.Add ("is_category", true) @@ -143,10 +144,10 @@ open System.Web // GET /tag/{tag}/ // GET /tag/{tag}/page/{pageNbr} -let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { - let webLog = WebLogCache.get ctx - let conn = conn ctx - match pathAndPageNumber ctx with +let pageOfTaggedPosts slugAndPage : HttpHandler = fun next ctx -> task { + let webLog = webLog ctx + let conn = conn ctx + match parseSlugAndPage slugAndPage with | Some pageNbr, rawTag -> let urlTag = HttpUtility.UrlDecode rawTag let! tag = backgroundTask { @@ -157,7 +158,7 @@ let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { match! Data.Post.findPageOfTaggedPosts webLog.id tag pageNbr webLog.postsPerPage conn with | posts when List.length posts > 0 -> let! hash = preparePostList webLog posts TagList rawTag pageNbr webLog.postsPerPage ctx conn - let pgTitle = if pageNbr = 1L then "" else $""" (Page {pageNbr})""" + let pgTitle = if pageNbr = 1 then "" else $""" (Page {pageNbr})""" hash.Add ("page_title", $"Posts Tagged “{tag}”{pgTitle}") hash.Add ("is_tag", true) return! themedView "index" next ctx hash @@ -166,15 +167,18 @@ let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { let spacedTag = tag.Replace ("-", " ") match! Data.Post.findPageOfTaggedPosts webLog.id spacedTag pageNbr 1 conn with | posts when List.length posts > 0 -> - let endUrl = if pageNbr = 1L then "" else $"page/{pageNbr}" - return! redirectTo true $"""/tag/{spacedTag.Replace (" ", "+")}/{endUrl}""" next ctx + let endUrl = if pageNbr = 1 then "" else $"page/{pageNbr}" + return! + redirectTo true + (WebLog.relativeUrl webLog (Permalink $"""tag/{spacedTag.Replace (" ", "+")}/{endUrl}""")) + next ctx | _ -> return! Error.notFound next ctx | None, _ -> return! Error.notFound next ctx } // GET / let home : HttpHandler = fun next ctx -> task { - let webLog = WebLogCache.get ctx + let webLog = webLog ctx match webLog.defaultPage with | "posts" -> return! pageOfPosts 1 next ctx | pageId -> @@ -190,6 +194,7 @@ let home : HttpHandler = fun next ctx -> task { | None -> return! Error.notFound next ctx } + open System.IO open System.ServiceModel.Syndication open System.Text.RegularExpressions @@ -198,12 +203,12 @@ open System.Xml // GET /feed.xml // (Routing handled by catch-all handler for future configurability) let generateFeed : HttpHandler = fun next ctx -> backgroundTask { - let conn = conn ctx - let webLog = WebLogCache.get ctx - let urlBase = $"https://{webLog.urlBase}/" + let conn = conn ctx + let webLog = webLog ctx // TODO: hard-coded number of items - let! posts = Data.Post.findPageOfPublishedPosts webLog.id 1L 10 conn - let! authors = getAuthors webLog posts conn + let! posts = Data.Post.findPageOfPublishedPosts webLog.id 1 10 conn + let! authors = getAuthors webLog posts conn + let! tagMaps = getTagMappings webLog posts conn let cats = CategoryCache.get ctx let toItem (post : Post) = @@ -213,25 +218,29 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | txt when txt.Length < 255 -> txt | txt -> $"{txt.Substring (0, 252)}..." let item = SyndicationItem ( - Id = $"{urlBase}{Permalink.toString post.permalink}", + Id = WebLog.absoluteUrl webLog post.permalink, Title = TextSyndicationContent.CreateHtmlContent post.title, PublishDate = DateTimeOffset post.publishedOn.Value, LastUpdatedTime = DateTimeOffset post.updatedOn, Content = TextSyndicationContent.CreatePlaintextContent plainText) item.AddPermalink (Uri item.Id) - let encoded = post.text.Replace("src=\"/", $"src=\"{urlBase}").Replace ("href=\"/", $"href=\"{urlBase}") + let encoded = + post.text.Replace("src=\"/", $"src=\"{webLog.urlBase}/").Replace ("href=\"/", $"href=\"{webLog.urlBase}/") item.ElementExtensions.Add ("encoded", "http://purl.org/rss/1.0/modules/content/", encoded) item.Authors.Add (SyndicationPerson ( Name = (authors |> List.find (fun a -> a.name = WebLogUserId.toString post.authorId)).value)) [ post.categoryIds |> List.map (fun catId -> let cat = cats |> Array.find (fun c -> c.id = CategoryId.toString catId) - SyndicationCategory (cat.name, $"{urlBase}category/{cat.slug}/", cat.name)) + SyndicationCategory (cat.name, WebLog.absoluteUrl webLog (Permalink $"category/{cat.slug}/"), cat.name)) post.tags |> List.map (fun tag -> - let urlTag = tag.Replace (" ", "+") - SyndicationCategory (tag, $"{urlBase}tag/{urlTag}/", $"{tag} (tag)")) + let urlTag = + match tagMaps |> List.tryFind (fun tm -> tm.tag = tag) with + | Some tm -> tm.urlValue + | None -> tag.Replace (" ", "+") + SyndicationCategory (tag, WebLog.absoluteUrl webLog (Permalink $"tag/{urlTag}/"), $"{tag} (tag)")) ] |> List.concat |> List.iter item.Categories.Add @@ -245,12 +254,12 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { feed.Generator <- generator ctx feed.Items <- posts |> Seq.ofList |> Seq.map toItem feed.Language <- "en" - feed.Id <- urlBase + feed.Id <- webLog.urlBase - feed.Links.Add (SyndicationLink (Uri $"{urlBase}feed.xml", "self", "", "application/rss+xml", 0L)) + feed.Links.Add (SyndicationLink (Uri $"{webLog.urlBase}/feed.xml", "self", "", "application/rss+xml", 0L)) feed.AttributeExtensions.Add (XmlQualifiedName ("content", "http://www.w3.org/2000/xmlns/"), "http://purl.org/rss/1.0/modules/content/") - feed.ElementExtensions.Add ("link", "", urlBase) + feed.ElementExtensions.Add ("link", "", webLog.urlBase) use mem = new MemoryStream () use xml = XmlWriter.Create mem @@ -266,15 +275,18 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { /// Sequence where the first returned value is the proper handler for the link let private deriveAction ctx : HttpHandler seq = - let webLog = WebLogCache.get ctx - let conn = conn ctx - let textLink = string ctx.Request.RouteValues["link"] - let permalink = Permalink textLink + let webLog = webLog ctx + let conn = conn ctx + let _, extra = WebLog.hostAndPath webLog + let textLink = if extra = "" then ctx.Request.Path.Value else ctx.Request.Path.Value.Substring extra.Length let await it = (Async.AwaitTask >> Async.RunSynchronously) it seq { + // Home page directory without the directory slash + if textLink = "" then yield redirectTo true (WebLog.relativeUrl webLog Permalink.empty) + let permalink = Permalink (textLink.Substring 1) // Current post match Data.Post.findByPermalink permalink webLog.id conn |> await with - | Some post -> + | Some post -> let model = preparePostList webLog [ post ] SinglePost "" 1 1 ctx conn |> await model.Add ("page_title", post.title) yield fun next ctx -> themedView "single-post" next ctx model @@ -288,27 +300,27 @@ let private deriveAction ctx : HttpHandler seq = | None -> () // RSS feed // TODO: configure this via web log - if textLink = "feed.xml" then yield generateFeed + if textLink = "/feed.xml" then yield generateFeed // Post differing only by trailing slash let altLink = Permalink (if textLink.EndsWith "/" then textLink[..textLink.Length - 2] else $"{textLink}/") match Data.Post.findByPermalink altLink webLog.id conn |> await with - | Some post -> yield redirectTo true $"/{Permalink.toString post.permalink}" + | Some post -> yield redirectTo true (WebLog.relativeUrl webLog post.permalink) | None -> () // Page differing only by trailing slash match Data.Page.findByPermalink altLink webLog.id conn |> await with - | Some page -> yield redirectTo true $"/{Permalink.toString page.permalink}" + | Some page -> yield redirectTo true (WebLog.relativeUrl webLog page.permalink) | None -> () // Prior post match Data.Post.findCurrentPermalink [ permalink; altLink ] webLog.id conn |> await with - | Some link -> yield redirectTo true $"/{Permalink.toString link}" + | Some link -> yield redirectTo true (WebLog.relativeUrl webLog link) | None -> () // Prior page match Data.Page.findCurrentPermalink [ permalink; altLink ] webLog.id conn |> await with - | Some link -> yield redirectTo true $"/{Permalink.toString link}" + | Some link -> yield redirectTo true (WebLog.relativeUrl webLog link) | None -> () } -// GET {**link} +// GET {all-of-the-above} let catchAll : HttpHandler = fun next ctx -> task { match deriveAction ctx |> Seq.tryHead with | Some handler -> return! handler next ctx @@ -318,8 +330,8 @@ let catchAll : HttpHandler = fun next ctx -> task { // GET /admin/posts // GET /admin/posts/page/{pageNbr} let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { - let webLog = WebLogCache.get ctx - let conn = conn ctx + let webLog = webLog ctx + let conn = conn ctx let! posts = Data.Post.findPageOfPosts webLog.id pageNbr 25 conn let! hash = preparePostList webLog posts AdminList "" pageNbr 25 ctx conn hash.Add ("page_title", "Posts") @@ -328,8 +340,8 @@ let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { // GET /admin/post/{id}/edit let edit postId : HttpHandler = requireUser >=> fun next ctx -> task { - let webLog = WebLogCache.get ctx - let conn = conn ctx + let webLog = webLog ctx + let conn = conn ctx let! result = task { match postId with | "new" -> return Some ("Write a New Post", { Post.empty with id = PostId "new" }) @@ -354,7 +366,7 @@ let edit postId : HttpHandler = requireUser >=> fun next ctx -> task { // GET /admin/post/{id}/permalinks let editPermalinks postId : HttpHandler = requireUser >=> fun next ctx -> task { - match! Data.Post.findByFullId (PostId postId) (webLogId ctx) (conn ctx) with + match! Data.Post.findByFullId (PostId postId) (webLog ctx).id (conn ctx) with | Some post -> return! Hash.FromAnonymousObject {| @@ -368,41 +380,43 @@ let editPermalinks postId : HttpHandler = requireUser >=> fun next ctx -> task { // POST /admin/post/permalinks let savePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - let! model = ctx.BindFormAsync () - let links = model.prior |> Array.map Permalink |> List.ofArray - match! Data.Post.updatePriorPermalinks (PostId model.id) (webLogId ctx) links (conn ctx) with + let webLog = webLog ctx + let! model = ctx.BindFormAsync () + let links = model.prior |> Array.map Permalink |> List.ofArray + match! Data.Post.updatePriorPermalinks (PostId model.id) webLog.id links (conn ctx) with | true -> do! addMessage ctx { UserMessage.success with message = "Post permalinks saved successfully" } - return! redirectToGet $"/admin/post/{model.id}/permalinks" next ctx + return! redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/post/{model.id}/permalinks")) next ctx | false -> return! Error.notFound next ctx } // POST /admin/post/{id}/delete let delete postId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - match! Data.Post.delete (PostId postId) (webLogId ctx) (conn ctx) with + let webLog = webLog ctx + match! Data.Post.delete (PostId postId) webLog.id (conn ctx) 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 + return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/posts")) next ctx } #nowarn "3511" // POST /admin/post/save let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - let! model = ctx.BindFormAsync () - let webLogId = webLogId ctx - let conn = conn ctx - let now = DateTime.UtcNow - let! pst = task { + let! model = ctx.BindFormAsync () + let webLog = webLog ctx + let conn = conn ctx + let now = DateTime.UtcNow + let! pst = task { match model.postId with | "new" -> return Some { Post.empty with id = PostId.create () - webLogId = webLogId + webLogId = webLog.id authorId = userId ctx } - | postId -> return! Data.Post.findByFullId (PostId postId) webLogId conn + | postId -> return! Data.Post.findByFullId (PostId postId) webLog.id conn } match pst with | Some post -> @@ -460,6 +474,7 @@ let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { |> List.length = List.length pst.Value.categoryIds) then do! CategoryCache.update ctx do! addMessage ctx { UserMessage.success with message = "Post saved successfully" } - return! redirectToGet $"/admin/post/{PostId.toString post.id}/edit" next ctx + return! + redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/post/{PostId.toString post.id}/edit")) next ctx | None -> return! Error.notFound next ctx } diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs index 42609bc..6694986 100644 --- a/src/MyWebLog/Handlers/Routes.fs +++ b/src/MyWebLog/Handlers/Routes.fs @@ -1,75 +1,89 @@ /// Routes for this application module MyWebLog.Handlers.Routes +open Giraffe +open MyWebLog + +let router : HttpHandler = choose [ + GET >=> choose [ + route "/" >=> Post.home + ] + subRoute "/admin" (requireUser >=> choose [ + GET >=> choose [ + route "" >=> Admin.dashboard + subRoute "/categor" (choose [ + route "ies" >=> Admin.listCategories + routef "y/%s/edit" Admin.editCategory + ]) + subRoute "/page" (choose [ + route "s" >=> Admin.listPages 1 + routef "s/page/%i" Admin.listPages + routef "/%s/edit" Admin.editPage + routef "/%s/permalinks" Admin.editPagePermalinks + ]) + 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 "/settings" >=> Admin.settings + subRoute "/tag-mapping" (choose [ + route "s" >=> Admin.tagMappings + routef "/%s/edit" Admin.editMapping + ]) + route "/user/edit" >=> User.edit + ] + POST >=> choose [ + subRoute "/category" (choose [ + route "/save" >=> Admin.saveCategory + routef "/%s/delete" Admin.deleteCategory + ]) + subRoute "/page" (choose [ + route "/save" >=> Admin.savePage + route "/permalinks" >=> Admin.savePagePermalinks + routef "/%s/delete" Admin.deletePage + ]) + subRoute "/post" (choose [ + route "/save" >=> Post.save + route "/permalinks" >=> Post.savePermalinks + routef "/%s/delete" Post.delete + ]) + route "/settings" >=> Admin.saveSettings + subRoute "/tag-mapping" (choose [ + route "/save" >=> Admin.saveMapping + routef "/%s/delete" Admin.deleteMapping + ]) + route "/user/save" >=> User.save + ] + ]) + GET >=> routexp "/category/(.*)" Post.pageOfCategorizedPosts + GET >=> routef "/page/%i" Post.pageOfPosts + GET >=> routexp "/tag/(.*)" Post.pageOfTaggedPosts + subRoute "/user" (choose [ + GET >=> choose [ + route "/log-on" >=> User.logOn None + route "/log-off" >=> User.logOff + ] + POST >=> choose [ + route "/log-on" >=> User.doLogOn + ] + ]) + GET >=> Post.catchAll + Error.notFound +] + +/// Wrap a router in a sub-route +let routerWithPath extraPath : HttpHandler = + subRoute extraPath router + +/// Handler to apply Giraffe routing with a possible sub-route +let handleRoute : HttpHandler = fun next ctx -> task { + let _, extraPath = WebLog.hostAndPath (webLog ctx) + return! (if extraPath = "" then router else routerWithPath extraPath) next ctx +} + open Giraffe.EndpointRouting -/// The endpoints defined in the above handlers -let endpoints = [ - GET [ - route "/" Post.home - ] - subRoute "/admin" [ - GET [ - route "" Admin.dashboard - subRoute "/categor" [ - route "ies" Admin.listCategories - routef "y/%s/edit" Admin.editCategory - ] - subRoute "/page" [ - route "s" (Admin.listPages 1) - routef "s/page/%d" Admin.listPages - routef "/%s/edit" Admin.editPage - routef "/%s/permalinks" Admin.editPagePermalinks - ] - subRoute "/post" [ - route "s" (Post.all 1) - routef "s/page/%d" Post.all - routef "/%s/edit" Post.edit - routef "/%s/permalinks" Post.editPermalinks - ] - route "/settings" Admin.settings - subRoute "/tag-mapping" [ - route "s" Admin.tagMappings - routef "/%s/edit" Admin.editMapping - ] - route "/user/edit" User.edit - ] - POST [ - subRoute "/category" [ - route "/save" Admin.saveCategory - routef "/%s/delete" Admin.deleteCategory - ] - subRoute "/page" [ - route "/save" Admin.savePage - route "/permalinks" Admin.savePagePermalinks - routef "/%s/delete" Admin.deletePage - ] - subRoute "/post" [ - route "/save" Post.save - route "/permalinks" Post.savePermalinks - routef "/%s/delete" Post.delete - ] - route "/settings" Admin.saveSettings - subRoute "/tag-mapping" [ - route "/save" Admin.saveMapping - routef "/%s/delete" Admin.deleteMapping - ] - route "/user/save" User.save - ] - ] - GET [ - route "/category/{**slug}" Post.pageOfCategorizedPosts - routef "/page/%d" Post.pageOfPosts - route "/tag/{**slug}" Post.pageOfTaggedPosts - ] - subRoute "/user" [ - GET [ - route "/log-on" (User.logOn None) - route "/log-off" User.logOff - ] - POST [ - route "/log-on" User.doLogOn - ] - ] - route "{**link}" Post.catchAll -] +/// Endpoint-routed handler to deal with sub-routes +let endpoint = [ route "{**url}" handleRoute ] diff --git a/src/MyWebLog/Handlers/User.fs b/src/MyWebLog/Handlers/User.fs index cfa4c69..9e7f9c6 100644 --- a/src/MyWebLog/Handlers/User.fs +++ b/src/MyWebLog/Handlers/User.fs @@ -41,7 +41,7 @@ open MyWebLog // POST /user/log-on let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { let! model = ctx.BindFormAsync () - let webLog = WebLogCache.get ctx + let webLog = webLog ctx match! Data.WebLogUser.findByEmail model.emailAddress webLog.id (conn ctx) with | Some user when user.passwordHash = hashedPassword model.password user.userName user.salt -> let claims = seq { @@ -56,7 +56,7 @@ let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow)) do! addMessage ctx { UserMessage.success with message = $"Logged on successfully | Welcome to {webLog.name}!" } - return! redirectToGet (match model.returnTo with Some url -> url | None -> "/admin") next ctx + return! redirectToGet (defaultArg model.returnTo (WebLog.relativeUrl webLog (Permalink "admin"))) next ctx | _ -> do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" } return! logOn model.returnTo next ctx @@ -66,7 +66,7 @@ let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { let logOff : HttpHandler = fun next ctx -> task { do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme do! addMessage ctx { UserMessage.info with message = "Log off successful" } - return! redirectToGet "/" next ctx + return! redirectToGet (WebLog.relativeUrl (webLog ctx) Permalink.empty) next ctx } /// Display the user edit page, with information possibly filled in @@ -107,7 +107,7 @@ let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { do! Data.WebLogUser.update user conn let pwMsg = if model.newPassword = "" then "" else " and updated your password" do! addMessage ctx { UserMessage.success with message = $"Saved your information{pwMsg} successfully" } - return! redirectToGet "/admin/user/edit" next ctx + return! redirectToGet (WebLog.relativeUrl (webLog ctx) (Permalink "admin/user/edit")) next ctx | None -> return! Error.notFound next ctx else do! addMessage ctx { UserMessage.error with message = "Passwords did not match; no updates made" } diff --git a/src/MyWebLog/MyWebLog.fsproj b/src/MyWebLog/MyWebLog.fsproj index e3991e8..0fa4905 100644 --- a/src/MyWebLog/MyWebLog.fsproj +++ b/src/MyWebLog/MyWebLog.fsproj @@ -15,6 +15,7 @@ + diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs index 3fabce8..8291e78 100644 --- a/src/MyWebLog/Program.fs +++ b/src/MyWebLog/Program.fs @@ -1,100 +1,23 @@ -open System.Collections.Generic -open Microsoft.AspNetCore.Http -open Microsoft.Extensions.DependencyInjection +open Microsoft.AspNetCore.Http open MyWebLog -open RethinkDb.Driver.Net -open System /// Middleware to derive the current web log type WebLogMiddleware (next : RequestDelegate) = member this.InvokeAsync (ctx : HttpContext) = task { - match WebLogCache.exists ctx with - | true -> return! next.Invoke ctx - | false -> - let conn = ctx.RequestServices.GetRequiredService () - match! Data.WebLog.findByHost (Cache.makeKey ctx) conn with - | Some webLog -> - WebLogCache.set ctx webLog - do! PageListCache.update ctx - do! CategoryCache.update ctx - return! next.Invoke ctx - | None -> ctx.Response.StatusCode <- 404 + if WebLogCache.exists ctx then + ctx.Items["webLog"] <- WebLogCache.get ctx + if PageListCache.exists ctx then () else do! PageListCache.update ctx + if CategoryCache.exists ctx then () else do! CategoryCache.update ctx + return! next.Invoke ctx + else + ctx.Response.StatusCode <- 404 } -/// DotLiquid filters -module DotLiquidBespoke = - - open System.IO - open DotLiquid - open MyWebLog.ViewModels - - /// A filter to generate a link with posts categorized under the given category - type CategoryLinkFilter () = - static member CategoryLink (_ : Context, catObj : obj) = - match catObj with - | :? DisplayCategory as cat -> $"/category/{cat.slug}/" - | :? DropProxy as proxy -> $"""/category/{proxy["slug"]}/""" - | _ -> $"alert('unknown category object type {catObj.GetType().Name}')" - - /// A filter to generate a link that will edit a page - type EditPageLinkFilter () = - static member EditPageLink (_ : Context, postId : string) = - $"/admin/page/{postId}/edit" - - /// A filter to generate a link that will edit a post - type EditPostLinkFilter () = - static member EditPostLink (_ : Context, postId : string) = - $"/admin/post/{postId}/edit" - - /// A filter to generate nav links, highlighting the active link (exact match) - type NavLinkFilter () = - static member NavLink (ctx : Context, url : string, text : string) = - seq { - "
  • " - text - "
  • " - } - |> Seq.fold (+) "" - - /// A filter to generate a link with posts tagged with the given tag - type TagLinkFilter () = - static member TagLink (ctx : Context, tag : string) = - match ctx.Environments[0].["tag_mappings"] :?> TagMap list - |> List.tryFind (fun it -> it.tag = tag) with - | Some tagMap -> $"/tag/{tagMap.urlValue}/" - | None -> $"""/tag/{tag.Replace (" ", "+")}/""" - - /// Create links for a user to log on or off, and a dashboard link if they are logged off - type UserLinksTag () = - inherit Tag () - - override this.Render (context : Context, result : TextWriter) = - seq { - """" - } - |> Seq.iter result.WriteLine - - /// A filter to retrieve the value of a meta item from a list - // (shorter than `{% assign item = list | where: "name", [name] | first %}{{ item.value }}`) - type ValueFilter () = - static member Value (_ : Context, items : MetaItem list, name : string) = - match items |> List.tryFind (fun it -> it.name = name) with - | Some item -> item.value - | None -> $"-- {name} not found --" - +open System +open Microsoft.Extensions.DependencyInjection +open RethinkDb.Driver.Net /// Create the default information for a new web log module NewWebLog = @@ -220,7 +143,9 @@ module NewWebLog = } +open System.Collections.Generic open DotLiquid +open DotLiquidBespoke open Giraffe open Giraffe.EndpointRouting open Microsoft.AspNetCore.Antiforgery @@ -257,6 +182,7 @@ let main args = task { let! conn = rethinkCfg.CreateConnectionAsync () do! Data.Startup.ensureDb rethinkCfg (loggerFac.CreateLogger (nameof Data.Startup)) conn + do! WebLogCache.fill conn return conn } |> Async.AwaitTask |> Async.RunSynchronously let _ = builder.Services.AddSingleton conn @@ -273,13 +199,12 @@ let main args = let _ = builder.Services.AddGiraffe () // Set up DotLiquid - [ typeof; typeof - typeof; typeof - typeof; typeof + [ typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof ] |> List.iter Template.RegisterFilter - Template.RegisterTag "user_links" + Template.RegisterTag "user_links" [ // Domain types typeof; typeof; typeof; typeof @@ -308,7 +233,7 @@ let main args = let _ = app.UseStaticFiles () let _ = app.UseRouting () let _ = app.UseSession () - let _ = app.UseGiraffe Handlers.Routes.endpoints + let _ = app.UseGiraffe Handlers.Routes.endpoint app.Run() diff --git a/src/MyWebLog/themes/admin/category-edit.liquid b/src/MyWebLog/themes/admin/category-edit.liquid index 720f919..5a2fe39 100644 --- a/src/MyWebLog/themes/admin/category-edit.liquid +++ b/src/MyWebLog/themes/admin/category-edit.liquid @@ -1,6 +1,6 @@ 

    {{ page_title }}

    -
    +
    diff --git a/src/MyWebLog/themes/admin/category-list.liquid b/src/MyWebLog/themes/admin/category-list.liquid index 2cfe7ae..e98e9f0 100644 --- a/src/MyWebLog/themes/admin/category-list.liquid +++ b/src/MyWebLog/themes/admin/category-list.liquid @@ -1,6 +1,6 @@ 

    {{ page_title }}

    - Add a New Category + Add a New Category @@ -17,16 +17,19 @@ {%- endif %} {{ cat.name }}
    - {%- if cat.post_count > 0 %} - - View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%} - + {%- if cat.post_count > 0 %} + + View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%} + + + {%- endif %} + {%- capture cat_edit %}admin/category/{{ cat.id }}/edit{% endcapture -%} + Edit - {%- endif %} - Edit - - + {%- capture cat_del %}admin/category/{{ cat.id }}/delete{% endcapture -%} + {%- capture cat_del_link %}{{ cat_del | relative_link }}{% endcapture -%} + Delete diff --git a/src/MyWebLog/themes/admin/dashboard.liquid b/src/MyWebLog/themes/admin/dashboard.liquid index 452e4f3..fb0ece6 100644 --- a/src/MyWebLog/themes/admin/dashboard.liquid +++ b/src/MyWebLog/themes/admin/dashboard.liquid @@ -9,8 +9,8 @@ Published {{ model.posts }}   Drafts {{ model.drafts }} - View All - Write a New Post + View All + Write a New Post @@ -22,8 +22,8 @@ All {{ model.pages }}   Shown in Page List {{ model.listed_pages }} - View All - Create a New Page + View All + Create a New Page @@ -37,15 +37,15 @@ All {{ model.categories }}   Top Level {{ model.top_level_categories }} - View All - Add a New Category + View All + Add a New Category diff --git a/src/MyWebLog/themes/admin/layout.liquid b/src/MyWebLog/themes/admin/layout.liquid index 1a8fceb..c7c6b70 100644 --- a/src/MyWebLog/themes/admin/layout.liquid +++ b/src/MyWebLog/themes/admin/layout.liquid @@ -12,7 +12,7 @@
    @@ -17,12 +17,15 @@ {%- if pg.is_default %}   HOME PAGE{% endif -%} {%- if pg.show_in_page_list %}   IN PAGE LIST {% endif -%}
    - View Page + {%- capture pg_link %}{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}{% endcapture -%} + View Page - Edit + Edit - + {%- capture pg_del %}admin/page/{{ pg.id }}/delete{% endcapture -%} + {%- capture pg_del_link %}{{ pg_del | relative_link }}{% endcapture -%} + Delete diff --git a/src/MyWebLog/themes/admin/permalinks.liquid b/src/MyWebLog/themes/admin/permalinks.liquid index 7acd1dc..9b82d8b 100644 --- a/src/MyWebLog/themes/admin/permalinks.liquid +++ b/src/MyWebLog/themes/admin/permalinks.liquid @@ -1,6 +1,7 @@ 

    {{ page_title }}

    - + {%- capture form_action %}admin/{{ model.entity }}/permalinks{% endcapture -%} +
    @@ -10,7 +11,8 @@ {{ model.current_title }}
    {{ model.current_permalink }}
    - « Back to Edit {{ model.entity | capitalize }} + {%- capture back_link %}admin/{{ model.entity }}/{{ model.id }}/edit{% endcapture -%} + « Back to Edit {{ model.entity | capitalize }}

    diff --git a/src/MyWebLog/themes/admin/post-edit.liquid b/src/MyWebLog/themes/admin/post-edit.liquid index ae6bbac..a3e04d6 100644 --- a/src/MyWebLog/themes/admin/post-edit.liquid +++ b/src/MyWebLog/themes/admin/post-edit.liquid @@ -1,6 +1,6 @@ 

    {{ page_title }}

    - +
    @@ -16,7 +16,8 @@ value="{{ model.permalink }}"> {%- if model.page_id != "new" %} - Manage Permalinks + {%- capture perm_edit %}admin/post/{{ model.post_id }}/permalinks{% endcapture -%} + Manage Permalinks {% endif -%}
    diff --git a/src/MyWebLog/themes/admin/post-list.liquid b/src/MyWebLog/themes/admin/post-list.liquid index 5f654ea..08d55aa 100644 --- a/src/MyWebLog/themes/admin/post-list.liquid +++ b/src/MyWebLog/themes/admin/post-list.liquid @@ -1,6 +1,6 @@

    {{ page_title }}

    @@ -23,12 +23,14 @@
    {{ post.title }}
    - View Post + View Post - Edit + Edit - + {%- capture post_del %}admin/post/{{ pg.id }}/delete{% endcapture -%} + {%- capture post_del_link %}{{ post_del | relative_link }}{% endcapture -%} + Delete @@ -44,12 +46,12 @@
    {% if model.newer_link %} -

    « Newer Posts

    +

    « Newer Posts

    {% endif %}
    {% if model.older_link %} -

    Older Posts »

    +

    Older Posts »

    {% endif %}
    diff --git a/src/MyWebLog/themes/admin/settings.liquid b/src/MyWebLog/themes/admin/settings.liquid index 116536d..df2e309 100644 --- a/src/MyWebLog/themes/admin/settings.liquid +++ b/src/MyWebLog/themes/admin/settings.liquid @@ -1,6 +1,6 @@ 

    {{ web_log.name }} Settings

    - +
    diff --git a/src/MyWebLog/themes/admin/tag-mapping-edit.liquid b/src/MyWebLog/themes/admin/tag-mapping-edit.liquid index 8c8f0a9..af98b0b 100644 --- a/src/MyWebLog/themes/admin/tag-mapping-edit.liquid +++ b/src/MyWebLog/themes/admin/tag-mapping-edit.liquid @@ -1,12 +1,12 @@ 

    {{ page_title }}

    - +
    diff --git a/src/MyWebLog/themes/admin/tag-mapping-list.liquid b/src/MyWebLog/themes/admin/tag-mapping-list.liquid index c430ae7..856e864 100644 --- a/src/MyWebLog/themes/admin/tag-mapping-list.liquid +++ b/src/MyWebLog/themes/admin/tag-mapping-list.liquid @@ -1,6 +1,8 @@ 

    {{ page_title }}

    - Add a New Tag Mapping + + Add a New Tag Mapping + @@ -15,8 +17,13 @@
    {{ map.tag }}
    - + {%- capture map_edit %}admin/tag-mapping/{{ map_id }}/edit{% endcapture -%} + Edit + + {%- capture map_del %}admin/tag-mapping/{{ map_id }}/delete{% endcapture -%} + {%- capture map_del_link %}{{ map_del | relative_link }}{% endcapture -%} + Delete diff --git a/src/MyWebLog/themes/admin/user-edit.liquid b/src/MyWebLog/themes/admin/user-edit.liquid index 5e13300..b82908e 100644 --- a/src/MyWebLog/themes/admin/user-edit.liquid +++ b/src/MyWebLog/themes/admin/user-edit.liquid @@ -1,6 +1,6 @@

    {{ page_title }}

    - +
    diff --git a/src/MyWebLog/themes/bit-badger/home-page.liquid b/src/MyWebLog/themes/bit-badger/home-page.liquid index 94ac8cb..7712f8c 100644 --- a/src/MyWebLog/themes/bit-badger/home-page.liquid +++ b/src/MyWebLog/themes/bit-badger/home-page.liquid @@ -2,7 +2,7 @@
    diff --git a/src/MyWebLog/themes/daniel-j-summers/index.liquid b/src/MyWebLog/themes/daniel-j-summers/index.liquid index 362806c..c1b5781 100644 --- a/src/MyWebLog/themes/daniel-j-summers/index.liquid +++ b/src/MyWebLog/themes/daniel-j-summers/index.liquid @@ -8,7 +8,8 @@ {%- for post in model.posts %}

    - + {{ post.title }}

    @@ -24,7 +25,7 @@ {% if logged_on %} - Edit Post + Edit Post {% endif %} @@ -34,12 +35,12 @@ @@ -74,7 +75,7 @@ {% for cat in categories -%} {%- assign indent = cat.parent_names | size -%}
  • 0 %} style="padding-left:{{ indent }}rem;"{% endif %}> - {{ cat.name }} + {{ cat.name }} {{ cat.post_count }}
  • {%- endfor %} diff --git a/src/MyWebLog/themes/daniel-j-summers/layout.liquid b/src/MyWebLog/themes/daniel-j-summers/layout.liquid index d4bc29e..f3b5cdc 100644 --- a/src/MyWebLog/themes/daniel-j-summers/layout.liquid +++ b/src/MyWebLog/themes/daniel-j-summers/layout.liquid @@ -18,17 +18,19 @@ {%- if is_home %} - + + {%- endif %}
    diff --git a/src/MyWebLog/wwwroot/themes/admin/admin.js b/src/MyWebLog/wwwroot/themes/admin/admin.js index e3c6822..e9dafaf 100644 --- a/src/MyWebLog/wwwroot/themes/admin/admin.js +++ b/src/MyWebLog/wwwroot/themes/admin/admin.js @@ -156,58 +156,52 @@ }, /** - * Confirm and delete a category - * @param id The ID of the category to be deleted - * @param name The name of the category to be deleted + * Confirm and delete an item + * @param name The name of the item to be deleted + * @param url The URL to which the form should be posted */ - deleteCategory(id, name) { - if (confirm(`Are you sure you want to delete the category "${name}"? This action cannot be undone.`)) { + deleteItem(name, url) { + if (confirm(`Are you sure you want to delete the ${name}? This action cannot be undone.`)) { const form = document.getElementById("deleteForm") - form.action = `/admin/category/${id}/delete` + form.action = url form.submit() } return false }, + + /** + * Confirm and delete a category + * @param name The name of the category to be deleted + * @param url The URL to which the form should be posted + */ + deleteCategory(name, url) { + return this.deleteItem(`category "${name}"`, url) + }, /** * Confirm and delete a page - * @param id The ID of the page to be deleted * @param title The title of the page to be deleted + * @param url The URL to which the form should be posted */ - deletePage(id, title) { - if (confirm(`Are you sure you want to delete the page "${name}"? This action cannot be undone.`)) { - const form = document.getElementById("deleteForm") - form.action = `/admin/page/${id}/delete` - form.submit() - } - return false + deletePage(title, url) { + return this.deleteItem(`page "${title}"`, url) }, /** * Confirm and delete a post - * @param id The ID of the post to be deleted * @param title The title of the post to be deleted + * @param url The URL to which the form should be posted */ - deletePost(id, title) { - if (confirm(`Are you sure you want to delete the post "${name}"? This action cannot be undone.`)) { - const form = document.getElementById("deleteForm") - form.action = `/admin/post/${id}/delete` - form.submit() - } - return false + deletePost(title, url) { + return this.deleteItem(`post "${title}"`, url) }, /** * Confirm and delete a tag mapping - * @param id The ID of the mapping to be deleted * @param tag The tag for which the mapping will be deleted + * @param url The URL to which the form should be posted */ - deleteTagMapping(id, tag) { - if (confirm(`Are you sure you want to delete the mapping for "${tag}"? This action cannot be undone.`)) { - const form = document.getElementById("deleteForm") - form.action = `/admin/tag-mapping/${id}/delete` - form.submit() - } - return false + deleteTagMapping(tag, url) { + return this.deleteItem(`mapping for "${tag}"`, url) } }