From b3eab095766354b70d5b7d5a69e49c97f9b4b51c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 7 Jul 2026 22:57:57 -0400 Subject: [PATCH] Add Fluid where filter, fix slug (#47) - Fix issue with path base and logon redirects --- src/MyWebLog/Handlers/Helpers.fs | 28 +++++++++++++++++----------- src/MyWebLog/Template.fs | 21 ++++++++++++++++++--- src/default-theme/index.liquid | 4 ++-- src/default-theme/single-post.liquid | 4 ++-- src/default-theme/version.txt | 2 +- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index d84fbab..71f06a2 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -7,11 +7,11 @@ open MyWebLog.Views /// Session extensions to get and set objects type ISession with - + /// Set an item in the session member this.Set<'T>(key, item: 'T) = this.SetString(key, JsonSerializer.Serialize item) - + /// Get an item from the session member this.TryGet<'T> key = match this.GetString key with @@ -139,7 +139,13 @@ module Error = /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response let notAuthorized : HttpHandler = fun next ctx -> if ctx.Request.Method = "GET" then - let redirectUrl = $"user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}" + let fullPath = + String.concat "" + (seq { + if ctx.Request.PathBase.HasValue then yield ctx.Request.PathBase.Value + yield ctx.Request.Path.Value + }) + let redirectUrl = $"user/log-on?returnUrl={WebUtility.UrlEncode fullPath}" (next, ctx) ||> if isHtmx ctx then withHxRedirect redirectUrl >=> withHxRetarget "body" >=> redirectToGet redirectUrl else redirectToGet redirectUrl @@ -161,7 +167,7 @@ module Error = |] RequestErrors.notFound (messagesToHeaders messages) earlyReturn ctx else RequestErrors.NOT_FOUND "Not found" earlyReturn ctx) - + let server message : HttpHandler = handleContext (fun ctx -> if isHtmx ctx then @@ -173,11 +179,11 @@ module Error = /// Render a view for the specified theme, using the specified template, layout, and context let viewForTheme themeId template next ctx (viewCtx: AppViewContext) = task { let! updated = updateViewContext ctx viewCtx - + // NOTE: Although Fluid's view engine support implements layouts and sections, it also relies on the filesystem. // As we are loading templates from memory or a database, we do a 2-pass render; the first for the content, // the second for the overall page. - + // Render view content... match! Template.Cache.get themeId template ctx.Data with | Ok contentTemplate -> @@ -193,12 +199,12 @@ let viewForTheme themeId template next ctx (viewCtx: AppViewContext) = task { let bareForTheme themeId template next ctx viewCtx = task { let! updated = updateViewContext ctx viewCtx let withContent = task { - if updated.Content = "" then + if updated.Content = "" then match! Template.Cache.get themeId template ctx.Data with | Ok contentTemplate -> return Ok { updated with Content = Template.render contentTemplate updated ctx.Data } | Error message -> return Error message else - return Ok viewCtx + return Ok viewCtx } match! withContent with | Ok completeCtx -> @@ -260,7 +266,7 @@ let requireAccess level : HttpHandler = fun next ctx -> task { return! Error.notAuthorized next ctx } -/// Determine if a user is authorized to edit a page or post, given the author +/// Determine if a user is authorized to edit a page or post, given the author let canEdit authorId (ctx: HttpContext) = ctx.UserId = authorId || ctx.HasAccessLevel Editor @@ -306,7 +312,7 @@ let getTagMappings (webLog: WebLog) (posts: Post list) (data: IData) = |> List.distinct |> fun tags -> data.TagMap.FindMappingForTags tags webLog.Id -/// Get all category IDs for the given slug (includes owned subcategories) +/// Get all category IDs for the given slug (includes owned subcategories) let getCategoryIds slug ctx = let allCats = CategoryCache.get ctx let cat = allCats |> Array.find (fun cat -> cat.Slug = slug) @@ -319,7 +325,7 @@ let getCategoryIds slug ctx = open NodaTime -/// Parse a date/time to UTC +/// Parse a date/time to UTC let parseToUtc (date: string) : Instant = let result = roundTrip.Parse date if result.Success then result.Value else raise result.Exception diff --git a/src/MyWebLog/Template.fs b/src/MyWebLog/Template.fs index 688e95a..f09c0ba 100644 --- a/src/MyWebLog/Template.fs +++ b/src/MyWebLog/Template.fs @@ -172,6 +172,21 @@ let options () = |> Option.defaultValue $"-- {name} not found --" |> sValue) + // A filter to search through a collection of objects for a matching property + it.Filters.AddFilter("where", + fun input args ctx -> + let name = args.At(0).ToStringValue() + let value = args.At(1).ToStringValue() + input.Enumerate(ctx) + |> Seq.filter (fun it -> + let theItem = it.ToObjectValue() + match theItem.GetType().GetProperties() |> Array.tryFind (fun x -> x.Name = name) with + | Some pi -> pi.GetValue(theItem) = value + | None -> false) + |> Seq.toArray + |> ArrayValue :> FluidValue + |> VTask) + it open Giraffe.Htmx.Common @@ -234,11 +249,11 @@ let parser = writer.WriteLine $"""{s}""" if app.WebLog.Rss.IsCategoryEnabled && app.IsCategoryHome then - let slug = context.AmbientValues["slug"] :?> string + let slug = (context.Model.ToObjectValue() :?> AppViewContext).Slug writer.WriteLine(feedLink app.WebLog.Name $"category/{slug}/{app.WebLog.Rss.FeedName}") if app.WebLog.Rss.IsTagEnabled && app.IsTagHome then - let slug = context.AmbientValues["slug"] :?> string + let slug = (context.Model.ToObjectValue() :?> AppViewContext).Slug writer.WriteLine(feedLink app.WebLog.Name $"tag/{slug}/{app.WebLog.Rss.FeedName}") if app.IsPost then @@ -287,7 +302,7 @@ let parser = "" } |> Seq.iter writer.WriteLine - ok()) + ok ()) it diff --git a/src/default-theme/index.liquid b/src/default-theme/index.liquid index 2ae0b24..1136d0d 100644 --- a/src/default-theme/index.liquid +++ b/src/default-theme/index.liquid @@ -13,8 +13,8 @@

- Published on {{ post.published_on | date: "MMMM d, yyyy" }} - at {{ post.published_on | date: "h:mmtt" | downcase }} + Published on {{ post.published_on | format_date: "MMMM d, yyyy" }} + at {{ post.published_on | format_date: "h:mmtt" | downcase }} by {{ model.authors | value: post.author_id }} {{ post.text }} {%- assign category_count = post.category_ids | size -%} diff --git a/src/default-theme/single-post.liquid b/src/default-theme/single-post.liquid index 089f09c..7fb66d7 100644 --- a/src/default-theme/single-post.liquid +++ b/src/default-theme/single-post.liquid @@ -2,8 +2,8 @@

{{ post.title }}

{% if post.published_on -%} - Published {{ post.published_on | date: "dddd, MMMM d, yyyy" }} - at {{ post.published_on | date: "h:mm tt" | downcase }} + Published {{ post.published_on | format_date: "dddd, MMMM d, yyyy" }} + at {{ post.published_on | format_date: "h:mm tt" | downcase }} {%- else -%} **DRAFT** {% endif %} diff --git a/src/default-theme/version.txt b/src/default-theme/version.txt index d57272f..7b72f03 100644 --- a/src/default-theme/version.txt +++ b/src/default-theme/version.txt @@ -1,2 +1,2 @@ myWebLog Default Theme -2.2 \ No newline at end of file +3 \ No newline at end of file