v8.4 #53

Merged
danieljsummers merged 4 commits from 8point4 into main 2024-06-29 00:22:44 +00:00
14 changed files with 250 additions and 272 deletions
Showing only changes of commit adbbf9cf4e - Show all commits

View File

@ -14,7 +14,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="9.0.4" /> <PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Include="NodaTime.Testing" Version="3.1.11" /> <PackageReference Include="NodaTime.Testing" Version="3.1.11" />
<PackageReference Update="FSharp.Core" Version="8.0.300" /> <PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup> </ItemGroup>

View File

@ -2,4 +2,4 @@
[<EntryPoint>] [<EntryPoint>]
let main argv = let main argv =
runTestsInAssembly defaultConfig argv runTestsInAssemblyWithCLIArgs [] argv

View File

@ -15,7 +15,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.9.12" /> <PackageReference Include="Giraffe.ViewEngine.Htmx" Version="2.0.0" />
<PackageReference Include="MailKit" Version="4.6.0" /> <PackageReference Include="MailKit" Version="4.6.0" />
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />

View File

@ -68,7 +68,7 @@ module Configure =
let cfg = svc.BuildServiceProvider().GetService<IConfiguration>() let cfg = svc.BuildServiceProvider().GetService<IConfiguration>()
let dsb = NpgsqlDataSourceBuilder(cfg.GetConnectionString "PrayerTracker") let dsb = NpgsqlDataSourceBuilder(cfg.GetConnectionString "PrayerTracker")
let _ = dsb.UseNodaTime() let _ = dsb.UseNodaTime()
Configuration.useDataSource (dsb.Build ()) dsb.Build() |> Configuration.useDataSource
let emailCfg = cfg.GetSection "Email" let emailCfg = cfg.GetSection "Email"
if (emailCfg.GetChildren >> Seq.isEmpty >> not) () then ConfigurationBinder.Bind(emailCfg, Email.smtpOptions) if (emailCfg.GetChildren >> Seq.isEmpty >> not) () then ConfigurationBinder.Bind(emailCfg, Email.smtpOptions)
@ -206,12 +206,12 @@ module Configure =
let _ = app.UseMiddleware<RequestStartMiddleware>() let _ = app.UseMiddleware<RequestStartMiddleware>()
let _ = app.UseRouting() let _ = app.UseRouting()
let _ = app.UseSession() let _ = app.UseSession()
let _ = app.UseRequestLocalization let _ = app.UseRequestLocalization(
(app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>().Value) app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>().Value)
let _ = app.UseAuthentication() let _ = app.UseAuthentication()
let _ = app.UseAuthorization() let _ = app.UseAuthorization()
let _ = app.UseEndpoints(fun e -> e.MapGiraffeEndpoints routes) let _ = app.UseEndpoints(fun e -> e.MapGiraffeEndpoints routes)
Views.I18N.setUpFactories <| app.ApplicationServices.GetRequiredService<IStringLocalizerFactory> () app.ApplicationServices.GetRequiredService<IStringLocalizerFactory>() |> Views.I18N.setUpFactories
/// The web application /// The web application

View File

