v8.4 #53
@ -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>
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
[<EntryPoint>]
|
[<EntryPoint>]
|
||||||
let main argv =
|
let main argv =
|
||||||
runTestsInAssembly defaultConfig argv
|
runTestsInAssemblyWithCLIArgs [] argv
|
||||||
|
@ -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" />
|
||||||
|
@ -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
|
||||||
|
@ -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($"""— %A{s[emptyText]} —""", "")
|
SelectListItem($"""— %A{s[emptyText]} —""", "")
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 }
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user