Complete NodaTime migration (#41)

- Tweak field layout (#38)
This commit is contained in:
Daniel J. Summers 2022-08-08 20:39:19 -04:00
parent e0d2be41d8
commit eb947a48af
8 changed files with 119 additions and 107 deletions

View File

@ -952,7 +952,8 @@ module PrayerRequest =
| Automatic, Expecting -> false
| Automatic, _ ->
// Automatic expiration
asOf.PlusDays -group.Preferences.DaysToExpire > req.UpdatedDate.InZone(SmallGroup.timeZone group).Date
Period.Between(req.UpdatedDate.InZone(SmallGroup.timeZone group).Date, asOf, PeriodUnits.Days).Days
>= group.Preferences.DaysToExpire
/// Is an update required for this long-term request?
let updateRequired asOf group req =

View File

@ -143,6 +143,8 @@ let memberTests =
[<Tests>]
let prayerRequestTests =
let instantNow = SystemClock.Instance.GetCurrentInstant
let localDateNow () = (instantNow ()).InUtc().Date
testList "PrayerRequest" [
test "empty is as expected" {
let mt = PrayerRequest.empty
@ -150,8 +152,8 @@ let prayerRequestTests =
Expect.equal mt.RequestType CurrentRequest "The request type should have been Current"
Expect.equal mt.UserId.Value Guid.Empty "The user ID should have been an empty GUID"
Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID"
Expect.equal mt.EnteredDate DateTime.MinValue "The entered date should have been the minimum"
Expect.equal mt.UpdatedDate DateTime.MinValue "The updated date should have been the minimum"
Expect.equal mt.EnteredDate Instant.MinValue "The entered date should have been the minimum"
Expect.equal mt.UpdatedDate Instant.MinValue "The updated date should have been the minimum"
Expect.isNone mt.Requestor "The requestor should not exist"
Expect.equal mt.Text "" "The request text should have been blank"
Expect.isFalse mt.NotifyChaplain "The notify chaplain flag should not have been set"
@ -159,62 +161,60 @@ let prayerRequestTests =
Expect.equal mt.User.Id.Value Guid.Empty "The user should have been an empty one"
Expect.equal mt.SmallGroup.Id.Value Guid.Empty "The small group should have been an empty one"
}
test "IsExpired always returns false for expecting requests" {
let req = { PrayerRequest.empty with RequestType = Expecting }
Expect.isFalse (req.IsExpired DateTime.Now 0) "An expecting request should never be considered expired"
test "isExpired always returns false for expecting requests" {
PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty
{ PrayerRequest.empty with RequestType = Expecting }
|> Flip.Expect.isFalse "An expecting request should never be considered expired"
}
test "IsExpired always returns false for manually-expired requests" {
let req = { PrayerRequest.empty with UpdatedDate = DateTime.Now.AddMonths -1; Expiration = Manual }
Expect.isFalse (req.IsExpired DateTime.Now 4) "A never-expired request should never be considered expired"
test "isExpired always returns false for manually-expired requests" {
PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty
{ PrayerRequest.empty with UpdatedDate = (instantNow ()) - Duration.FromDays 1; Expiration = Manual }
|> Flip.Expect.isFalse "A never-expired request should never be considered expired"
}
test "IsExpired always returns false for long term/recurring requests" {
let req = { PrayerRequest.empty with RequestType = LongTermRequest }
Expect.isFalse (req.IsExpired DateTime.Now 0)
"A recurring/long-term request should never be considered expired"
test "isExpired always returns false for long term/recurring requests" {
PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty
{ PrayerRequest.empty with RequestType = LongTermRequest }
|> Flip.Expect.isFalse "A recurring/long-term request should never be considered expired"
}
test "IsExpired always returns true for force-expired requests" {
let req = { PrayerRequest.empty with UpdatedDate = DateTime.Now; Expiration = Forced }
Expect.isTrue (req.IsExpired DateTime.Now 5) "A force-expired request should always be considered expired"
test "isExpired always returns true for force-expired requests" {
PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty
{ PrayerRequest.empty with UpdatedDate = (instantNow ()); Expiration = Forced }
|> Flip.Expect.isTrue "A force-expired request should always be considered expired"
}
test "IsExpired returns false for non-expired requests" {
let now = DateTime.Now
let req = { PrayerRequest.empty with UpdatedDate = now.AddDays -5. }
Expect.isFalse (req.IsExpired now 7) "A request updated 5 days ago should not be considered expired"
test "isExpired returns false for non-expired requests" {
let now = instantNow ()
PrayerRequest.isExpired (now.InUtc().Date) SmallGroup.empty
{ PrayerRequest.empty with UpdatedDate = now - Duration.FromDays 5 }
|> Flip.Expect.isFalse "A request updated 5 days ago should not be considered expired"
}
test "IsExpired returns true for expired requests" {
let now = DateTime.Now
let req = { PrayerRequest.empty with UpdatedDate = now.AddDays -8. }
Expect.isTrue (req.IsExpired now 7) "A request updated 8 days ago should be considered expired"
test "isExpired returns true for expired requests" {
let now = instantNow ()
PrayerRequest.isExpired (now.InUtc().Date) SmallGroup.empty
{ PrayerRequest.empty with UpdatedDate = now - Duration.FromDays 15 }
|> Flip.Expect.isTrue "A request updated 15 days ago should be considered expired"
}
test "IsExpired returns true for same-day expired requests" {
let now = DateTime.Now
let req = { PrayerRequest.empty with UpdatedDate = now.Date.AddDays(-7.).AddSeconds -1. }
Expect.isTrue (req.IsExpired now 7)
"A request entered a second before midnight should be considered expired"
test "isExpired returns true for same-day expired requests" {
let now = instantNow ()
PrayerRequest.isExpired (now.InUtc().Date) SmallGroup.empty
{ PrayerRequest.empty with UpdatedDate = now - (Duration.FromDays 14) - (Duration.FromSeconds 1L) }
|> Flip.Expect.isTrue "A request entered a second before midnight should be considered expired"
}
test "UpdateRequired returns false for expired requests" {
let req = { PrayerRequest.empty with Expiration = Forced }
Expect.isFalse (req.UpdateRequired DateTime.Now 7 4) "An expired request should not require an update"
test "updateRequired returns false for expired requests" {
PrayerRequest.updateRequired (localDateNow ()) SmallGroup.empty
{ PrayerRequest.empty with Expiration = Forced }
|> Flip.Expect.isFalse "An expired request should not require an update"
}
test "UpdateRequired returns false when an update is not required for an active request" {
let now = DateTime.Now
let req =
{ PrayerRequest.empty with
RequestType = LongTermRequest
UpdatedDate = now.AddDays -14.
}
Expect.isFalse (req.UpdateRequired now 7 4)
"An active request updated 14 days ago should not require an update until 28 days"
test "updateRequired returns false when an update is not required for an active request" {
let now = instantNow ()
PrayerRequest.updateRequired (localDateNow ()) SmallGroup.empty
{ PrayerRequest.empty with RequestType = LongTermRequest; UpdatedDate = now - Duration.FromDays 14 }
|> Flip.Expect.isFalse "An active request updated 14 days ago should not require an update until 28 days"
}
test "UpdateRequired returns true when an update is required for an active request" {
let now = DateTime.Now
let req =
{ PrayerRequest.empty with
RequestType = LongTermRequest
UpdatedDate = now.AddDays -34.
}
Expect.isTrue (req.UpdateRequired now 7 4)
"An active request updated 34 days ago should require an update (past 28 days)"
let now = instantNow ()
PrayerRequest.updateRequired (localDateNow ()) SmallGroup.empty
{ PrayerRequest.empty with RequestType = LongTermRequest; UpdatedDate = now - Duration.FromDays 34 }
|> Flip.Expect.isTrue "An active request updated 34 days ago should require an update (past 28 days)"
}
]
@ -288,9 +288,9 @@ let requestSortTests =
[<Tests>]
let smallGroupTests =
testList "SmallGroup" [
let now = DateTime (2017, 5, 12, 12, 15, 0, DateTimeKind.Utc)
let now = Instant.FromDateTimeUtc (DateTime (2017, 5, 12, 12, 15, 0, DateTimeKind.Utc))
let withFakeClock f () =
FakeClock (Instant.FromDateTimeUtc now) |> f
FakeClock now |> f
yield test "empty is as expected" {
let mt = SmallGroup.empty
Expect.equal mt.Id.Value Guid.Empty "The small group ID should have been an empty GUID"
@ -311,10 +311,12 @@ let smallGroupTests =
{ SmallGroup.empty with
Preferences = { ListPreferences.empty with TimeZoneId = TimeZoneId "Europe/Berlin" }
}
Expect.isGreaterThan (grp.LocalTimeNow clock) now "UTC to Europe/Berlin should have added hours"
let tz = DateTimeZoneProviders.Tzdb["Europe/Berlin"]
Expect.isGreaterThan (SmallGroup.localTimeNow clock grp) (now.InUtc().LocalDateTime)
"UTC to Europe/Berlin should have added hours"
"LocalTimeNow adjusts the time behind UTC",
fun clock ->
Expect.isLessThan (SmallGroup.empty.LocalTimeNow clock) now
Expect.isLessThan (SmallGroup.localTimeNow clock SmallGroup.empty) (now.InUtc().LocalDateTime)
"UTC to America/Denver should have subtracted hours"
"LocalTimeNow returns UTC when the time zone is invalid",
fun clock ->
@ -322,16 +324,17 @@ let smallGroupTests =
{ SmallGroup.empty with
Preferences = { ListPreferences.empty with TimeZoneId = TimeZoneId "garbage" }
}
Expect.equal (grp.LocalTimeNow clock) now "UTC should have been returned for an invalid time zone"
Expect.equal (SmallGroup.localTimeNow clock grp) (now.InUtc().LocalDateTime)
"UTC should have been returned for an invalid time zone"
]
yield test "LocalTimeNow fails when clock is not passed" {
Expect.throws (fun () -> (SmallGroup.empty.LocalTimeNow >> ignore) null)
yield test "localTimeNow fails when clock is not passed" {
Expect.throws (fun () -> (SmallGroup.localTimeNow null SmallGroup.empty |> ignore))
"Should have raised an exception for null clock"
}
yield test "LocalDateNow returns the date portion" {
let now' = DateTime (2017, 5, 12, 1, 15, 0, DateTimeKind.Utc)
let clock = FakeClock (Instant.FromDateTimeUtc now')
Expect.isLessThan (SmallGroup.empty.LocalDateNow clock) now.Date "The date should have been a day earlier"
let clock = FakeClock (Instant.FromDateTimeUtc (DateTime (2017, 5, 12, 1, 15, 0, DateTimeKind.Utc)))
Expect.isLessThan (SmallGroup.localDateNow clock SmallGroup.empty) (now.InUtc().Date)
"The date should have been a day earlier"
}
]

View File

@ -3,6 +3,7 @@
open System
open Expecto
open Microsoft.AspNetCore.Html
open NodaTime
open PrayerTracker.Entities
open PrayerTracker.Tests.TestLocalization
open PrayerTracker.Utils
@ -121,7 +122,7 @@ let appViewInfoTests =
Expect.isNone vi.HelpLink "The help link should have been set to none"
Expect.isEmpty vi.Messages "There should have been no messages set"
Expect.equal vi.Version "" "The version should have been blank"
Expect.isGreaterThan vi.RequestStart DateTime.MinValue.Ticks "The request start time should have been set"
Expect.equal vi.RequestStart Instant.MinValue "The request start time should have been the minimum value"
Expect.isNone vi.User "There should not have been a user"
Expect.isNone vi.Group "There should not have been a small group"
}
@ -497,26 +498,27 @@ let messageLevelTests =
let requestListTests =
testList "RequestList" [
let withRequestList f () =
let today = SystemClock.Instance.GetCurrentInstant ()
{ Requests = [
{ PrayerRequest.empty with
RequestType = CurrentRequest
Requestor = Some "Zeb"
Text = "zyx"
UpdatedDate = DateTime.Today
UpdatedDate = today
}
{ PrayerRequest.empty with
RequestType = CurrentRequest
Requestor = Some "Aaron"
Text = "abc"
UpdatedDate = DateTime.Today - TimeSpan.FromDays 9.
UpdatedDate = today - Duration.FromDays 9
}
{ PrayerRequest.empty with
RequestType = PraiseReport
Text = "nmo"
UpdatedDate = DateTime.Today
UpdatedDate = today
}
]
Date = DateTime.Today
Date = today.InUtc().Date
SmallGroup = SmallGroup.empty
ShowHeader = false
Recipients = []
@ -574,7 +576,7 @@ let requestListTests =
[ """<div style="text-align:center;font-family:Century Gothic,Tahoma,Luxi Sans,sans-serif">"""
"""<span style="font-size:16pt;"><strong>Prayer Requests</strong></span><br>"""
"""<span style="font-size:12pt;"><strong>Test HTML Group</strong><br>"""
htmlList.Date.ToString "MMMM d, yyyy"
htmlList.Date.ToString ("MMMM d, yyyy", null)
"</span></div><br>"
]
|> String.concat ""
@ -592,7 +594,7 @@ let requestListTests =
}
let html = htmlList.AsHtml _s
let expected =
htmlList.Requests[0].UpdatedDate.ToShortDateString ()
htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("d", null)
|> sprintf """<strong>Zeb</strong> &mdash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>"""
// spot check; if one request has it, they all should
Expect.stringContains html expected "Expected short as-of date not found"
@ -607,7 +609,7 @@ let requestListTests =
}
let html = htmlList.AsHtml _s
let expected =
htmlList.Requests[0].UpdatedDate.ToLongDateString ()
htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("D", null)
|> sprintf """<strong>Zeb</strong> &mdash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>"""
// spot check; if one request has it, they all should
Expect.stringContains html expected "Expected long as-of date not found"
@ -617,7 +619,8 @@ let requestListTests =
let text = textList.AsText _s
Expect.stringContains text $"{textList.SmallGroup.Name}\n" "Small group name not found"
Expect.stringContains text "Prayer Requests\n" "List heading not found"
Expect.stringContains text ((textList.Date.ToString "MMMM d, yyyy") + "\n \n") "List date not found"
Expect.stringContains text ((textList.Date.ToString ("MMMM d, yyyy", null)) + "\n \n")
"List date not found"
Expect.stringContains text "--------------------\n CURRENT REQUESTS\n--------------------\n"
"""Heading for category "Current Requests" not found"""
Expect.stringContains text " + Zeb - zyx\n" "First request not found"
@ -637,7 +640,7 @@ let requestListTests =
}
let text = textList.AsText _s
let expected =
textList.Requests[0].UpdatedDate.ToShortDateString ()
textList.Requests[0].UpdatedDate.InUtc().Date.ToString ("d", null)
|> sprintf " + Zeb - zyx (as of %s)"
// spot check; if one request has it, they all should
Expect.stringContains text expected "Expected short as-of date not found"
@ -652,7 +655,7 @@ let requestListTests =
}
let text = textList.AsText _s
let expected =
textList.Requests[0].UpdatedDate.ToLongDateString ()
textList.Requests[0].UpdatedDate.InUtc().Date.ToString ("D", null)
|> sprintf " + Zeb - zyx (as of %s)"
// spot check; if one request has it, they all should
Expect.stringContains text expected "Expected long as-of date not found"

View File

@ -38,15 +38,13 @@ let edit (model : EditRequest) today ctx viewInfo =
inputField "date" (nameof model.EnteredDate) "" [ _placeholder today ]
]
else
// TODO: do these need to be nested like this?
div [ _inputField ] [
div [ _checkboxField ] [
br []
div [ _checkboxField ] [
inputField "checkbox" (nameof model.SkipDateUpdate) "True" []
label [ _for (nameof model.SkipDateUpdate) ] [ locStr s["Check to not update the date"] ]
br []
small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ]
]
small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ]
]
]
div [ _fieldRow ] [
@ -57,7 +55,7 @@ let edit (model : EditRequest) today ctx viewInfo =
let radioId = String.concat "_" [ nameof model.Expiration; fst exp ]
span [ _class "text-nowrap" ] [
radio (nameof model.Expiration) radioId (fst exp) model.Expiration
label [ _for radioId ] [ locStr (snd exp) ]
label [ _for radioId ] [ space; locStr (snd exp) ]
rawText " &nbsp; &nbsp; "
])
|> div [ _class "pt-center-text" ]

View File

@ -768,7 +768,7 @@ with
/// Is this request new?
member this.IsNew (req : PrayerRequest) =
let reqDate = req.UpdatedDate.InZone(SmallGroup.timeZone this.SmallGroup).Date
Period.Between(this.Date, reqDate, PeriodUnits.Days).Days <= this.SmallGroup.Preferences.DaysToKeepNew
Period.Between(reqDate, this.Date, PeriodUnits.Days).Days <= this.SmallGroup.Preferences.DaysToKeepNew
/// Generate this list as HTML
member this.AsHtml (s : IStringLocalizer) =
@ -797,6 +797,7 @@ with
]
]
]
let tz = SmallGroup.timeZone this.SmallGroup
reqs
|> List.map (fun req ->
let bullet = if this.IsNew req then "circle" else "disc"
@ -804,7 +805,7 @@ with
match req.Requestor with
| Some r when r <> "" ->
strong [] [ str r ]
rawText " &mdash; "
rawText " &ndash; "
| Some _ -> ()
| None -> ()
rawText req.Text
@ -814,8 +815,8 @@ with
| LongDate ->
let dt =
match p.AsOfDateDisplay with
| ShortDate -> req.UpdatedDate.ToString ("d", null)
| LongDate -> req.UpdatedDate.ToString ("D", null)
| ShortDate -> req.UpdatedDate.InZone(tz).Date.ToString ("d", null)
| LongDate -> req.UpdatedDate.InZone(tz).Date.ToString ("D", null)
| _ -> ""
i [ _style $"font-size:%.2f{asOfSize}pt" ] [
rawText "&nbsp; ("; str s["as of"].Value; str " "; str dt; rawText ")"
@ -828,6 +829,7 @@ with
/// Generate this list as plain text
member this.AsText (s : IStringLocalizer) =
let tz = SmallGroup.timeZone this.SmallGroup
seq {
this.SmallGroup.Name
s["Prayer Requests"].Value
@ -846,8 +848,8 @@ with
| _ ->
let dt =
match this.SmallGroup.Preferences.AsOfDateDisplay with
| ShortDate -> req.UpdatedDate.ToString ("d", null)
| LongDate -> req.UpdatedDate.ToString ("D", null)
| ShortDate -> req.UpdatedDate.InZone(tz).Date.ToString ("d", null)
| LongDate -> req.UpdatedDate.InZone(tz).Date.ToString ("D", null)
| _ -> ""
$""" ({s["as of"].Value} {dt})"""
|> sprintf " %s %s%s%s" bullet requestor (htmlToPlainText req.Text)

View File

@ -4,19 +4,26 @@ module PrayerTracker.Extensions
open Microsoft.AspNetCore.Http
open Microsoft.FSharpLu
open Newtonsoft.Json
open NodaTime
open NodaTime.Serialization.JsonNet
open PrayerTracker.Entities
open PrayerTracker.ViewModels
/// JSON.NET serializer settings for NodaTime
let private jsonSettings = JsonSerializerSettings().ConfigureForNodaTime DateTimeZoneProviders.Tzdb
/// Extensions on the .NET session object
type ISession with
/// Set an object in the session
member this.SetObject key value =
this.SetString (key, JsonConvert.SerializeObject value)
this.SetString (key, JsonConvert.SerializeObject (value, jsonSettings))
/// Get an object from the session
member this.GetObject<'T> key =
match this.GetString key with null -> Unchecked.defaultof<'T> | v -> JsonConvert.DeserializeObject<'T> v
match this.GetString key with
| null -> Unchecked.defaultof<'T>
| v -> JsonConvert.DeserializeObject<'T> (v, jsonSettings)
/// The currently logged on small group
member this.CurrentGroup
@ -63,7 +70,6 @@ type ClaimsPrincipal with
open Giraffe
open NodaTime
open PrayerTracker
/// Extensions on the ASP.NET Core HTTP context

View File

@ -27,6 +27,7 @@
<PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" />
<PackageReference Include="NeoSmart.Caching.Sqlite" Version="6.0.1" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
<PackageReference Update="FSharp.Core" Version="6.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="6.0.6" />

View File

@ -238,6 +238,7 @@ footer a:hover {
flex-flow: row wrap;
align-items: center;
justify-content: center;
gap: 2rem;
}
.pt-field {
display: flex;
@ -255,9 +256,6 @@ footer a:hover {
text-transform: uppercase;
color: #777;
}
.pt-field ~ .pt-field {
margin-left: 3rem;
}
.pt-center-text {
text-align: center;
}