@ -12,8 +12,7 @@ let toSelectList<'T> valFunc textFunc withDefault emptyText (items : 'T seq) =
let s = PrayerTracker.Views.I18N.localizer.Force() let s = PrayerTracker.Views.I18N.localizer.Force()
SelectListItem($"""&mdash; %A{s[emptyText]} &mdash;""", "") SelectListItem($"""&mdash; %A{s[emptyText]} &mdash;""", "")
| _ -> () | _ -> ()
yield! items |> Seq.map (fun x -> SelectListItem (textFunc x, valFunc x)) yield! items |> Seq.map (fun x -> SelectListItem(textFunc x, valFunc x)) ]
]
/// Create a select list from an enumeration /// Create a select list from an enumeration
let toSelectListWithEmpty<'T> valFunc textFunc emptyText (items: 'T seq) = let toSelectListWithEmpty<'T> valFunc textFunc emptyText (items: 'T seq) =
@ -67,8 +66,7 @@ let viewInfo (ctx : HttpContext) =
RequestStart = ctx.Items[Key.startTime] :?> Instant RequestStart = ctx.Items[Key.startTime] :?> Instant
User = ctx.Session.CurrentUser User = ctx.Session.CurrentUser
Group = ctx.Session.CurrentGroup Group = ctx.Session.CurrentGroup
Layout = layout Layout = layout }
}
/// The view is the last parameter, so it can be composed /// The view is the last parameter, so it can be composed
let renderHtml next ctx view = let renderHtml next ctx view =
@ -88,7 +86,7 @@ let fourOhFour (ctx : HttpContext) =
/// Handler to validate CSRF prevention token /// Handler to validate CSRF prevention token
let validateCsrf : HttpHandler = fun next ctx -> task { let validateCsrf : HttpHandler = fun next ctx -> task {
match! (ctx.GetService<Microsoft.AspNetCore.Antiforgery.IAntiforgery> ()).IsRequestValidAsync ctx with match! ctx.GetService<Microsoft.AspNetCore.Antiforgery.IAntiforgery>().IsRequestValidAsync ctx with
| true -> return! next ctx | true -> return! next ctx
| false -> return! (clearResponse >=> setStatusCode 400 >=> text "Quit hacking...") earlyReturn ctx | false -> return! (clearResponse >=> setStatusCode 400 >=> text "Quit hacking...") earlyReturn ctx
} }

View File

@ -27,8 +27,7 @@ type EmailOptions =
PlainTextBody: string PlainTextBody: string
/// Use the current user's preferred language /// Use the current user's preferred language
Strings : IStringLocalizer Strings: IStringLocalizer }
}
/// Options to use when sending e-mail /// Options to use when sending e-mail
type SmtpServerOptions() = type SmtpServerOptions() =
@ -78,8 +77,7 @@ let createHtmlMessage opts =
opts.Strings["Generated by P R A Y E R T R A C K E R"].Value opts.Strings["Generated by P R A Y E R T R A C K E R"].Value
"<br><small>" "<br><small>"
opts.Strings["from Bit Badger Solutions"].Value opts.Strings["from Bit Badger Solutions"].Value
"</small></div></body></html>" "</small></div></body></html>" ]
]
|> String.concat "" |> String.concat ""
let msg = createMessage opts let msg = createMessage opts
msg.Body <- new TextPart(TextFormat.Html, Text = bodyText) msg.Body <- new TextPart(TextFormat.Html, Text = bodyText)
@ -92,8 +90,7 @@ let createTextMessage opts =
"\n\n--\n" "\n\n--\n"
opts.Strings["Generated by P R A Y E R T R A C K E R"].Value opts.Strings["Generated by P R A Y E R T R A C K E R"].Value
"\n" "\n"
opts.Strings["from Bit Badger Solutions"].Value opts.Strings["from Bit Badger Solutions"].Value ]
]
|> String.concat "" |> String.concat ""
let msg = createMessage opts let msg = createMessage opts
msg.Body <- new TextPart(TextFormat.Plain, Text = bodyText) msg.Body <- new TextPart(TextFormat.Plain, Text = bodyText)

View File

@ -27,16 +27,14 @@ let private generateRequestList (ctx : HttpContext) date = task {
Clock = ctx.Clock Clock = ctx.Clock
ListDate = Some listDate ListDate = Some listDate
ActiveOnly = true ActiveOnly = true
PageNumber = 0 PageNumber = 0 }
}
return return
{ Requests = reqs { Requests = reqs
Date = listDate Date = listDate
SmallGroup = group SmallGroup = group
ShowHeader = true ShowHeader = true
CanEmail = Option.isSome ctx.User.UserId CanEmail = Option.isSome ctx.User.UserId
Recipients = [] Recipients = [] }
}
} }
open NodaTime.Text open NodaTime.Text
@ -96,8 +94,7 @@ let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
Subject = s["Prayer Requests for {0} - {1:MMMM d, yyyy}", group.Name, list.Date].Value Subject = s["Prayer Requests for {0} - {1:MMMM d, yyyy}", group.Name, list.Date].Value
HtmlBody = list.AsHtml s HtmlBody = list.AsHtml s
PlainTextBody = list.AsText s PlainTextBody = list.AsText s
Strings = s Strings = s }
}
do! client.DisconnectAsync true do! client.DisconnectAsync true
return! return!
viewInfo ctx viewInfo ctx
@ -137,8 +134,7 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne
Clock = ctx.Clock Clock = ctx.Clock
ListDate = None ListDate = None
ActiveOnly = true ActiveOnly = true
PageNumber = 0 PageNumber = 0 }
}
return! return!
viewInfo ctx viewInfo ctx
|> Views.PrayerRequest.list |> Views.PrayerRequest.list
@ -147,8 +143,7 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne
SmallGroup = group SmallGroup = group
ShowHeader = true ShowHeader = true
CanEmail = Option.isSome ctx.User.UserId CanEmail = Option.isSome ctx.User.UserId
Recipients = [] Recipients = [] }
}
|> renderHtml next ctx |> renderHtml next ctx
| Some _ -> | Some _ ->
addError ctx ctx.Strings["The request list for the group you tried to view is not public."] addError ctx ctx.Strings["The request list for the group you tried to view is not public."]
@ -182,8 +177,7 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
{ MaintainRequests.empty with { MaintainRequests.empty with
Requests = reqs Requests = reqs
SearchTerm = Some search SearchTerm = Some search
PageNbr = Some pageNbr PageNbr = Some pageNbr }
}
| Result.Error _ -> | Result.Error _ ->
let! reqs = let! reqs =
PrayerRequests.forGroup PrayerRequests.forGroup
@ -191,14 +185,12 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
Clock = ctx.Clock Clock = ctx.Clock
ListDate = None ListDate = None
ActiveOnly = onlyActive ActiveOnly = onlyActive
PageNumber = pageNbr PageNumber = pageNbr }
}
return return
{ MaintainRequests.empty with { MaintainRequests.empty with
Requests = reqs Requests = reqs
OnlyActive = Some onlyActive OnlyActive = Some onlyActive
PageNbr = if onlyActive then None else Some pageNbr PageNbr = if onlyActive then None else Some pageNbr }
}
} }
return! return!
{ viewInfo ctx with HelpLink = Some Help.maintainRequests } { viewInfo ctx with HelpLink = Some Help.maintainRequests }

View File

@ -25,7 +25,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BitBadger.AspNetCore.CanonicalDomains" Version="1.0.0" /> <PackageReference Include="BitBadger.AspNetCore.CanonicalDomains" Version="1.0.0" />
<PackageReference Include="Giraffe.Htmx" Version="1.9.12" /> <PackageReference Include="Giraffe.Htmx" Version="2.0.0" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.1.0" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.1.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" /> <PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup> </ItemGroup>

View File

@ -146,8 +146,7 @@ let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
Clock = ctx.Clock Clock = ctx.Clock
ListDate = None ListDate = None
ActiveOnly = true ActiveOnly = true
PageNumber = 0 PageNumber = 0 }
}
let! reqCount = PrayerRequests.countByGroup group.Id let! reqCount = PrayerRequests.countByGroup group.Id
let! mbrCount = Members.countByGroup group.Id let! mbrCount = Members.countByGroup group.Id
let! admins = Users.listByGroupId group.Id let! admins = Users.listByGroupId group.Id
@ -160,11 +159,9 @@ let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|> Seq.ofList |> Seq.ofList
|> Seq.map (fun req -> req.RequestType) |> Seq.map (fun req -> req.RequestType)
|> Seq.distinct |> Seq.distinct
|> Seq.map (fun reqType -> |> Seq.map (fun reqType -> reqType, reqs |> List.filter (fun r -> r.RequestType = reqType) |> List.length)
reqType, reqs |> List.filter (fun r -> r.RequestType = reqType) |> List.length)
|> Map.ofSeq) |> Map.ofSeq)
Admins = admins Admins = admins }
}
return! return!
viewInfo ctx viewInfo ctx
|> Views.SmallGroup.overview model |> Views.SmallGroup.overview model
@ -213,8 +210,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun n
{ mbr with { mbr with
Name = model.Name Name = model.Name
Email = model.Email Email = model.Email
Format = String.noneIfBlank model.Format |> Option.map EmailFormat.fromCode Format = String.noneIfBlank model.Format |> Option.map EmailFormat.fromCode }
}
let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower() let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower()
addInfo ctx ctx.Strings["Successfully {0} group member", act] addInfo ctx ctx.Strings["Successfully {0} group member", act]
return! redirectTo false "/small-group/members" next ctx return! redirectTo false "/small-group/members" next ctx
@ -275,11 +271,10 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
Recipients = recipients Recipients = recipients
Group = group Group = group
Subject = s["Announcement for {0} - {1:MMMM d, yyyy} {2}", group.Name, now.Date, Subject = s["Announcement for {0} - {1:MMMM d, yyyy} {2}", group.Name, now.Date,
(now.ToString ("h:mm tt", null)).ToLower ()].Value now.ToString("h:mm tt", null).ToLower()].Value
HtmlBody = htmlText HtmlBody = htmlText
PlainTextBody = plainText PlainTextBody = plainText
Strings = s Strings = s }
}
do! client.DisconnectAsync true do! client.DisconnectAsync true
// Add to the request list if desired // Add to the request list if desired
match model.SendToClass, model.AddToRequestList with match model.SendToClass, model.AddToRequestList with
@ -296,8 +291,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
RequestType = (Option.get >> PrayerRequestType.fromCode) model.RequestType RequestType = (Option.get >> PrayerRequestType.fromCode) model.RequestType
Text = requestText Text = requestText
EnteredDate = now.Date.AtStartOfDayInZone(zone).ToInstant() EnteredDate = now.Date.AtStartOfDayInZone(zone).ToInstant()
UpdatedDate = now.InZoneLeniently(zone).ToInstant() UpdatedDate = now.InZoneLeniently(zone).ToInstant() }
}
// Tell 'em what they've won, Johnny! // Tell 'em what they've won, Johnny!
let toWhom = let toWhom =
if model.SendToClass = "N" then s["{0} users", s["PrayerTracker"]].Value if model.SendToClass = "N" then s["{0} users", s["PrayerTracker"]].Value

View File

@ -155,11 +155,9 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsr
[ "This is likely due to one of the following reasons:<ul>" [ "This is likely due to one of the following reasons:<ul>"
"<li>The e-mail address “{0}” is invalid.</li>" "<li>The e-mail address “{0}” is invalid.</li>"
"<li>The password entered does not match the password for the given e-mail address.</li>" "<li>The password entered does not match the password for the given e-mail address.</li>"
"<li>You are not authorized to administer the selected group.</li></ul>" "<li>You are not authorized to administer the selected group.</li></ul>" ]
]
|> String.concat "" |> String.concat ""
Some (HtmlString (s[detail, WebUtility.HtmlEncode model.Email].Value)) Some (HtmlString(s[detail, WebUtility.HtmlEncode model.Email].Value)) }
}
|> addUserMessage ctx |> addUserMessage ctx
return! redirectTo false "/user/log-on" next ctx return! redirectTo false "/user/log-on" next ctx
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
@ -235,8 +233,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c
Description = Description =
h s["Please select at least one group for which this user ({0}) is authorized", h s["Please select at least one group for which this user ({0}) is authorized",
updatedUser.Name] updatedUser.Name]
|> Some |> Some }
}
|> addUserMessage ctx |> addUserMessage ctx
return! redirectTo false $"/user/{shortGuid usr.Id.Value}/small-groups" next ctx return! redirectTo false $"/user/{shortGuid usr.Id.Value}/small-groups" next ctx
else else