diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9e16426..d54687e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,9 +1,11 @@ - 7.6.0.0 - 7.6.0.0 + net6.0 + 8.0.0.0 + 8.0.0.0 danieljsummers Bit Badger Solutions - 7.6.0 + 8.0.0 + Embedded diff --git a/src/PrayerTracker.Data/DataAccess.fs b/src/PrayerTracker.Data/DataAccess.fs index 2749d66..95065fb 100644 --- a/src/PrayerTracker.Data/DataAccess.fs +++ b/src/PrayerTracker.Data/DataAccess.fs @@ -20,10 +20,11 @@ module private Helpers = .ThenByDescending (fun req -> req.enteredDate) /// Paginate a prayer request query - let paginate pageNbr pageSize (q : IQueryable) = - q.Skip((pageNbr - 1) * pageSize).Take pageSize + let paginate (pageNbr : int) pageSize (q : IQueryable) = + if pageNbr > 0 then q.Skip((pageNbr - 1) * pageSize).Take pageSize else q +open System open System.Collections.Generic open Microsoft.EntityFrameworkCore open Microsoft.FSharpLu @@ -95,7 +96,7 @@ type AppDbContext with this.PrayerRequests.Where(fun req -> req.smallGroupId = grp.smallGroupId) |> function | q when activeOnly -> - let asOf = theDate.AddDays(-(float grp.preferences.daysToExpire)).Date + let asOf = DateTime (theDate.AddDays(-(float grp.preferences.daysToExpire)).Date.Ticks, DateTimeKind.Utc) q.Where(fun req -> ( req.updatedDate > asOf || req.expiration = Manual diff --git a/src/PrayerTracker.Data/Entities.fs b/src/PrayerTracker.Data/Entities.fs index 4ab03c8..4e64591 100644 --- a/src/PrayerTracker.Data/Entities.fs +++ b/src/PrayerTracker.Data/Entities.fs @@ -642,18 +642,25 @@ with and [] SmallGroup = { /// The Id of this small group smallGroupId : SmallGroupId + /// The church to which this group belongs churchId : ChurchId + /// The name of the group name : string + /// The church to which this small group belongs church : Church + /// The preferences for the request list preferences : ListPreferences + /// The members of the group members : ICollection + /// Prayer requests for this small group prayerRequests : ICollection + /// The users authorized to manage this group users : ICollection } @@ -699,10 +706,13 @@ with and [] TimeZone = { /// The Id for this time zone (uses tzdata names) timeZoneId : TimeZoneId + /// The description of this time zone description : string + /// The order in which this timezone should be displayed sortOrder : int + /// Whether this timezone is active isActive : bool } @@ -731,18 +741,25 @@ with and [] User = { /// The Id of this user userId : UserId + /// The first name of this user firstName : string + /// The last name of this user lastName : string + /// The e-mail address of the user emailAddress : string + /// Whether this user is a PrayerTracker system administrator isAdmin : bool + /// The user's hashed password passwordHash : string + /// The salt for the user's hashed password salt : Guid option + /// The small groups which this user is authorized smallGroups : ICollection } @@ -785,10 +802,13 @@ with and [] UserSmallGroup = { /// The Id of the user who has access to the small group userId : UserId + /// The Id of the small group to which the user has access smallGroupId : SmallGroupId + /// The user who has access to the small group user : User + /// The small group to which the user has access smallGroup : SmallGroup } diff --git a/src/PrayerTracker.Data/PrayerTracker.Data.fsproj b/src/PrayerTracker.Data/PrayerTracker.Data.fsproj index b7eeb49..3566319 100644 --- a/src/PrayerTracker.Data/PrayerTracker.Data.fsproj +++ b/src/PrayerTracker.Data/PrayerTracker.Data.fsproj @@ -15,8 +15,9 @@ - - + + + diff --git a/src/PrayerTracker.Tests/Data/EntitiesTests.fs b/src/PrayerTracker.Tests/Data/EntitiesTests.fs index 28f6185..b511878 100644 --- a/src/PrayerTracker.Tests/Data/EntitiesTests.fs +++ b/src/PrayerTracker.Tests/Data/EntitiesTests.fs @@ -7,356 +7,369 @@ open System [] let asOfDateDisplayTests = - testList "AsOfDateDisplay" [ - test "NoDisplay code is correct" { - Expect.equal NoDisplay.code "N" "The code for NoDisplay should have been \"N\"" - } - test "ShortDate code is correct" { - Expect.equal ShortDate.code "S" "The code for ShortDate should have been \"S\"" - } - test "LongDate code is correct" { - Expect.equal LongDate.code "L" "The code for LongDate should have been \"N\"" - } - test "fromCode N should return NoDisplay" { - Expect.equal (AsOfDateDisplay.fromCode "N") NoDisplay "\"N\" should have been converted to NoDisplay" - } - test "fromCode S should return ShortDate" { - Expect.equal (AsOfDateDisplay.fromCode "S") ShortDate "\"S\" should have been converted to ShortDate" - } - test "fromCode L should return LongDate" { - Expect.equal (AsOfDateDisplay.fromCode "L") LongDate "\"L\" should have been converted to LongDate" - } - test "fromCode X should raise" { - Expect.throws (fun () -> AsOfDateDisplay.fromCode "X" |> ignore) - "An unknown code should have raised an exception" - } + testList "AsOfDateDisplay" [ + test "NoDisplay code is correct" { + Expect.equal NoDisplay.code "N" "The code for NoDisplay should have been \"N\"" + } + test "ShortDate code is correct" { + Expect.equal ShortDate.code "S" "The code for ShortDate should have been \"S\"" + } + test "LongDate code is correct" { + Expect.equal LongDate.code "L" "The code for LongDate should have been \"N\"" + } + test "fromCode N should return NoDisplay" { + Expect.equal (AsOfDateDisplay.fromCode "N") NoDisplay "\"N\" should have been converted to NoDisplay" + } + test "fromCode S should return ShortDate" { + Expect.equal (AsOfDateDisplay.fromCode "S") ShortDate "\"S\" should have been converted to ShortDate" + } + test "fromCode L should return LongDate" { + Expect.equal (AsOfDateDisplay.fromCode "L") LongDate "\"L\" should have been converted to LongDate" + } + test "fromCode X should raise" { + Expect.throws (fun () -> AsOfDateDisplay.fromCode "X" |> ignore) + "An unknown code should have raised an exception" + } ] [] let churchTests = - testList "Church" [ - test "empty is as expected" { - let mt = Church.empty - Expect.equal mt.churchId Guid.Empty "The church ID should have been an empty GUID" - Expect.equal mt.name "" "The name should have been blank" - Expect.equal mt.city "" "The city should have been blank" - Expect.equal mt.st "" "The state should have been blank" - Expect.isFalse mt.hasInterface "The church should not show that it has an interface" - Expect.isNone mt.interfaceAddress "The interface address should not exist" - Expect.isNotNull mt.smallGroups "The small groups navigation property should not be null" - Expect.isEmpty mt.smallGroups "There should be no small groups for an empty church" - } + testList "Church" [ + test "empty is as expected" { + let mt = Church.empty + Expect.equal mt.churchId Guid.Empty "The church ID should have been an empty GUID" + Expect.equal mt.name "" "The name should have been blank" + Expect.equal mt.city "" "The city should have been blank" + Expect.equal mt.st "" "The state should have been blank" + Expect.isFalse mt.hasInterface "The church should not show that it has an interface" + Expect.isNone mt.interfaceAddress "The interface address should not exist" + Expect.isNotNull mt.smallGroups "The small groups navigation property should not be null" + Expect.isEmpty mt.smallGroups "There should be no small groups for an empty church" + } ] [] let emailFormatTests = - testList "EmailFormat" [ - test "HtmlFormat code is correct" { - Expect.equal HtmlFormat.code "H" "The code for HtmlFormat should have been \"H\"" - } - test "PlainTextFormat code is correct" { - Expect.equal PlainTextFormat.code "P" "The code for PlainTextFormat should have been \"P\"" - } - test "fromCode H should return HtmlFormat" { - Expect.equal (EmailFormat.fromCode "H") HtmlFormat "\"H\" should have been converted to HtmlFormat" - } - test "fromCode P should return ShortDate" { - Expect.equal (EmailFormat.fromCode "P") PlainTextFormat "\"P\" should have been converted to PlainTextFormat" - } - test "fromCode Z should raise" { - Expect.throws (fun () -> EmailFormat.fromCode "Z" |> ignore) "An unknown code should have raised an exception" - } + testList "EmailFormat" [ + test "HtmlFormat code is correct" { + Expect.equal HtmlFormat.code "H" "The code for HtmlFormat should have been \"H\"" + } + test "PlainTextFormat code is correct" { + Expect.equal PlainTextFormat.code "P" "The code for PlainTextFormat should have been \"P\"" + } + test "fromCode H should return HtmlFormat" { + Expect.equal (EmailFormat.fromCode "H") HtmlFormat "\"H\" should have been converted to HtmlFormat" + } + test "fromCode P should return ShortDate" { + Expect.equal (EmailFormat.fromCode "P") PlainTextFormat + "\"P\" should have been converted to PlainTextFormat" + } + test "fromCode Z should raise" { + Expect.throws (fun () -> EmailFormat.fromCode "Z" |> ignore) + "An unknown code should have raised an exception" + } ] [] let expirationTests = - testList "Expiration" [ - test "Automatic code is correct" { - Expect.equal Automatic.code "A" "The code for Automatic should have been \"A\"" - } - test "Manual code is correct" { - Expect.equal Manual.code "M" "The code for Manual should have been \"M\"" - } - test "Forced code is correct" { - Expect.equal Forced.code "F" "The code for Forced should have been \"F\"" - } - test "fromCode A should return Automatic" { - Expect.equal (Expiration.fromCode "A") Automatic "\"A\" should have been converted to Automatic" - } - test "fromCode M should return Manual" { - Expect.equal (Expiration.fromCode "M") Manual "\"M\" should have been converted to Manual" - } - test "fromCode F should return Forced" { - Expect.equal (Expiration.fromCode "F") Forced "\"F\" should have been converted to Forced" - } - test "fromCode V should raise" { - Expect.throws (fun () -> Expiration.fromCode "V" |> ignore) "An unknown code should have raised an exception" - } + testList "Expiration" [ + test "Automatic code is correct" { + Expect.equal Automatic.code "A" "The code for Automatic should have been \"A\"" + } + test "Manual code is correct" { + Expect.equal Manual.code "M" "The code for Manual should have been \"M\"" + } + test "Forced code is correct" { + Expect.equal Forced.code "F" "The code for Forced should have been \"F\"" + } + test "fromCode A should return Automatic" { + Expect.equal (Expiration.fromCode "A") Automatic "\"A\" should have been converted to Automatic" + } + test "fromCode M should return Manual" { + Expect.equal (Expiration.fromCode "M") Manual "\"M\" should have been converted to Manual" + } + test "fromCode F should return Forced" { + Expect.equal (Expiration.fromCode "F") Forced "\"F\" should have been converted to Forced" + } + test "fromCode V should raise" { + Expect.throws (fun () -> Expiration.fromCode "V" |> ignore) + "An unknown code should have raised an exception" + } ] [] let listPreferencesTests = - testList "ListPreferences" [ - test "empty is as expected" { - let mt = ListPreferences.empty - Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" - Expect.equal mt.daysToExpire 14 "The default days to expire should have been 14" - Expect.equal mt.daysToKeepNew 7 "The default days to keep new should have been 7" - Expect.equal mt.longTermUpdateWeeks 4 "The default long term update weeks should have been 4" - Expect.equal mt.emailFromName "PrayerTracker" "The default e-mail from name should have been PrayerTracker" - Expect.equal mt.emailFromAddress "prayer@djs-consulting.com" - "The default e-mail from address should have been prayer@djs-consulting.com" - Expect.equal mt.listFonts "Century Gothic,Tahoma,Luxi Sans,sans-serif" "The default list fonts were incorrect" - Expect.equal mt.headingColor "maroon" "The default heading text color should have been maroon" - Expect.equal mt.lineColor "navy" "The default heding line color should have been navy" - Expect.equal mt.headingFontSize 16 "The default heading font size should have been 16" - Expect.equal mt.textFontSize 12 "The default text font size should have been 12" - Expect.equal mt.requestSort SortByDate "The default request sort should have been by date" - Expect.equal mt.groupPassword "" "The default group password should have been blank" - Expect.equal mt.defaultEmailType HtmlFormat "The default e-mail type should have been HTML" - Expect.isFalse mt.isPublic "The isPublic flag should not have been set" - Expect.equal mt.timeZoneId "America/Denver" "The default time zone should have been America/Denver" - Expect.equal mt.timeZone.timeZoneId "" "The default preferences should have included an empty time zone" - Expect.equal mt.pageSize 100 "The default page size should have been 100" - Expect.equal mt.asOfDateDisplay NoDisplay "The as-of date display should have been No Display" - } + testList "ListPreferences" [ + test "empty is as expected" { + let mt = ListPreferences.empty + Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" + Expect.equal mt.daysToExpire 14 "The default days to expire should have been 14" + Expect.equal mt.daysToKeepNew 7 "The default days to keep new should have been 7" + Expect.equal mt.longTermUpdateWeeks 4 "The default long term update weeks should have been 4" + Expect.equal mt.emailFromName "PrayerTracker" "The default e-mail from name should have been PrayerTracker" + Expect.equal mt.emailFromAddress "prayer@djs-consulting.com" + "The default e-mail from address should have been prayer@djs-consulting.com" + Expect.equal mt.listFonts "Century Gothic,Tahoma,Luxi Sans,sans-serif" + "The default list fonts were incorrect" + Expect.equal mt.headingColor "maroon" "The default heading text color should have been maroon" + Expect.equal mt.lineColor "navy" "The default heding line color should have been navy" + Expect.equal mt.headingFontSize 16 "The default heading font size should have been 16" + Expect.equal mt.textFontSize 12 "The default text font size should have been 12" + Expect.equal mt.requestSort SortByDate "The default request sort should have been by date" + Expect.equal mt.groupPassword "" "The default group password should have been blank" + Expect.equal mt.defaultEmailType HtmlFormat "The default e-mail type should have been HTML" + Expect.isFalse mt.isPublic "The isPublic flag should not have been set" + Expect.equal mt.timeZoneId "America/Denver" "The default time zone should have been America/Denver" + Expect.equal mt.timeZone.timeZoneId "" "The default preferences should have included an empty time zone" + Expect.equal mt.pageSize 100 "The default page size should have been 100" + Expect.equal mt.asOfDateDisplay NoDisplay "The as-of date display should have been No Display" + } ] [] let memberTests = - testList "Member" [ - test "empty is as expected" { - let mt = Member.empty - Expect.equal mt.memberId Guid.Empty "The member ID should have been an empty GUID" - Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" - Expect.equal mt.memberName "" "The member name should have been blank" - Expect.equal mt.email "" "The member e-mail address should have been blank" - Expect.isNone mt.format "The preferred e-mail format should not exist" - Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one" - } + testList "Member" [ + test "empty is as expected" { + let mt = Member.empty + Expect.equal mt.memberId Guid.Empty "The member ID should have been an empty GUID" + Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" + Expect.equal mt.memberName "" "The member name should have been blank" + Expect.equal mt.email "" "The member e-mail address should have been blank" + Expect.isNone mt.format "The preferred e-mail format should not exist" + Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one" + } ] [] let prayerRequestTests = - testList "PrayerRequest" [ - test "empty is as expected" { - let mt = PrayerRequest.empty - Expect.equal mt.prayerRequestId Guid.Empty "The request ID should have been an empty GUID" - Expect.equal mt.requestType CurrentRequest "The request type should have been Current" - Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" - Expect.equal mt.smallGroupId 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.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" - Expect.equal mt.expiration Automatic "The expiration should have been Automatic" - Expect.equal mt.user.userId Guid.Empty "The user should have been an empty one" - Expect.equal mt.smallGroup.smallGroupId 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 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 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 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 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 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 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 "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 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 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)" - } + testList "PrayerRequest" [ + test "empty is as expected" { + let mt = PrayerRequest.empty + Expect.equal mt.prayerRequestId Guid.Empty "The request ID should have been an empty GUID" + Expect.equal mt.requestType CurrentRequest "The request type should have been Current" + Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" + Expect.equal mt.smallGroupId 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.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" + Expect.equal mt.expiration Automatic "The expiration should have been Automatic" + Expect.equal mt.user.userId Guid.Empty "The user should have been an empty one" + Expect.equal mt.smallGroup.smallGroupId 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 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 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 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 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 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 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 "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 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 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 prayerRequestTypeTests = - testList "PrayerRequestType" [ - test "CurrentRequest code is correct" { - Expect.equal CurrentRequest.code "C" "The code for CurrentRequest should have been \"C\"" - } - test "LongTermRequest code is correct" { - Expect.equal LongTermRequest.code "L" "The code for LongTermRequest should have been \"L\"" - } - test "PraiseReport code is correct" { - Expect.equal PraiseReport.code "P" "The code for PraiseReport should have been \"P\"" - } - test "Expecting code is correct" { - Expect.equal Expecting.code "E" "The code for Expecting should have been \"E\"" - } - test "Announcement code is correct" { - Expect.equal Announcement.code "A" "The code for Announcement should have been \"A\"" - } - test "fromCode C should return CurrentRequest" { - Expect.equal (PrayerRequestType.fromCode "C") CurrentRequest - "\"C\" should have been converted to CurrentRequest" - } - test "fromCode L should return LongTermRequest" { - Expect.equal (PrayerRequestType.fromCode "L") LongTermRequest - "\"L\" should have been converted to LongTermRequest" - } - test "fromCode P should return PraiseReport" { - Expect.equal (PrayerRequestType.fromCode "P") PraiseReport "\"P\" should have been converted to PraiseReport" - } - test "fromCode E should return Expecting" { - Expect.equal (PrayerRequestType.fromCode "E") Expecting "\"E\" should have been converted to Expecting" - } - test "fromCode A should return Announcement" { - Expect.equal (PrayerRequestType.fromCode "A") Announcement "\"A\" should have been converted to Announcement" - } - test "fromCode R should raise" { - Expect.throws (fun () -> PrayerRequestType.fromCode "R" |> ignore) - "An unknown code should have raised an exception" - } + testList "PrayerRequestType" [ + test "CurrentRequest code is correct" { + Expect.equal CurrentRequest.code "C" "The code for CurrentRequest should have been \"C\"" + } + test "LongTermRequest code is correct" { + Expect.equal LongTermRequest.code "L" "The code for LongTermRequest should have been \"L\"" + } + test "PraiseReport code is correct" { + Expect.equal PraiseReport.code "P" "The code for PraiseReport should have been \"P\"" + } + test "Expecting code is correct" { + Expect.equal Expecting.code "E" "The code for Expecting should have been \"E\"" + } + test "Announcement code is correct" { + Expect.equal Announcement.code "A" "The code for Announcement should have been \"A\"" + } + test "fromCode C should return CurrentRequest" { + Expect.equal (PrayerRequestType.fromCode "C") CurrentRequest + "\"C\" should have been converted to CurrentRequest" + } + test "fromCode L should return LongTermRequest" { + Expect.equal (PrayerRequestType.fromCode "L") LongTermRequest + "\"L\" should have been converted to LongTermRequest" + } + test "fromCode P should return PraiseReport" { + Expect.equal (PrayerRequestType.fromCode "P") PraiseReport + "\"P\" should have been converted to PraiseReport" + } + test "fromCode E should return Expecting" { + Expect.equal (PrayerRequestType.fromCode "E") Expecting "\"E\" should have been converted to Expecting" + } + test "fromCode A should return Announcement" { + Expect.equal (PrayerRequestType.fromCode "A") Announcement + "\"A\" should have been converted to Announcement" + } + test "fromCode R should raise" { + Expect.throws (fun () -> PrayerRequestType.fromCode "R" |> ignore) + "An unknown code should have raised an exception" + } ] [] let requestSortTests = - testList "RequestSort" [ - test "SortByDate code is correct" { - Expect.equal SortByDate.code "D" "The code for SortByDate should have been \"D\"" - } - test "SortByRequestor code is correct" { - Expect.equal SortByRequestor.code "R" "The code for SortByRequestor should have been \"R\"" - } - test "fromCode D should return SortByDate" { - Expect.equal (RequestSort.fromCode "D") SortByDate "\"D\" should have been converted to SortByDate" - } - test "fromCode R should return SortByRequestor" { - Expect.equal (RequestSort.fromCode "R") SortByRequestor "\"R\" should have been converted to SortByRequestor" - } - test "fromCode Q should raise" { - Expect.throws (fun () -> RequestSort.fromCode "Q" |> ignore) "An unknown code should have raised an exception" - } + testList "RequestSort" [ + test "SortByDate code is correct" { + Expect.equal SortByDate.code "D" "The code for SortByDate should have been \"D\"" + } + test "SortByRequestor code is correct" { + Expect.equal SortByRequestor.code "R" "The code for SortByRequestor should have been \"R\"" + } + test "fromCode D should return SortByDate" { + Expect.equal (RequestSort.fromCode "D") SortByDate "\"D\" should have been converted to SortByDate" + } + test "fromCode R should return SortByRequestor" { + Expect.equal (RequestSort.fromCode "R") SortByRequestor + "\"R\" should have been converted to SortByRequestor" + } + test "fromCode Q should raise" { + Expect.throws (fun () -> RequestSort.fromCode "Q" |> ignore) + "An unknown code should have raised an exception" + } ] [] let smallGroupTests = - testList "SmallGroup" [ - let now = DateTime (2017, 5, 12, 12, 15, 0, DateTimeKind.Utc) - let withFakeClock f () = - FakeClock (Instant.FromDateTimeUtc now) |> f - yield test "empty is as expected" { - let mt = SmallGroup.empty - Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" - Expect.equal mt.churchId Guid.Empty "The church ID should have been an empty GUID" - Expect.equal mt.name "" "The name should have been blank" - Expect.equal mt.church.churchId Guid.Empty "The church should have been an empty one" - Expect.isNotNull mt.members "The members navigation property should not be null" - Expect.isEmpty mt.members "There should be no members for an empty small group" - Expect.isNotNull mt.prayerRequests "The prayer requests navigation property should not be null" - Expect.isEmpty mt.prayerRequests "There should be no prayer requests for an empty small group" - Expect.isNotNull mt.users "The users navigation property should not be null" - Expect.isEmpty mt.users "There should be no users for an empty small group" - } - yield! testFixture withFakeClock [ - "localTimeNow adjusts the time ahead of UTC", - fun clock -> - let grp = { SmallGroup.empty with preferences = { ListPreferences.empty with timeZoneId = "Europe/Berlin" } } - Expect.isGreaterThan (grp.localTimeNow clock) now "UTC to Europe/Berlin should have added hours" - "localTimeNow adjusts the time behind UTC", - fun clock -> - Expect.isLessThan (SmallGroup.empty.localTimeNow clock) now - "UTC to America/Denver should have subtracted hours" - "localTimeNow returns UTC when the time zone is invalid", - fun clock -> - let grp = { SmallGroup.empty with preferences = { ListPreferences.empty with timeZoneId = "garbage" } } - Expect.equal (grp.localTimeNow clock) now "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) - "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" - } + testList "SmallGroup" [ + let now = DateTime (2017, 5, 12, 12, 15, 0, DateTimeKind.Utc) + let withFakeClock f () = + FakeClock (Instant.FromDateTimeUtc now) |> f + yield test "empty is as expected" { + let mt = SmallGroup.empty + Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" + Expect.equal mt.churchId Guid.Empty "The church ID should have been an empty GUID" + Expect.equal mt.name "" "The name should have been blank" + Expect.equal mt.church.churchId Guid.Empty "The church should have been an empty one" + Expect.isNotNull mt.members "The members navigation property should not be null" + Expect.isEmpty mt.members "There should be no members for an empty small group" + Expect.isNotNull mt.prayerRequests "The prayer requests navigation property should not be null" + Expect.isEmpty mt.prayerRequests "There should be no prayer requests for an empty small group" + Expect.isNotNull mt.users "The users navigation property should not be null" + Expect.isEmpty mt.users "There should be no users for an empty small group" + } + yield! testFixture withFakeClock [ + "localTimeNow adjusts the time ahead of UTC", + fun clock -> + let grp = + { SmallGroup.empty with + preferences = { ListPreferences.empty with timeZoneId = "Europe/Berlin" } + } + Expect.isGreaterThan (grp.localTimeNow clock) now "UTC to Europe/Berlin should have added hours" + "localTimeNow adjusts the time behind UTC", + fun clock -> + Expect.isLessThan (SmallGroup.empty.localTimeNow clock) now + "UTC to America/Denver should have subtracted hours" + "localTimeNow returns UTC when the time zone is invalid", + fun clock -> + let grp = { SmallGroup.empty with preferences = { ListPreferences.empty with timeZoneId = "garbage" } } + Expect.equal (grp.localTimeNow clock) now "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) + "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 timeZoneTests = - testList "TimeZone" [ - test "empty is as expected" { - let mt = TimeZone.empty - Expect.equal mt.timeZoneId "" "The time zone ID should have been blank" - Expect.equal mt.description "" "The description should have been blank" - Expect.equal mt.sortOrder 0 "The sort order should have been zero" - Expect.isFalse mt.isActive "The is-active flag should not have been set" - } + testList "TimeZone" [ + test "empty is as expected" { + let mt = TimeZone.empty + Expect.equal mt.timeZoneId "" "The time zone ID should have been blank" + Expect.equal mt.description "" "The description should have been blank" + Expect.equal mt.sortOrder 0 "The sort order should have been zero" + Expect.isFalse mt.isActive "The is-active flag should not have been set" + } ] [] let userTests = - testList "User" [ - test "empty is as expected" { - let mt = User.empty - Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" - Expect.equal mt.firstName "" "The first name should have been blank" - Expect.equal mt.lastName "" "The last name should have been blank" - Expect.equal mt.emailAddress "" "The e-mail address should have been blank" - Expect.isFalse mt.isAdmin "The is admin flag should not have been set" - Expect.equal mt.passwordHash "" "The password hash should have been blank" - Expect.isNone mt.salt "The password salt should not exist" - Expect.isNotNull mt.smallGroups "The small groups navigation property should not have been null" - Expect.isEmpty mt.smallGroups "There should be no small groups for an empty user" - } - test "fullName concatenates first and last names" { - let user = { User.empty with firstName = "Unit"; lastName = "Test" } - Expect.equal user.fullName "Unit Test" "The full name should be the first and last, separated by a space" - } + testList "User" [ + test "empty is as expected" { + let mt = User.empty + Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" + Expect.equal mt.firstName "" "The first name should have been blank" + Expect.equal mt.lastName "" "The last name should have been blank" + Expect.equal mt.emailAddress "" "The e-mail address should have been blank" + Expect.isFalse mt.isAdmin "The is admin flag should not have been set" + Expect.equal mt.passwordHash "" "The password hash should have been blank" + Expect.isNone mt.salt "The password salt should not exist" + Expect.isNotNull mt.smallGroups "The small groups navigation property should not have been null" + Expect.isEmpty mt.smallGroups "There should be no small groups for an empty user" + } + test "fullName concatenates first and last names" { + let user = { User.empty with firstName = "Unit"; lastName = "Test" } + Expect.equal user.fullName "Unit Test" "The full name should be the first and last, separated by a space" + } ] [] let userSmallGroupTests = - testList "UserSmallGroup" [ - test "empty is as expected" { - let mt = UserSmallGroup.empty - Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" - Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" - Expect.equal mt.user.userId Guid.Empty "The user should have been an empty one" - Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one" - } + testList "UserSmallGroup" [ + test "empty is as expected" { + let mt = UserSmallGroup.empty + Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" + Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" + Expect.equal mt.user.userId Guid.Empty "The user should have been an empty one" + Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one" + } ] diff --git a/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj b/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj index 14f1306..fe2953b 100644 --- a/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj +++ b/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj @@ -17,7 +17,8 @@ - + + diff --git a/src/PrayerTracker.Tests/UI/CommonFunctionsTests.fs b/src/PrayerTracker.Tests/UI/CommonFunctionsTests.fs index 437feed..f858ce1 100644 --- a/src/PrayerTracker.Tests/UI/CommonFunctionsTests.fs +++ b/src/PrayerTracker.Tests/UI/CommonFunctionsTests.fs @@ -1,208 +1,214 @@ module PrayerTracker.UI.CommonFunctionsTests +open System.IO open Expecto open Giraffe.ViewEngine open Microsoft.AspNetCore.Mvc.Localization open Microsoft.Extensions.Localization open PrayerTracker.Tests.TestLocalization open PrayerTracker.Views -open System.IO [] let iconSizedTests = - testList "iconSized" [ - test "succeeds" { - let ico = iconSized 18 "tom-&-jerry" |> renderHtmlNode - Expect.equal ico "tom-&-jerry" "icon HTML not correct" - } + testList "iconSized" [ + test "succeeds" { + let ico = iconSized 18 "tom-&-jerry" |> renderHtmlNode + Expect.equal ico """tom-&-jerry""" "icon HTML not correct" + } ] [] let iconTests = - testList "icon" [ - test "succeeds" { - let ico = icon "bob-&-tom" |> renderHtmlNode - Expect.equal ico "bob-&-tom" "icon HTML not correct" - } + testList "icon" [ + test "succeeds" { + let ico = icon "bob-&-tom" |> renderHtmlNode + Expect.equal ico """bob-&-tom""" "icon HTML not correct" + } ] [] let locStrTests = - testList "locStr" [ - test "succeeds" { - let enc = locStr (LocalizedString ("test", "test&")) |> renderHtmlNode - Expect.equal enc "test&" "string not encoded correctly" - } + testList "locStr" [ + test "succeeds" { + let enc = locStr (LocalizedString ("test", "test&")) |> renderHtmlNode + Expect.equal enc "test&" "string not encoded correctly" + } ] [] let namedColorListTests = - testList "namedColorList" [ - test "succeeds with default values" { - let expected = - [ "" - ] - |> String.concat "" - let selectList = namedColorList "the-name" "" [] _s |> renderHtmlNode - Expect.equal expected selectList "The default select list was not generated correctly" - } - test "succeeds with a selected value" { - let selectList = namedColorList "the-name" "white" [] _s |> renderHtmlNode - Expect.stringContains selectList " selected>white" "Selected option not generated correctly" - } - test "succeeds with extra attributes" { - let selectList = namedColorList "the-name" "" [ _id "myId" ] _s |> renderHtmlNode - Expect.stringStarts selectList """" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + """""" + "" + ] + |> String.concat "" + let selectList = namedColorList "the-name" "" [] _s |> renderHtmlNode + Expect.equal expected selectList "The default select list was not generated correctly" + } + test "succeeds with a selected value" { + let selectList = namedColorList "the-name" "white" [] _s |> renderHtmlNode + Expect.stringContains selectList " selected>white" "Selected option not generated correctly" + } + test "succeeds with extra attributes" { + let selectList = namedColorList "the-name" "" [ _id "myId" ] _s |> renderHtmlNode + Expect.stringStarts selectList """" - "Unselected radio button not generated correctly" - } - test "succeeds when selected" { - let rad = radio "a-name" "anId" "unit" "unit" |> renderHtmlNode - Expect.equal rad "" - "Selected radio button not generated correctly" - } + testList "radio" [ + test "succeeds when not selected" { + let rad = radio "a-name" "anId" "test" "unit" |> renderHtmlNode + Expect.equal rad """""" + "Unselected radio button not generated correctly" + } + test "succeeds when selected" { + let rad = radio "a-name" "anId" "unit" "unit" |> renderHtmlNode + Expect.equal rad """""" + "Selected radio button not generated correctly" + } ] [] let rawLocTextTests = - testList "rawLocText" [ - test "succeeds" { - use sw = new StringWriter () - let raw = rawLocText sw (LocalizedHtmlString ("test", "test&")) |> renderHtmlNode - Expect.equal raw "test&" "string not written correctly" - } + testList "rawLocText" [ + test "succeeds" { + use sw = new StringWriter () + let raw = rawLocText sw (LocalizedHtmlString ("test", "test&")) |> renderHtmlNode + Expect.equal raw "test&" "string not written correctly" + } ] [] let selectDefaultTests = - testList "selectDefault" [ - test "succeeds" { - Expect.equal (selectDefault "a&b") "— a&b —" "Default selection not generated correctly" - } + testList "selectDefault" [ + test "succeeds" { + Expect.equal (selectDefault "a&b") "— a&b —" "Default selection not generated correctly" + } ] [] let selectListTests = - testList "selectList" [ - test "succeeds with minimum options" { - let theList = selectList "a-list" "" [] [] |> renderHtmlNode - Expect.equal theList "" "Empty select list not generated correctly" - } - test "succeeds with all options" { - let theList = - [ "tom", "Tom&" - "bob", "Bob" - "jan", "Jan" - ] - |> selectList "the-list" "bob" [ _style "ugly" ] - |> renderHtmlNode - let expected = - [ "" - ] - |> String.concat "" - Expect.equal theList expected "Filled select list not generated correctly" - } + testList "selectList" [ + test "succeeds with minimum options" { + let theList = selectList "a-list" "" [] [] |> renderHtmlNode + Expect.equal theList """""" + "Empty select list not generated correctly" + } + test "succeeds with all options" { + let theList = + [ "tom", "Tom&" + "bob", "Bob" + "jan", "Jan" + ] + |> selectList "the-list" "bob" [ _style "ugly" ] + |> renderHtmlNode + let expected = + [ """""" + ] + |> String.concat "" + Expect.equal theList expected "Filled select list not generated correctly" + } ] [] let spaceTests = - testList "space" [ - test "succeeds" { - Expect.equal (renderHtmlNode space) " " "space literal not correct" - } + testList "space" [ + test "succeeds" { + Expect.equal (renderHtmlNode space) " " "space literal not correct" + } ] [] let submitTests = - testList "submit" [ - test "succeeds" { - let btn = submit [ _class "slick" ] "file-ico" _s.["a&b"] |> renderHtmlNode - Expect.equal - btn - "" - "Submit button not generated correctly" - } + testList "submit" [ + test "succeeds" { + let btn = submit [ _class "slick" ] "file-ico" _s["a&b"] |> renderHtmlNode + Expect.equal + btn + """""" + "Submit button not generated correctly" + } ] [] let tableSummaryTests = - testList "tableSummary" [ - test "succeeds for no entries" { - let sum = tableSummary 0 _s |> renderHtmlNode - Expect.equal sum "
No Entries to Display
" - "Summary for no items is incorrect" - } - test "succeeds for one entry" { - let sum = tableSummary 1 _s |> renderHtmlNode - Expect.equal sum "
Displaying 1 Entry
" - "Summary for one item is incorrect" - } - test "succeeds for many entries" { - let sum = tableSummary 5 _s |> renderHtmlNode - Expect.equal sum "
Displaying 5 Entries
" - "Summary for many items is incorrect" - } + testList "tableSummary" [ + test "succeeds for no entries" { + let sum = tableSummary 0 _s |> renderHtmlNode + Expect.equal sum """
No Entries to Display
""" + "Summary for no items is incorrect" + } + test "succeeds for one entry" { + let sum = tableSummary 1 _s |> renderHtmlNode + Expect.equal sum """
Displaying 1 Entry
""" + "Summary for one item is incorrect" + } + test "succeeds for many entries" { + let sum = tableSummary 5 _s |> renderHtmlNode + Expect.equal sum """
Displaying 5 Entries
""" + "Summary for many items is incorrect" + } ] module TimeZones = - open PrayerTracker.Views.CommonFunctions.TimeZones + open PrayerTracker.Views.CommonFunctions.TimeZones - [] - let nameTests = - testList "TimeZones.name" [ - test "succeeds for US Eastern time" { - Expect.equal (name "America/New_York" _s |> string) "Eastern" "US Eastern time zone not returned correctly" - } - test "succeeds for US Central time" { - Expect.equal (name "America/Chicago" _s |> string) "Central" "US Central time zone not returned correctly" - } - test "succeeds for US Mountain time" { - Expect.equal (name "America/Denver" _s |> string) "Mountain" "US Mountain time zone not returned correctly" - } - test "succeeds for US Mountain (AZ) time" { - Expect.equal (name "America/Phoenix" _s |> string) "Mountain (Arizona)" - "US Mountain (AZ) time zone not returned correctly" - } - test "succeeds for US Pacific time" { - Expect.equal (name "America/Los_Angeles" _s |> string) "Pacific" "US Pacific time zone not returned correctly" - } - test "succeeds for Central European time" { - Expect.equal (name "Europe/Berlin" _s |> string) "Central European" - "Central European time zone not returned correctly" - } - test "fails for unexpected time zone" { - Expect.equal (name "Wakanda" _s |> string) "Wakanda" "Unexpected time zone should have returned the original ID" - } - ] + [] + let nameTests = + testList "TimeZones.name" [ + test "succeeds for US Eastern time" { + Expect.equal (name "America/New_York" _s |> string) "Eastern" + "US Eastern time zone not returned correctly" + } + test "succeeds for US Central time" { + Expect.equal (name "America/Chicago" _s |> string) "Central" + "US Central time zone not returned correctly" + } + test "succeeds for US Mountain time" { + Expect.equal (name "America/Denver" _s |> string) "Mountain" + "US Mountain time zone not returned correctly" + } + test "succeeds for US Mountain (AZ) time" { + Expect.equal (name "America/Phoenix" _s |> string) "Mountain (Arizona)" + "US Mountain (AZ) time zone not returned correctly" + } + test "succeeds for US Pacific time" { + Expect.equal (name "America/Los_Angeles" _s |> string) "Pacific" + "US Pacific time zone not returned correctly" + } + test "succeeds for Central European time" { + Expect.equal (name "Europe/Berlin" _s |> string) "Central European" + "Central European time zone not returned correctly" + } + test "fails for unexpected time zone" { + Expect.equal (name "Wakanda" _s |> string) "Wakanda" + "Unexpected time zone should have returned the original ID" + } + ] diff --git a/src/PrayerTracker.Tests/UI/UtilsTests.fs b/src/PrayerTracker.Tests/UI/UtilsTests.fs index e5285f3..0b3c232 100644 --- a/src/PrayerTracker.Tests/UI/UtilsTests.fs +++ b/src/PrayerTracker.Tests/UI/UtilsTests.fs @@ -136,7 +136,7 @@ module StringTests = [] let stripTagsTests = - let testString = "

Here is some text

and some more

" + let testString = """

Here is some text

and some more

""" testList "stripTags" [ test "does nothing if all tags are allowed" { Expect.equal (stripTags [ "p"; "br" ] testString) testString diff --git a/src/PrayerTracker.Tests/UI/ViewModelsTests.fs b/src/PrayerTracker.Tests/UI/ViewModelsTests.fs index 46d4ddc..ab1678a 100644 --- a/src/PrayerTracker.Tests/UI/ViewModelsTests.fs +++ b/src/PrayerTracker.Tests/UI/ViewModelsTests.fs @@ -1,12 +1,12 @@ module PrayerTracker.UI.ViewModelsTests +open System open Expecto open Microsoft.AspNetCore.Html open PrayerTracker.Entities open PrayerTracker.Tests.TestLocalization open PrayerTracker.Utils open PrayerTracker.ViewModels -open System /// Filter function that filters nothing @@ -15,666 +15,708 @@ let countAll _ = true module ReferenceListTests = - [] - let asOfDateListTests = - testList "ReferenceList.asOfDateList" [ - test "has all three options listed" { - let asOf = ReferenceList.asOfDateList _s - Expect.hasCountOf asOf 3u countAll "There should have been 3 as-of choices returned" - Expect.exists asOf (fun (x, _) -> x = NoDisplay.code) "The option for no display was not found" - Expect.exists asOf (fun (x, _) -> x = ShortDate.code) "The option for a short date was not found" - Expect.exists asOf (fun (x, _) -> x = LongDate.code) "The option for a full date was not found" - } - ] - - [] - let emailTypeListTests = - testList "ReferenceList.emailTypeList" [ - test "includes default type" { - let typs = ReferenceList.emailTypeList HtmlFormat _s - Expect.hasCountOf typs 3u countAll "There should have been 3 e-mail type options returned" - let top = Seq.head typs - Expect.equal (fst top) "" "The default option should have been blank" - Expect.equal (snd top).Value "Group Default (HTML Format)" "The default option label was incorrect" - let nxt = typs |> Seq.skip 1 |> Seq.head - Expect.equal (fst nxt) HtmlFormat.code "The 2nd option should have been HTML" - let lst = typs |> Seq.last - Expect.equal (fst lst) PlainTextFormat.code "The 3rd option should have been plain text" - } - ] - - [] - let expirationListTests = - testList "ReferenceList.expirationList" [ - test "excludes immediate expiration if not required" { - let exps = ReferenceList.expirationList _s false - Expect.hasCountOf exps 2u countAll "There should have been 2 expiration types returned" - Expect.exists exps (fun (exp, _) -> exp = Automatic.code) "The option for automatic expiration was not found" - Expect.exists exps (fun (exp, _) -> exp = Manual.code) "The option for manual expiration was not found" - } - test "includes immediate expiration if required" { - let exps = ReferenceList.expirationList _s true - Expect.hasCountOf exps 3u countAll "There should have been 3 expiration types returned" - Expect.exists exps (fun (exp, _) -> exp = Automatic.code) "The option for automatic expiration was not found" - Expect.exists exps (fun (exp, _) -> exp = Manual.code) "The option for manual expiration was not found" - Expect.exists exps (fun (exp, _) -> exp = Forced.code) "The option for immediate expiration was not found" - } - ] - - [] - let requestTypeListTests = - testList "ReferenceList.requestTypeList" [ - let withList f () = - (ReferenceList.requestTypeList >> f) _s - yield! testFixture withList [ - yield "returns 5 types", - fun typs -> Expect.hasCountOf typs 5u countAll "There should have been 5 request types returned" - yield! [ CurrentRequest; LongTermRequest; PraiseReport; Expecting; Announcement ] - |> List.map (fun typ -> - sprintf "contains \"%O\"" typ, - fun typs -> - Expect.isSome (typs |> List.tryFind (fun x -> fst x = typ)) - (sprintf "The \"%O\" option was not found" typ)) + [] + let asOfDateListTests = + testList "ReferenceList.asOfDateList" [ + test "has all three options listed" { + let asOf = ReferenceList.asOfDateList _s + Expect.hasCountOf asOf 3u countAll "There should have been 3 as-of choices returned" + Expect.exists asOf (fun (x, _) -> x = NoDisplay.code) "The option for no display was not found" + Expect.exists asOf (fun (x, _) -> x = ShortDate.code) "The option for a short date was not found" + Expect.exists asOf (fun (x, _) -> x = LongDate.code) "The option for a full date was not found" + } + ] + + [] + let emailTypeListTests = + testList "ReferenceList.emailTypeList" [ + test "includes default type" { + let typs = ReferenceList.emailTypeList HtmlFormat _s + Expect.hasCountOf typs 3u countAll "There should have been 3 e-mail type options returned" + let top = Seq.head typs + Expect.equal (fst top) "" "The default option should have been blank" + Expect.equal (snd top).Value "Group Default (HTML Format)" "The default option label was incorrect" + let nxt = typs |> Seq.skip 1 |> Seq.head + Expect.equal (fst nxt) HtmlFormat.code "The 2nd option should have been HTML" + let lst = typs |> Seq.last + Expect.equal (fst lst) PlainTextFormat.code "The 3rd option should have been plain text" + } + ] + + [] + let expirationListTests = + testList "ReferenceList.expirationList" [ + test "excludes immediate expiration if not required" { + let exps = ReferenceList.expirationList _s false + Expect.hasCountOf exps 2u countAll "There should have been 2 expiration types returned" + Expect.exists exps (fun (exp, _) -> exp = Automatic.code) + "The option for automatic expiration was not found" + Expect.exists exps (fun (exp, _) -> exp = Manual.code) "The option for manual expiration was not found" + } + test "includes immediate expiration if required" { + let exps = ReferenceList.expirationList _s true + Expect.hasCountOf exps 3u countAll "There should have been 3 expiration types returned" + Expect.exists exps (fun (exp, _) -> exp = Automatic.code) + "The option for automatic expiration was not found" + Expect.exists exps (fun (exp, _) -> exp = Manual.code) "The option for manual expiration was not found" + Expect.exists exps (fun (exp, _) -> exp = Forced.code) + "The option for immediate expiration was not found" + } + ] + + [] + let requestTypeListTests = + testList "ReferenceList.requestTypeList" [ + let withList f () = + (ReferenceList.requestTypeList >> f) _s + yield! testFixture withList [ + yield "returns 5 types", + fun typs -> Expect.hasCountOf typs 5u countAll "There should have been 5 request types returned" + yield! + [ CurrentRequest; LongTermRequest; PraiseReport; Expecting; Announcement ] + |> List.map (fun typ -> + $"contains \"%O{typ}\"", + fun typs -> + Expect.isSome (typs |> List.tryFind (fun x -> fst x = typ)) + $"""The "%O{typ}" option was not found""") + ] ] - ] [] let announcementTests = - let empty = { sendToClass = "N"; text = "

unit testing

"; addToRequestList = None; requestType = None } - testList "Announcement" [ - test "plainText strips HTML" { - let ann = { empty with text = "

unit testing

" } - Expect.equal (ann.plainText ()) "unit testing" "Plain text should have stripped HTML" - } - test "plainText wraps at 74 characters" { - let ann = { empty with text = String.replicate 80 "x" } - let txt = (ann.plainText ()).Split "\n" - Expect.hasCountOf txt 3u countAll "There should have been two lines of plain text returned" - Expect.stringHasLength txt.[0] 74 "The first line should have been wrapped at 74 characters" - Expect.stringHasLength txt.[1] 6 "The second line should have had the remaining 6 characters" - Expect.stringHasLength txt.[2] 0 "The third line should have been blank" - } - test "plainText wraps at 74 characters and strips HTML" { - let ann = { empty with text = sprintf "%s" (String.replicate 80 "z") } - let txt = ann.plainText () - Expect.stringStarts txt "zzz" "HTML should have been stripped from the front of the plain text" - Expect.equal (txt.ToCharArray ()).[74] '\n' "The text should have been broken at 74 characters" - } + let empty = { SendToClass = "N"; Text = "

unit testing

"; AddToRequestList = None; RequestType = None } + testList "Announcement" [ + test "plainText strips HTML" { + let ann = { empty with Text = "

unit testing

" } + Expect.equal ann.PlainText "unit testing" "Plain text should have stripped HTML" + } + test "plainText wraps at 74 characters" { + let ann = { empty with Text = String.replicate 80 "x" } + let txt = ann.PlainText.Split "\n" + Expect.hasCountOf txt 3u countAll "There should have been two lines of plain text returned" + Expect.stringHasLength txt[0] 74 "The first line should have been wrapped at 74 characters" + Expect.stringHasLength txt[1] 6 "The second line should have had the remaining 6 characters" + Expect.stringHasLength txt[2] 0 "The third line should have been blank" + } + test "plainText wraps at 74 characters and strips HTML" { + let ann = { empty with Text = sprintf "%s" (String.replicate 80 "z") } + let txt = ann.PlainText + Expect.stringStarts txt "zzz" "HTML should have been stripped from the front of the plain text" + Expect.equal (txt.ToCharArray ()).[74] '\n' "The text should have been broken at 74 characters" + } ] [] let appViewInfoTests = - testList "AppViewInfo" [ - test "fresh is constructed properly" { - let vi = AppViewInfo.fresh - Expect.isEmpty vi.style "There should have been no styles set" - Expect.isEmpty vi.script "There should have been no scripts set" - 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.isNone vi.user "There should not have been a user" - Expect.isNone vi.group "There should not have been a small group" - } + testList "AppViewInfo" [ + test "fresh is constructed properly" { + let vi = AppViewInfo.fresh + Expect.isEmpty vi.Style "There should have been no styles set" + Expect.isEmpty vi.Script "There should have been no scripts set" + 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.isNone vi.User "There should not have been a user" + Expect.isNone vi.Group "There should not have been a small group" + } ] [] let assignGroupsTests = - testList "AssignGroups" [ - test "fromUser populates correctly" { - let usr = { User.empty with userId = Guid.NewGuid (); firstName = "Alice"; lastName = "Bob" } - let asg = AssignGroups.fromUser usr - Expect.equal asg.userId usr.userId "The user ID was not filled correctly" - Expect.equal asg.userName usr.fullName "The user name was not filled correctly" - Expect.equal asg.smallGroups "" "The small group string was not filled correctly" - } + testList "AssignGroups" [ + test "fromUser populates correctly" { + let usr = { User.empty with userId = Guid.NewGuid (); firstName = "Alice"; lastName = "Bob" } + let asg = AssignGroups.fromUser usr + Expect.equal asg.UserId usr.userId "The user ID was not filled correctly" + Expect.equal asg.UserName usr.fullName "The user name was not filled correctly" + Expect.equal asg.SmallGroups "" "The small group string was not filled correctly" + } ] [] let editChurchTests = - testList "EditChurch" [ - test "fromChurch populates correctly when interface exists" { - let church = - { Church.empty with - churchId = Guid.NewGuid () - name = "Unit Test" - city = "Testlandia" - st = "UT" - hasInterface = true - interfaceAddress = Some "https://test-dem-units.test" - } - let edit = EditChurch.fromChurch church - Expect.equal edit.churchId church.churchId "The church ID was not filled correctly" - Expect.equal edit.name church.name "The church name was not filled correctly" - Expect.equal edit.city church.city "The church's city was not filled correctly" - Expect.equal edit.st church.st "The church's state was not filled correctly" - Expect.isSome edit.hasInterface "The church should show that it has an interface" - Expect.equal edit.hasInterface (Some true) "The hasInterface flag should be true" - Expect.isSome edit.interfaceAddress "The interface address should exist" - Expect.equal edit.interfaceAddress church.interfaceAddress "The interface address was not filled correctly" - } - test "fromChurch populates correctly when interface does not exist" { - let edit = - EditChurch.fromChurch - { Church.empty with - churchId = Guid.NewGuid () - name = "Unit Test" - city = "Testlandia" - st = "UT" - } - Expect.isNone edit.hasInterface "The church should not show that it has an interface" - Expect.isNone edit.interfaceAddress "The interface address should not exist" - } - test "empty is as expected" { - let edit = EditChurch.empty - Expect.equal edit.churchId Guid.Empty "The church ID should be the empty GUID" - Expect.equal edit.name "" "The church name should be blank" - Expect.equal edit.city "" "The church's city should be blank" - Expect.equal edit.st "" "The church's state should be blank" - Expect.isNone edit.hasInterface "The church should not show that it has an interface" - Expect.isNone edit.interfaceAddress "The interface address should not exist" - } - test "isNew works on a new church" { - Expect.isTrue (EditChurch.empty.isNew ()) "An empty GUID should be flagged as a new church" - } - test "isNew works on an existing church" { - Expect.isFalse ({ EditChurch.empty with churchId = Guid.NewGuid () }.isNew ()) - "A non-empty GUID should not be flagged as a new church" - } - test "populateChurch works correctly when an interface exists" { - let edit = - { EditChurch.empty with - churchId = Guid.NewGuid () - name = "Test Baptist Church" - city = "Testerville" - st = "TE" - hasInterface = Some true - interfaceAddress = Some "https://test.units" - } - let church = edit.populateChurch Church.empty - Expect.notEqual church.churchId edit.churchId "The church ID should not have been modified" - Expect.equal church.name edit.name "The church name was not updated correctly" - Expect.equal church.city edit.city "The church's city was not updated correctly" - Expect.equal church.st edit.st "The church's state was not updated correctly" - Expect.isTrue church.hasInterface "The church should show that it has an interface" - Expect.isSome church.interfaceAddress "The interface address should exist" - Expect.equal church.interfaceAddress edit.interfaceAddress "The interface address was not updated correctly" - } - test "populateChurch works correctly when an interface does not exist" { - let church = - { EditChurch.empty with - name = "Test Baptist Church" - city = "Testerville" - st = "TE" - }.populateChurch Church.empty - Expect.isFalse church.hasInterface "The church should show that it has an interface" - Expect.isNone church.interfaceAddress "The interface address should exist" - } + testList "EditChurch" [ + test "fromChurch populates correctly when interface exists" { + let church = + { Church.empty with + churchId = Guid.NewGuid () + name = "Unit Test" + city = "Testlandia" + st = "UT" + hasInterface = true + interfaceAddress = Some "https://test-dem-units.test" + } + let edit = EditChurch.fromChurch church + Expect.equal edit.ChurchId church.churchId "The church ID was not filled correctly" + Expect.equal edit.Name church.name "The church name was not filled correctly" + Expect.equal edit.City church.city "The church's city was not filled correctly" + Expect.equal edit.State church.st "The church's state was not filled correctly" + Expect.isSome edit.HasInterface "The church should show that it has an interface" + Expect.equal edit.HasInterface (Some true) "The hasInterface flag should be true" + Expect.isSome edit.InterfaceAddress "The interface address should exist" + Expect.equal edit.InterfaceAddress church.interfaceAddress "The interface address was not filled correctly" + } + test "fromChurch populates correctly when interface does not exist" { + let edit = + EditChurch.fromChurch + { Church.empty with + churchId = Guid.NewGuid () + name = "Unit Test" + city = "Testlandia" + st = "UT" + } + Expect.isNone edit.HasInterface "The church should not show that it has an interface" + Expect.isNone edit.InterfaceAddress "The interface address should not exist" + } + test "empty is as expected" { + let edit = EditChurch.empty + Expect.equal edit.ChurchId Guid.Empty "The church ID should be the empty GUID" + Expect.equal edit.Name "" "The church name should be blank" + Expect.equal edit.City "" "The church's city should be blank" + Expect.equal edit.State "" "The church's state should be blank" + Expect.isNone edit.HasInterface "The church should not show that it has an interface" + Expect.isNone edit.InterfaceAddress "The interface address should not exist" + } + test "isNew works on a new church" { + Expect.isTrue EditChurch.empty.IsNew "An empty GUID should be flagged as a new church" + } + test "isNew works on an existing church" { + Expect.isFalse { EditChurch.empty with ChurchId = Guid.NewGuid () }.IsNew + "A non-empty GUID should not be flagged as a new church" + } + test "populateChurch works correctly when an interface exists" { + let edit = + { EditChurch.empty with + ChurchId = Guid.NewGuid () + Name = "Test Baptist Church" + City = "Testerville" + State = "TE" + HasInterface = Some true + InterfaceAddress = Some "https://test.units" + } + let church = edit.PopulateChurch Church.empty + Expect.notEqual church.churchId edit.ChurchId "The church ID should not have been modified" + Expect.equal church.name edit.Name "The church name was not updated correctly" + Expect.equal church.city edit.City "The church's city was not updated correctly" + Expect.equal church.st edit.State "The church's state was not updated correctly" + Expect.isTrue church.hasInterface "The church should show that it has an interface" + Expect.isSome church.interfaceAddress "The interface address should exist" + Expect.equal church.interfaceAddress edit.InterfaceAddress "The interface address was not updated correctly" + } + test "populateChurch works correctly when an interface does not exist" { + let church = + { EditChurch.empty with + Name = "Test Baptist Church" + City = "Testerville" + State = "TE" + }.PopulateChurch Church.empty + Expect.isFalse church.hasInterface "The church should show that it has an interface" + Expect.isNone church.interfaceAddress "The interface address should exist" + } ] [] let editMemberTests = - testList "EditMember" [ - test "fromMember populates with group default format" { - let mbr = - { Member.empty with - memberId = Guid.NewGuid () - memberName = "Test Name" - email = "test_units@example.com" - } - let edit = EditMember.fromMember mbr - Expect.equal edit.memberId mbr.memberId "The member ID was not filled correctly" - Expect.equal edit.memberName mbr.memberName "The member name was not filled correctly" - Expect.equal edit.emailAddress mbr.email "The e-mail address was not filled correctly" - Expect.equal edit.emailType "" "The e-mail type should have been blank for group default" - } - test "fromMember populates with specific format" { - let edit = EditMember.fromMember { Member.empty with format = Some HtmlFormat.code } - Expect.equal edit.emailType HtmlFormat.code "The e-mail type was not filled correctly" - } - test "empty is as expected" { - let edit = EditMember.empty - Expect.equal edit.memberId Guid.Empty "The member ID should have been an empty GUID" - Expect.equal edit.memberName "" "The member name should have been blank" - Expect.equal edit.emailAddress "" "The e-mail address should have been blank" - Expect.equal edit.emailType "" "The e-mail type should have been blank" - } - test "isNew works for a new member" { - Expect.isTrue (EditMember.empty.isNew ()) "An empty GUID should be flagged as a new member" - } - test "isNew works for an existing member" { - Expect.isFalse ({ EditMember.empty with memberId = Guid.NewGuid () }.isNew ()) - "A non-empty GUID should not be flagged as a new member" - } + testList "EditMember" [ + test "fromMember populates with group default format" { + let mbr = + { Member.empty with + memberId = Guid.NewGuid () + memberName = "Test Name" + email = "test_units@example.com" + } + let edit = EditMember.fromMember mbr + Expect.equal edit.MemberId mbr.memberId "The member ID was not filled correctly" + Expect.equal edit.Name mbr.memberName "The member name was not filled correctly" + Expect.equal edit.Email mbr.email "The e-mail address was not filled correctly" + Expect.equal edit.Format "" "The e-mail format should have been blank for group default" + } + test "fromMember populates with specific format" { + let edit = EditMember.fromMember { Member.empty with format = Some HtmlFormat.code } + Expect.equal edit.Format HtmlFormat.code "The e-mail format was not filled correctly" + } + test "empty is as expected" { + let edit = EditMember.empty + Expect.equal edit.MemberId Guid.Empty "The member ID should have been an empty GUID" + Expect.equal edit.Name "" "The member name should have been blank" + Expect.equal edit.Email "" "The e-mail address should have been blank" + Expect.equal edit.Format "" "The e-mail format should have been blank" + } + test "isNew works for a new member" { + Expect.isTrue EditMember.empty.IsNew "An empty GUID should be flagged as a new member" + } + test "isNew works for an existing member" { + Expect.isFalse { EditMember.empty with MemberId = Guid.NewGuid () }.IsNew + "A non-empty GUID should not be flagged as a new member" + } ] [] let editPreferencesTests = - testList "EditPreferences" [ - test "fromPreferences succeeds for named colors and private list" { - let prefs = ListPreferences.empty - let edit = EditPreferences.fromPreferences prefs - Expect.equal edit.expireDays prefs.daysToExpire "The expiration days were not filled correctly" - Expect.equal edit.daysToKeepNew prefs.daysToKeepNew "The days to keep new were not filled correctly" - Expect.equal edit.longTermUpdateWeeks prefs.longTermUpdateWeeks "The weeks for update were not filled correctly" - Expect.equal edit.requestSort prefs.requestSort.code "The request sort was not filled correctly" - Expect.equal edit.emailFromName prefs.emailFromName "The e-mail from name was not filled correctly" - Expect.equal edit.emailFromAddress prefs.emailFromAddress "The e-mail from address was not filled correctly" - Expect.equal edit.defaultEmailType prefs.defaultEmailType.code "The default e-mail type was not filled correctly" - Expect.equal edit.headingLineType "Name" "The heading line color type was not derived correctly" - Expect.equal edit.headingLineColor prefs.lineColor "The heading line color was not filled correctly" - Expect.equal edit.headingTextType "Name" "The heading text color type was not derived correctly" - Expect.equal edit.headingTextColor prefs.headingColor "The heading text color was not filled correctly" - Expect.equal edit.listFonts prefs.listFonts "The list fonts were not filled correctly" - Expect.equal edit.headingFontSize prefs.headingFontSize "The heading font size was not filled correctly" - Expect.equal edit.listFontSize prefs.textFontSize "The list text font size was not filled correctly" - Expect.equal edit.timeZone prefs.timeZoneId "The time zone was not filled correctly" - Expect.isSome edit.groupPassword "The group password should have been set" - Expect.equal edit.groupPassword (Some prefs.groupPassword) "The group password was not filled correctly" - Expect.equal edit.listVisibility RequestVisibility.``private`` "The list visibility was not derived correctly" - } - test "fromPreferences succeeds for RGB line color and password-protected list" { - let prefs = { ListPreferences.empty with lineColor = "#ff0000"; groupPassword = "pw" } - let edit = EditPreferences.fromPreferences prefs - Expect.equal edit.headingLineType "RGB" "The heading line color type was not derived correctly" - Expect.equal edit.headingLineColor prefs.lineColor "The heading line color was not filled correctly" - Expect.isSome edit.groupPassword "The group password should have been set" - Expect.equal edit.groupPassword (Some prefs.groupPassword) "The group password was not filled correctly" - Expect.equal edit.listVisibility RequestVisibility.passwordProtected - "The list visibility was not derived correctly" - } - test "fromPreferences succeeds for RGB text color and public list" { - let prefs = { ListPreferences.empty with headingColor = "#0000ff"; isPublic = true } - let edit = EditPreferences.fromPreferences prefs - Expect.equal edit.headingTextType "RGB" "The heading text color type was not derived correctly" - Expect.equal edit.headingTextColor prefs.headingColor "The heading text color was not filled correctly" - Expect.isSome edit.groupPassword "The group password should have been set" - Expect.equal edit.groupPassword (Some "") "The group password was not filled correctly" - Expect.equal edit.listVisibility RequestVisibility.``public`` "The list visibility was not derived correctly" - } + testList "EditPreferences" [ + test "fromPreferences succeeds for named colors and private list" { + let prefs = ListPreferences.empty + let edit = EditPreferences.fromPreferences prefs + Expect.equal edit.ExpireDays prefs.daysToExpire "The expiration days were not filled correctly" + Expect.equal edit.DaysToKeepNew prefs.daysToKeepNew "The days to keep new were not filled correctly" + Expect.equal edit.LongTermUpdateWeeks prefs.longTermUpdateWeeks + "The weeks for update were not filled correctly" + Expect.equal edit.RequestSort prefs.requestSort.code "The request sort was not filled correctly" + Expect.equal edit.EmailFromName prefs.emailFromName "The e-mail from name was not filled correctly" + Expect.equal edit.EmailFromAddress prefs.emailFromAddress "The e-mail from address was not filled correctly" + Expect.equal edit.DefaultEmailType prefs.defaultEmailType.code + "The default e-mail type was not filled correctly" + Expect.equal edit.LineColorType "Name" "The heading line color type was not derived correctly" + Expect.equal edit.LineColor prefs.lineColor "The heading line color was not filled correctly" + Expect.equal edit.HeadingColorType "Name" "The heading text color type was not derived correctly" + Expect.equal edit.HeadingColor prefs.headingColor "The heading text color was not filled correctly" + Expect.equal edit.Fonts prefs.listFonts "The list fonts were not filled correctly" + Expect.equal edit.HeadingFontSize prefs.headingFontSize "The heading font size was not filled correctly" + Expect.equal edit.ListFontSize prefs.textFontSize "The list text font size was not filled correctly" + Expect.equal edit.TimeZone prefs.timeZoneId "The time zone was not filled correctly" + Expect.isSome edit.GroupPassword "The group password should have been set" + Expect.equal edit.GroupPassword (Some prefs.groupPassword) "The group password was not filled correctly" + Expect.equal edit.Visibility RequestVisibility.``private`` + "The list visibility was not derived correctly" + Expect.equal edit.PageSize prefs.pageSize "The page size was not filled correctly" + Expect.equal edit.AsOfDate prefs.asOfDateDisplay.code "The as-of date display was not filled correctly" + } + test "fromPreferences succeeds for RGB line color and password-protected list" { + let prefs = { ListPreferences.empty with lineColor = "#ff0000"; groupPassword = "pw" } + let edit = EditPreferences.fromPreferences prefs + Expect.equal edit.LineColorType "RGB" "The heading line color type was not derived correctly" + Expect.equal edit.LineColor prefs.lineColor "The heading line color was not filled correctly" + Expect.isSome edit.GroupPassword "The group password should have been set" + Expect.equal edit.GroupPassword (Some prefs.groupPassword) "The group password was not filled correctly" + Expect.equal edit.Visibility RequestVisibility.passwordProtected + "The list visibility was not derived correctly" + } + test "fromPreferences succeeds for RGB text color and public list" { + let prefs = { ListPreferences.empty with headingColor = "#0000ff"; isPublic = true } + let edit = EditPreferences.fromPreferences prefs + Expect.equal edit.HeadingColorType "RGB" "The heading text color type was not derived correctly" + Expect.equal edit.HeadingColor prefs.headingColor "The heading text color was not filled correctly" + Expect.isSome edit.GroupPassword "The group password should have been set" + Expect.equal edit.GroupPassword (Some "") "The group password was not filled correctly" + Expect.equal edit.Visibility RequestVisibility.``public`` + "The list visibility was not derived correctly" + } ] [] let editRequestTests = - testList "EditRequest" [ - test "empty is as expected" { - let mt = EditRequest.empty - Expect.equal mt.requestId Guid.Empty "The request ID should be an empty GUID" - Expect.equal mt.requestType CurrentRequest.code "The request type should have been \"Current\"" - Expect.isNone mt.enteredDate "The entered date should have been None" - Expect.isNone mt.skipDateUpdate "The \"skip date update\" flag should have been None" - Expect.isNone mt.requestor "The requestor should have been None" - Expect.equal mt.expiration Automatic.code "The expiration should have been \"A\" (Automatic)" - Expect.equal mt.text "" "The text should have been blank" - } - test "fromRequest succeeds" { - let req = - { PrayerRequest.empty with - prayerRequestId = Guid.NewGuid () - requestType = CurrentRequest - requestor = Some "Me" - expiration = Manual - text = "the text" - } - let edit = EditRequest.fromRequest req - Expect.equal edit.requestId req.prayerRequestId "The request ID was not filled correctly" - Expect.equal edit.requestType req.requestType.code "The request type was not filled correctly" - Expect.equal edit.requestor req.requestor "The requestor was not filled correctly" - Expect.equal edit.expiration Manual.code "The expiration was not filled correctly" - Expect.equal edit.text req.text "The text was not filled correctly" - } - test "isNew works for a new request" { - Expect.isTrue (EditRequest.empty.isNew ()) "An empty GUID should be flagged as a new request" - } - test "isNew works for an existing request" { - Expect.isFalse ({ EditRequest.empty with requestId = Guid.NewGuid () }.isNew ()) - "A non-empty GUID should not be flagged as a new request" - } + testList "EditRequest" [ + test "empty is as expected" { + let mt = EditRequest.empty + Expect.equal mt.RequestId Guid.Empty "The request ID should be an empty GUID" + Expect.equal mt.RequestType CurrentRequest.code "The request type should have been \"Current\"" + Expect.isNone mt.EnteredDate "The entered date should have been None" + Expect.isNone mt.SkipDateUpdate """The "skip date update" flag should have been None""" + Expect.isNone mt.Requestor "The requestor should have been None" + Expect.equal mt.Expiration Automatic.code """The expiration should have been "A" (Automatic)""" + Expect.equal mt.Text "" "The text should have been blank" + } + test "fromRequest succeeds" { + let req = + { PrayerRequest.empty with + prayerRequestId = Guid.NewGuid () + requestType = CurrentRequest + requestor = Some "Me" + expiration = Manual + text = "the text" + } + let edit = EditRequest.fromRequest req + Expect.equal edit.RequestId req.prayerRequestId "The request ID was not filled correctly" + Expect.equal edit.RequestType req.requestType.code "The request type was not filled correctly" + Expect.equal edit.Requestor req.requestor "The requestor was not filled correctly" + Expect.equal edit.Expiration Manual.code "The expiration was not filled correctly" + Expect.equal edit.Text req.text "The text was not filled correctly" + } + test "isNew works for a new request" { + Expect.isTrue EditRequest.empty.IsNew "An empty GUID should be flagged as a new request" + } + test "isNew works for an existing request" { + Expect.isFalse { EditRequest.empty with RequestId = Guid.NewGuid () }.IsNew + "A non-empty GUID should not be flagged as a new request" + } ] [] let editSmallGroupTests = - testList "EditSmallGroup" [ - test "fromGroup succeeds" { - let grp = - { SmallGroup.empty with - smallGroupId = Guid.NewGuid () - name = "test group" - churchId = Guid.NewGuid () - } - let edit = EditSmallGroup.fromGroup grp - Expect.equal edit.smallGroupId grp.smallGroupId "The small group ID was not filled correctly" - Expect.equal edit.name grp.name "The name was not filled correctly" - Expect.equal edit.churchId grp.churchId "The church ID was not filled correctly" - } - test "empty is as expected" { - let mt = EditSmallGroup.empty - Expect.equal mt.smallGroupId Guid.Empty "The small group ID should be an empty GUID" - Expect.equal mt.name "" "The name should be blank" - Expect.equal mt.churchId Guid.Empty "The church ID should be an empty GUID" - } - test "isNew works for a new small group" { - Expect.isTrue (EditSmallGroup.empty.isNew ()) "An empty GUID should be flagged as a new small group" - } - test "isNew works for an existing small group" { - Expect.isFalse ({ EditSmallGroup.empty with smallGroupId = Guid.NewGuid () }.isNew ()) - "A non-empty GUID should not be flagged as a new small group" - } - test "populateGroup succeeds" { - let edit = - { EditSmallGroup.empty with - name = "test name" - churchId = Guid.NewGuid () - } - let grp = edit.populateGroup SmallGroup.empty - Expect.equal grp.name edit.name "The name was not populated correctly" - Expect.equal grp.churchId edit.churchId "The church ID was not populated correctly" - } + testList "EditSmallGroup" [ + test "fromGroup succeeds" { + let grp = + { SmallGroup.empty with + smallGroupId = Guid.NewGuid () + name = "test group" + churchId = Guid.NewGuid () + } + let edit = EditSmallGroup.fromGroup grp + Expect.equal edit.SmallGroupId grp.smallGroupId "The small group ID was not filled correctly" + Expect.equal edit.Name grp.name "The name was not filled correctly" + Expect.equal edit.ChurchId grp.churchId "The church ID was not filled correctly" + } + test "empty is as expected" { + let mt = EditSmallGroup.empty + Expect.equal mt.SmallGroupId Guid.Empty "The small group ID should be an empty GUID" + Expect.equal mt.Name "" "The name should be blank" + Expect.equal mt.ChurchId Guid.Empty "The church ID should be an empty GUID" + } + test "isNew works for a new small group" { + Expect.isTrue EditSmallGroup.empty.IsNew "An empty GUID should be flagged as a new small group" + } + test "isNew works for an existing small group" { + Expect.isFalse { EditSmallGroup.empty with SmallGroupId = Guid.NewGuid () }.IsNew + "A non-empty GUID should not be flagged as a new small group" + } + test "populateGroup succeeds" { + let edit = + { EditSmallGroup.empty with + Name = "test name" + ChurchId = Guid.NewGuid () + } + let grp = edit.populateGroup SmallGroup.empty + Expect.equal grp.name edit.Name "The name was not populated correctly" + Expect.equal grp.churchId edit.ChurchId "The church ID was not populated correctly" + } ] [] let editUserTests = - testList "EditUser" [ - test "empty is as expected" { - let mt = EditUser.empty - Expect.equal mt.userId Guid.Empty "The user ID should be an empty GUID" - Expect.equal mt.firstName "" "The first name should be blank" - Expect.equal mt.lastName "" "The last name should be blank" - Expect.equal mt.emailAddress "" "The e-mail address should be blank" - Expect.equal mt.password "" "The password should be blank" - Expect.equal mt.passwordConfirm "" "The confirmed password should be blank" - Expect.isNone mt.isAdmin "The isAdmin flag should be None" - } - test "fromUser succeeds" { - let usr = - { User.empty with - userId = Guid.NewGuid () - firstName = "user" - lastName = "test" - emailAddress = "a@b.c" - } - let edit = EditUser.fromUser usr - Expect.equal edit.userId usr.userId "The user ID was not filled correctly" - Expect.equal edit.firstName usr.firstName "The first name was not filled correctly" - Expect.equal edit.lastName usr.lastName "The last name was not filled correctly" - Expect.equal edit.emailAddress usr.emailAddress "The e-mail address was not filled correctly" - Expect.isNone edit.isAdmin "The isAdmin flag was not filled correctly" - } - test "isNew works for a new user" { - Expect.isTrue (EditUser.empty.isNew ()) "An empty GUID should be flagged as a new user" - } - test "isNew works for an existing user" { - Expect.isFalse ({ EditUser.empty with userId = Guid.NewGuid () }.isNew ()) - "A non-empty GUID should not be flagged as a new user" - } - test "populateUser succeeds" { - let edit = - { EditUser.empty with - firstName = "name" - lastName = "eman" - emailAddress = "n@m.e" - isAdmin = Some true - password = "testpw" - } - let hasher = fun x -> x + "+" - let usr = edit.populateUser User.empty hasher - Expect.equal usr.firstName edit.firstName "The first name was not populated correctly" - Expect.equal usr.lastName edit.lastName "The last name was not populated correctly" - Expect.equal usr.emailAddress edit.emailAddress "The e-mail address was not populated correctly" - Expect.isTrue usr.isAdmin "The isAdmin flag was not populated correctly" - Expect.equal usr.passwordHash (hasher edit.password) "The password hash was not populated correctly" - } + testList "EditUser" [ + test "empty is as expected" { + let mt = EditUser.empty + Expect.equal mt.UserId Guid.Empty "The user ID should be an empty GUID" + Expect.equal mt.FirstName "" "The first name should be blank" + Expect.equal mt.LastName "" "The last name should be blank" + Expect.equal mt.Email "" "The e-mail address should be blank" + Expect.equal mt.Password "" "The password should be blank" + Expect.equal mt.PasswordConfirm "" "The confirmed password should be blank" + Expect.isNone mt.IsAdmin "The IsAdmin flag should be None" + } + test "fromUser succeeds" { + let usr = + { User.empty with + userId = Guid.NewGuid () + firstName = "user" + lastName = "test" + emailAddress = "a@b.c" + } + let edit = EditUser.fromUser usr + Expect.equal edit.UserId usr.userId "The user ID was not filled correctly" + Expect.equal edit.FirstName usr.firstName "The first name was not filled correctly" + Expect.equal edit.LastName usr.lastName "The last name was not filled correctly" + Expect.equal edit.Email usr.emailAddress "The e-mail address was not filled correctly" + Expect.isNone edit.IsAdmin "The IsAdmin flag was not filled correctly" + } + test "isNew works for a new user" { + Expect.isTrue EditUser.empty.IsNew "An empty GUID should be flagged as a new user" + } + test "isNew works for an existing user" { + Expect.isFalse { EditUser.empty with UserId = Guid.NewGuid () }.IsNew + "A non-empty GUID should not be flagged as a new user" + } + test "populateUser succeeds" { + let edit = + { EditUser.empty with + FirstName = "name" + LastName = "eman" + Email = "n@m.e" + IsAdmin = Some true + Password = "testpw" + } + let hasher = fun x -> x + "+" + let usr = edit.PopulateUser User.empty hasher + Expect.equal usr.firstName edit.FirstName "The first name was not populated correctly" + Expect.equal usr.lastName edit.LastName "The last name was not populated correctly" + Expect.equal usr.emailAddress edit.Email "The e-mail address was not populated correctly" + Expect.isTrue usr.isAdmin "The isAdmin flag was not populated correctly" + Expect.equal usr.passwordHash (hasher edit.Password) "The password hash was not populated correctly" + } ] [] let groupLogOnTests = - testList "GroupLogOn" [ - test "empty is as expected" { - let mt = GroupLogOn.empty - Expect.equal mt.smallGroupId Guid.Empty "The small group ID should be an empty GUID" - Expect.equal mt.password "" "The password should be blank" - Expect.isNone mt.rememberMe "Remember Me should be None" - } + testList "GroupLogOn" [ + test "empty is as expected" { + let mt = GroupLogOn.empty + Expect.equal mt.SmallGroupId Guid.Empty "The small group ID should be an empty GUID" + Expect.equal mt.Password "" "The password should be blank" + Expect.isNone mt.RememberMe "Remember Me should be None" + } ] [] let maintainRequestsTests = - testList "MaintainRequests" [ - test "empty is as expected" { - let mt = MaintainRequests.empty - Expect.isEmpty mt.requests "The requests for the model should have been empty" - Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one" - Expect.isNone mt.onlyActive "The only active flag should have been None" - Expect.isNone mt.searchTerm "The search term should have been None" - Expect.isNone mt.pageNbr "The page number should have been None" - } + testList "MaintainRequests" [ + test "empty is as expected" { + let mt = MaintainRequests.empty + Expect.isEmpty mt.Requests "The requests for the model should have been empty" + Expect.equal mt.SmallGroup.smallGroupId Guid.Empty "The small group should have been an empty one" + Expect.isNone mt.OnlyActive "The only active flag should have been None" + Expect.isNone mt.SearchTerm "The search term should have been None" + Expect.isNone mt.PageNbr "The page number should have been None" + } + ] + +[] +let messageLevelTests = + testList "MessageLevel" [ + test "toString for Info is as expected" { + Expect.equal (MessageLevel.toString Info) "Info" """The string value of "Info" is incorrect""" + } + test "toString for Warning is as expected" { + Expect.equal (MessageLevel.toString Warning) "WARNING" """The string value of "Warning" is incorrect""" + } + test "toString for Error is as expected" { + Expect.equal (MessageLevel.toString Error) "ERROR" """The string value of "Error" is incorrect""" + } + test "toCssClass for Info is as expected" { + Expect.equal (MessageLevel.toCssClass Info) "info" """The string value of "Info" is incorrect""" + } + test "toCssClass for Warning is as expected" { + Expect.equal (MessageLevel.toCssClass Warning) "warning" """The string value of "Warning" is incorrect""" + } + test "toCssClass for Error is as expected" { + Expect.equal (MessageLevel.toCssClass Error) "error" """The string value of "Error" is incorrect""" + } ] [] let requestListTests = - testList "RequestList" [ - let withRequestList f () = - { requests = [ - { PrayerRequest.empty with - requestType = CurrentRequest - requestor = Some "Zeb" - text = "zyx" - updatedDate = DateTime.Today - } - { PrayerRequest.empty with - requestType = CurrentRequest - requestor = Some "Aaron" - text = "abc" - updatedDate = DateTime.Today - TimeSpan.FromDays 9. - } - { PrayerRequest.empty with - requestType = PraiseReport - text = "nmo" - updatedDate = DateTime.Today + testList "RequestList" [ + let withRequestList f () = + { Requests = [ + { PrayerRequest.empty with + requestType = CurrentRequest + requestor = Some "Zeb" + text = "zyx" + updatedDate = DateTime.Today + } + { PrayerRequest.empty with + requestType = CurrentRequest + requestor = Some "Aaron" + text = "abc" + updatedDate = DateTime.Today - TimeSpan.FromDays 9. + } + { PrayerRequest.empty with + requestType = PraiseReport + text = "nmo" + updatedDate = DateTime.Today + } + ] + Date = DateTime.Today + SmallGroup = SmallGroup.empty + ShowHeader = false + Recipients = [] + CanEmail = false } + |> f + yield! testFixture withRequestList [ + "AsHtml succeeds without header or as-of date", + fun reqList -> + let htmlList = { reqList with SmallGroup = { reqList.SmallGroup with name = "Test HTML Group" } } + let html = htmlList.AsHtml _s + Expect.equal -1 (html.IndexOf "Test HTML Group") + "The small group name should not have existed (no header)" + let curReqHeading = + [ """""" + "" + """
""" + "    Current Requests   
" + ] + |> String.concat "" + Expect.stringContains html curReqHeading """Heading for category "Current Requests" not found""" + let curReqHtml = + [ "
    " + """
  • """ + "Zeb — zyx
  • " + """
  • """ + "Aaron — abc
" + ] + |> String.concat "" + Expect.stringContains html curReqHtml """Expected HTML for "Current Requests" requests not found""" + let praiseHeading = + [ """""" + "" + """
""" + "    Praise Reports   
" + ] + |> String.concat "" + Expect.stringContains html praiseHeading """Heading for category "Praise Reports" not found""" + let praiseHtml = + [ "
    " + """
  • """ + "nmo
" + ] + |> String.concat "" + Expect.stringContains html praiseHtml """Expected HTML for "Praise Reports" requests not found""" + "AsHtml succeeds with header", + fun reqList -> + let htmlList = + { reqList with + SmallGroup = { reqList.SmallGroup with name = "Test HTML Group" } + ShowHeader = true + } + let html = htmlList.AsHtml _s + let lstHeading = + [ """
""" + """Prayer Requests
""" + """Test HTML Group
""" + htmlList.Date.ToString "MMMM d, yyyy" + "

" + ] + |> String.concat "" + Expect.stringContains html lstHeading "Expected HTML for the list heading not found" + // spot check; without header test tests this exhaustively + Expect.stringContains html "Zeb — zyx" "Expected requests not found" + "AsHtml succeeds with short as-of date", + fun reqList -> + let htmlList = + { reqList with + SmallGroup = + { reqList.SmallGroup with + preferences = { reqList.SmallGroup.preferences with asOfDateDisplay = ShortDate } + } + } + let html = htmlList.AsHtml _s + let expected = + htmlList.Requests[0].updatedDate.ToShortDateString () + |> sprintf """Zeb — zyx  (as of %s)""" + // spot check; if one request has it, they all should + Expect.stringContains html expected "Expected short as-of date not found" + "AsHtml succeeds with long as-of date", + fun reqList -> + let htmlList = + { reqList with + SmallGroup = + { reqList.SmallGroup with + preferences = { reqList.SmallGroup.preferences with asOfDateDisplay = LongDate } + } + } + let html = htmlList.AsHtml _s + let expected = + htmlList.Requests[0].updatedDate.ToLongDateString () + |> sprintf """Zeb — zyx  (as of %s)""" + // spot check; if one request has it, they all should + Expect.stringContains html expected "Expected long as-of date not found" + "AsText succeeds with no as-of date", + fun reqList -> + let textList = { reqList with SmallGroup = { reqList.SmallGroup with name = "Test Group" } } + 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 "--------------------\n CURRENT REQUESTS\n--------------------\n" + """Heading for category "Current Requests" not found""" + Expect.stringContains text " + Zeb - zyx\n" "First request not found" + Expect.stringContains text " - Aaron - abc\n \n" + "Second request not found; should have been end of category" + Expect.stringContains text "------------------\n PRAISE REPORTS\n------------------\n" + """Heading for category "Praise Reports" not found""" + Expect.stringContains text " + nmo\n \n" "Last request not found" + "AsText succeeds with short as-of date", + fun reqList -> + let textList = + { reqList with + SmallGroup = + { reqList.SmallGroup with + preferences = { reqList.SmallGroup.preferences with asOfDateDisplay = ShortDate } + } + } + let text = textList.AsText _s + let expected = + textList.Requests[0].updatedDate.ToShortDateString () + |> 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" + "AsText succeeds with long as-of date", + fun reqList -> + let textList = + { reqList with + SmallGroup = + { reqList.SmallGroup with + preferences = { reqList.SmallGroup.preferences with asOfDateDisplay = LongDate } + } + } + let text = textList.AsText _s + let expected = + textList.Requests[0].updatedDate.ToLongDateString () + |> 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" + "IsNew succeeds for both old and new requests", + fun reqList -> + let allReqs = reqList.RequestsByType _s + let _, _, reqs = allReqs |> List.find (fun (typ, _, _) -> typ = CurrentRequest) + Expect.hasCountOf reqs 2u countAll "There should have been two requests" + Expect.isTrue (reqList.IsNew (List.head reqs)) "The first request should have been new" + Expect.isFalse (reqList.IsNew (List.last reqs)) "The second request should not have been new" + "RequestsByType succeeds", + fun reqList -> + let allReqs = reqList.RequestsByType _s + Expect.hasLength allReqs 2 "There should have been two types of request groupings" + let maybeCurrent = allReqs |> List.tryFind (fun (typ, _, _) -> typ = CurrentRequest) + Expect.isSome maybeCurrent "There should have been current requests" + let _, _, reqs = Option.get maybeCurrent + Expect.hasCountOf reqs 2u countAll "There should have been two requests" + let first = List.head reqs + Expect.equal first.text "zyx" "The requests should be sorted by updated date descending" + Expect.isTrue (allReqs |> List.exists (fun (typ, _, _) -> typ = PraiseReport)) + "There should have been praise reports" + Expect.isFalse (allReqs |> List.exists (fun (typ, _, _) -> typ = Announcement)) + "There should not have been announcements" + "RequestsByType succeeds and sorts by requestor", + fun reqList -> + let newList = + { reqList with + SmallGroup = + { reqList.SmallGroup with + preferences = { reqList.SmallGroup.preferences with requestSort = SortByRequestor } + } + } + let allReqs = newList.RequestsByType _s + let _, _, reqs = allReqs |> List.find (fun (typ, _, _) -> typ = CurrentRequest) + Expect.hasCountOf reqs 2u countAll "There should have been two requests" + let first = List.head reqs + Expect.equal first.text "abc" "The requests should be sorted by requestor" ] - date = DateTime.Today - listGroup = SmallGroup.empty - showHeader = false - recipients = [] - canEmail = false - } - |> f - yield! testFixture withRequestList [ - "asHtml succeeds without header or as-of date", - fun reqList -> - let htmlList = { reqList with listGroup = { reqList.listGroup with name = "Test HTML Group" } } - let html = htmlList.asHtml _s - Expect.equal -1 (html.IndexOf "Test HTML Group") "The small group name should not have existed (no header)" - let curReqHeading = - [ "" - "" - "
" - "    Current Requests   
" - ] - |> String.concat "" - Expect.stringContains html curReqHeading "Heading for category \"Current Requests\" not found" - let curReqHtml = - [ "
    " - "
  • " - "Zeb — zyx
  • " - "
  • " - "Aaron — abc
" - ] - |> String.concat "" - Expect.stringContains html curReqHtml "Expected HTML for \"Current Requests\" requests not found" - let praiseHeading = - [ "" - "" - "
" - "    Praise Reports   
" - ] - |> String.concat "" - Expect.stringContains html praiseHeading "Heading for category \"Praise Reports\" not found" - let praiseHtml = - [ "
    " - "
  • " - "nmo
" - ] - |> String.concat "" - Expect.stringContains html praiseHtml "Expected HTML for \"Praise Reports\" requests not found" - "asHtml succeeds with header", - fun reqList -> - let htmlList = - { reqList with - listGroup = { reqList.listGroup with name = "Test HTML Group" } - showHeader = true - } - let html = htmlList.asHtml _s - let lstHeading = - [ "
" - "Prayer Requests
" - "Test HTML Group
" - htmlList.date.ToString "MMMM d, yyyy" - "

" - ] - |> String.concat "" - Expect.stringContains html lstHeading "Expected HTML for the list heading not found" - // spot check; without header test tests this exhaustively - Expect.stringContains html "Zeb — zyx" "Expected requests not found" - "asHtml succeeds with short as-of date", - fun reqList -> - let htmlList = - { reqList with - listGroup = - { reqList.listGroup with - preferences = { reqList.listGroup.preferences with asOfDateDisplay = ShortDate } - } - } - let html = htmlList.asHtml _s - let expected = - htmlList.requests.[0].updatedDate.ToShortDateString () - |> sprintf "Zeb — zyx  (as of %s)" - // spot check; if one request has it, they all should - Expect.stringContains html expected "Expected short as-of date not found" - "asHtml succeeds with long as-of date", - fun reqList -> - let htmlList = - { reqList with - listGroup = - { reqList.listGroup with - preferences = { reqList.listGroup.preferences with asOfDateDisplay = LongDate } - } - } - let html = htmlList.asHtml _s - let expected = - htmlList.requests.[0].updatedDate.ToLongDateString () - |> sprintf "Zeb — zyx  (as of %s)" - // spot check; if one request has it, they all should - Expect.stringContains html expected "Expected long as-of date not found" - "asText succeeds with no as-of date", - fun reqList -> - let textList = { reqList with listGroup = { reqList.listGroup with name = "Test Group" } } - let text = textList.asText _s - Expect.stringContains text (textList.listGroup.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 "--------------------\n CURRENT REQUESTS\n--------------------\n" - "Heading for category \"Current Requests\" not found" - Expect.stringContains text " + Zeb - zyx\n" "First request not found" - Expect.stringContains text " - Aaron - abc\n \n" "Second request not found; should have been end of category" - Expect.stringContains text "------------------\n PRAISE REPORTS\n------------------\n" - "Heading for category \"Praise Reports\" not found" - Expect.stringContains text " + nmo\n \n" "Last request not found" - "asText succeeds with short as-of date", - fun reqList -> - let textList = - { reqList with - listGroup = - { reqList.listGroup with - preferences = { reqList.listGroup.preferences with asOfDateDisplay = ShortDate } - } - } - let text = textList.asText _s - let expected = - textList.requests.[0].updatedDate.ToShortDateString () - |> 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" - "asText succeeds with long as-of date", - fun reqList -> - let textList = - { reqList with - listGroup = - { reqList.listGroup with - preferences = { reqList.listGroup.preferences with asOfDateDisplay = LongDate } - } - } - let text = textList.asText _s - let expected = - textList.requests.[0].updatedDate.ToLongDateString () - |> 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" - "isNew succeeds for both old and new requests", - fun reqList -> - let reqs = reqList.requestsInCategory CurrentRequest - Expect.hasCountOf reqs 2u countAll "There should have been two requests" - Expect.isTrue (reqList.isNew (List.head reqs)) "The first request should have been new" - Expect.isFalse (reqList.isNew (List.last reqs)) "The second request should not have been new" - "requestsInCategory succeeds when requests exist", - fun reqList -> - let reqs = reqList.requestsInCategory CurrentRequest - Expect.hasCountOf reqs 2u countAll "There should have been two requests" - let first = List.head reqs - Expect.equal first.text "zyx" "The requests should be sorted by updated date descending" - "requestsInCategory succeeds when requests do not exist", - fun reqList -> - Expect.isEmpty (reqList.requestsInCategory Announcement) "There should have been no \"Announcement\" requests" - "requestsInCategory succeeds and sorts by requestor", - fun reqList -> - let newList = - { reqList with - listGroup = - { reqList.listGroup with - preferences = { reqList.listGroup.preferences with requestSort = SortByRequestor } - } - } - let reqs = newList.requestsInCategory CurrentRequest - Expect.hasCountOf reqs 2u countAll "There should have been two requests" - let first = List.head reqs - Expect.equal first.text "abc" "The requests should be sorted by requestor" - ] ] [] let userLogOnTests = - testList "UserLogOn" [ - test "empty is as expected" { - let mt = UserLogOn.empty - Expect.equal mt.emailAddress "" "The e-mail address should be blank" - Expect.equal mt.password "" "The password should be blank" - Expect.equal mt.smallGroupId Guid.Empty "The small group ID should be an empty GUID" - Expect.isNone mt.rememberMe "Remember Me should be None" - Expect.isNone mt.redirectUrl "Redirect URL should be None" - } + testList "UserLogOn" [ + test "empty is as expected" { + let mt = UserLogOn.empty + Expect.equal mt.Email "" "The e-mail address should be blank" + Expect.equal mt.Password "" "The password should be blank" + Expect.equal mt.SmallGroupId Guid.Empty "The small group ID should be an empty GUID" + Expect.isNone mt.RememberMe "Remember Me should be None" + Expect.isNone mt.RedirectUrl "Redirect URL should be None" + } ] [] let userMessageTests = - testList "UserMessage" [ - test "Error is constructed properly" { - let msg = UserMessage.error - Expect.equal msg.level "ERROR" "Incorrect message level" - Expect.equal msg.text HtmlString.Empty "Text should have been blank" - Expect.isNone msg.description "Description should have been None" - } - test "Warning is constructed properly" { - let msg = UserMessage.warning - Expect.equal msg.level "WARNING" "Incorrect message level" - Expect.equal msg.text HtmlString.Empty "Text should have been blank" - Expect.isNone msg.description "Description should have been None" - } - test "Info is constructed properly" { - let msg = UserMessage.info - Expect.equal msg.level "Info" "Incorrect message level" - Expect.equal msg.text HtmlString.Empty "Text should have been blank" - Expect.isNone msg.description "Description should have been None" - } + testList "UserMessage" [ + test "Error is constructed properly" { + let msg = UserMessage.error + Expect.equal msg.Level Error "Incorrect message level" + Expect.equal msg.Text HtmlString.Empty "Text should have been blank" + Expect.isNone msg.Description "Description should have been None" + } + test "Warning is constructed properly" { + let msg = UserMessage.warning + Expect.equal msg.Level Warning "Incorrect message level" + Expect.equal msg.Text HtmlString.Empty "Text should have been blank" + Expect.isNone msg.Description "Description should have been None" + } + test "Info is constructed properly" { + let msg = UserMessage.info + Expect.equal msg.Level Info "Incorrect message level" + Expect.equal msg.Text HtmlString.Empty "Text should have been blank" + Expect.isNone msg.Description "Description should have been None" + } ] diff --git a/src/PrayerTracker.UI/Church.fs b/src/PrayerTracker.UI/Church.fs index a235f7f..28ec25c 100644 --- a/src/PrayerTracker.UI/Church.fs +++ b/src/PrayerTracker.UI/Church.fs @@ -6,45 +6,50 @@ open PrayerTracker.ViewModels /// View for the church edit page let edit (m : EditChurch) ctx vi = - let pageTitle = match m.isNew () with true -> "Add a New Church" | false -> "Edit Church" + let pageTitle = match m.IsNew with true -> "Add a New Church" | false -> "Edit Church" let s = I18N.localizer.Force () [ form [ _action "/web/church/save"; _method "post"; _class "pt-center-columns" ] [ style [ _scoped ] [ rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }" ] csrfToken ctx - input [ _type "hidden"; _name "churchId"; _value (flatGuid m.churchId) ] + input [ _type "hidden"; _name (nameof m.ChurchId); _value (flatGuid m.ChurchId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "name" ] [ locStr s["Church Name"] ] - input [ _type "text"; _name "name"; _id "name"; _required; _autofocus; _value m.name ] + input [ _type "text"; _name (nameof m.Name); _id "name"; _required; _autofocus; _value m.Name ] ] div [ _class "pt-field" ] [ label [ _for "City"] [ locStr s["City"] ] - input [ _type "text"; _name "city"; _id "city"; _required; _value m.city ] + input [ _type "text"; _name (nameof m.City); _id "city"; _required; _value m.City ] ] div [ _class "pt-field" ] [ - label [ _for "ST" ] [ locStr s["State"] ] - input [ _type "text"; _name "st"; _id "st"; _required; _minlength "2"; _maxlength "2"; _value m.st ] + label [ _for "state" ] [ locStr s["State or Province"] ] + input [ _type "text" + _name (nameof m.State) + _id "state" + _required + _minlength "2"; _maxlength "2" + _value m.State ] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-checkbox-field" ] [ input [ _type "checkbox" - _name "hasInterface" + _name (nameof m.HasInterface) _id "hasInterface" _value "True" - match m.hasInterface with Some x when x -> _checked | _ -> () ] + if defaultArg m.HasInterface false then _checked ] label [ _for "hasInterface" ] [ locStr s["Has an interface with Virtual Prayer Room"] ] ] ] div [ _class "pt-field-row pt-fadeable"; _id "divInterfaceAddress" ] [ div [ _class "pt-field" ] [ label [ _for "interfaceAddress" ] [ locStr s["VPR Interface URL"] ] - input - [ _type "url"; _name "interfaceAddress"; _id "interfaceAddress"; - _value (match m.interfaceAddress with Some ia -> ia | None -> "") - ] + input [ _type "url" + _name (nameof m.InterfaceAddress) + _id "interfaceAddress"; + _value (defaultArg m.InterfaceAddress "") ] ] ] div [ _class "pt-field-row" ] [ submit [] "save" s["Save Church"] ] diff --git a/src/PrayerTracker.UI/Home.fs b/src/PrayerTracker.UI/Home.fs index 06abe48..654eed5 100644 --- a/src/PrayerTracker.UI/Home.fs +++ b/src/PrayerTracker.UI/Home.fs @@ -37,7 +37,7 @@ let error code vi = _alt $"""%A{s["PrayerTracker"]} %A{s["from Bit Badger Solutions"]}""" _title $"""%A{s["PrayerTracker"]} %A{s["from Bit Badger Solutions"]}""" _style "vertical-align:text-bottom;" ] - str vi.version + str vi.Version ] ] |> div [] diff --git a/src/PrayerTracker.UI/Layout.fs b/src/PrayerTracker.UI/Layout.fs index cc3ae4b..38e178d 100644 --- a/src/PrayerTracker.UI/Layout.fs +++ b/src/PrayerTracker.UI/Layout.fs @@ -20,7 +20,7 @@ module Navigation = let s = I18N.localizer.Force () let menuSpacer = rawText "  " let leftLinks = [ - match m.user with + match m.User with | Some u -> li [ _class "dropdown" ] [ a [ _class "dropbtn"; _role "button"; _aria "label" s["Requests"].Value; _title s["Requests"].Value ] @@ -56,7 +56,7 @@ module Navigation = ] ] | None -> - match m.group with + match m.Group with | Some _ -> li [] [ a [ _href "/web/prayer-requests/view" @@ -91,9 +91,9 @@ module Navigation = ] ] let rightLinks = - match m.group with + match m.Group with | Some _ -> [ - match m.user with + match m.User with | Some _ -> li [] [ a [ _href "/web/user/password" @@ -137,9 +137,9 @@ module Navigation = rawText "   •   " a [ _href "/web/language/es" ] [ locStr s["Cambie a Español"] ] ] - match m.group with + match m.Group with | Some g ->[ - match m.user with + match m.User with | Some u -> span [ _class "u" ] [ locStr s["Currently Logged On"] ] rawText "   " @@ -151,7 +151,7 @@ module Navigation = rawText "  " icon "group" space - match m.user with + match m.User with | Some _ -> a [ _href "/web/small-group" ] [ strong [] [ str g.name ] ] | None -> strong [] [ str g.name ] rawText "  " @@ -190,9 +190,9 @@ let private htmlHead m pageTitle = meta [ _charset "UTF-8" ] title [] [ locStr pageTitle; titleSep; locStr s["PrayerTracker"] ] yield! commonHead - for cssFile in m.style do + for cssFile in m.Style do link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ] - for jsFile in m.script do + for jsFile in m.Script do script [ _src $"/js/{jsFile}.js" ] [] ] @@ -207,25 +207,25 @@ let private helpLink link = /// Render the page title, and optionally a help link let private renderPageTitle m pageTitle = h2 [ _id "pt-page-title" ] [ - match m.helpLink with Some link -> Help.fullLink (langCode ()) link |> helpLink | None -> () + match m.HelpLink with Some link -> Help.fullLink (langCode ()) link |> helpLink | None -> () locStr pageTitle ] /// Render the messages that may need to be displayed to the user let private messages m = let s = I18N.localizer.Force () - m.messages + m.Messages |> List.map (fun msg -> - table [ _class $"pt-msg {msg.level.ToLower ()}" ] [ + table [ _class $"pt-msg {MessageLevel.toCssClass msg.Level}" ] [ tr [] [ td [] [ - match msg.level with - | "Info" -> () + match msg.Level with + | Info -> () | lvl -> - strong [] [ locStr s[lvl] ] + strong [] [ locStr s[MessageLevel.toString lvl] ] rawText " » " - rawText msg.text.Value - match msg.description with + rawText msg.Text.Value + match msg.Description with | Some desc -> br [] div [ _class "description" ] [ rawText desc.Value ] @@ -238,7 +238,7 @@ let private messages m = let private htmlFooter m = let s = I18N.localizer.Force () let imgText = sprintf "%O %O" s["PrayerTracker"] s["from Bit Badger Solutions"] - let resultTime = TimeSpan(DateTime.Now.Ticks - m.requestStart).TotalSeconds + let resultTime = TimeSpan(DateTime.Now.Ticks - m.RequestStart).TotalSeconds footer [] [ div [ _id "pt-legal" ] [ a [ _href "/web/legal/privacy-policy" ] [ locStr s["Privacy Policy"] ] @@ -255,7 +255,7 @@ let private htmlFooter m = div [ _id "pt-footer" ] [ a [ _href "/web/"; _style "line-height:28px;" ] [ img [ _src $"""/img/%O{s["footer_en"]}.png"""; _alt imgText; _title imgText ] ] - str m.version + str m.Version space i [ _title s["This page loaded in {0:N3} seconds", resultTime].Value; _class "material-icons md-18" ] [ str "schedule" ] diff --git a/src/PrayerTracker.UI/PrayerRequest.fs b/src/PrayerTracker.UI/PrayerRequest.fs index 3cca333..3efe208 100644 --- a/src/PrayerTracker.UI/PrayerRequest.fs +++ b/src/PrayerTracker.UI/PrayerRequest.fs @@ -13,35 +13,35 @@ open PrayerTracker.ViewModels /// View for the prayer request edit page let edit (m : EditRequest) today ctx vi = let s = I18N.localizer.Force () - let pageTitle = if m.isNew () then "Add a New Request" else "Edit Request" + let pageTitle = if m.IsNew then "Add a New Request" else "Edit Request" [ form [ _action "/web/prayer-request/save"; _method "post"; _class "pt-center-columns" ] [ csrfToken ctx - input [ _type "hidden"; _name "requestId"; _value (flatGuid m.requestId) ] + input [ _type "hidden"; _name (nameof m.RequestId); _value (flatGuid m.RequestId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "requestType" ] [ locStr s["Request Type"] ] + label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ] ReferenceList.requestTypeList s |> Seq.ofList |> Seq.map (fun (typ, desc) -> typ.code, desc.Value) - |> selectList "requestType" m.requestType [ _required; _autofocus ] + |> selectList (nameof m.RequestType) m.RequestType [ _required; _autofocus ] ] div [ _class "pt-field" ] [ label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ] input [ _type "text" - _name "requestor" + _name (nameof m.Requestor) _id "requestor" - _value (match m.requestor with Some x -> x | None -> "") ] + _value (defaultArg m.Requestor "") ] ] - if m.isNew () then + if m.IsNew then div [ _class "pt-field" ] [ label [ _for "enteredDate" ] [ locStr s["Date"] ] - input [ _type "date"; _name "enteredDate"; _id "enteredDate"; _placeholder today ] + input [ _type "date"; _name (nameof m.EnteredDate); _id "enteredDate"; _placeholder today ] ] else div [ _class "pt-field" ] [ div [ _class "pt-checkbox-field" ] [ br [] - input [ _type "checkbox"; _name "skipDateUpdate"; _id "skipDateUpdate"; _value "True" ] + input [ _type "checkbox"; _name (nameof m.SkipDateUpdate); _id "skipDateUpdate"; _value "True" ] label [ _for "skipDateUpdate" ] [ locStr s["Check to not update the date"] ] br [] small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ] @@ -51,11 +51,11 @@ let edit (m : EditRequest) today ctx vi = div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [] [ locStr s["Expiration"] ] - ReferenceList.expirationList s ((m.isNew >> not) ()) + ReferenceList.expirationList s (not m.IsNew) |> List.map (fun exp -> let radioId = $"expiration_{fst exp}" span [ _class "text-nowrap" ] [ - radio "expiration" radioId (fst exp) m.expiration + radio (nameof m.Expiration) radioId (fst exp) m.Expiration label [ _for radioId ] [ locStr (snd exp) ] rawText "     " ]) @@ -65,7 +65,7 @@ let edit (m : EditRequest) today ctx vi = div [ _class "pt-field-row" ] [ div [ _class "pt-field pt-editor" ] [ label [ _for "text" ] [ locStr s["Request"] ] - textarea [ _name "text"; _id "text" ] [ str m.text ] + textarea [ _name (nameof m.Text); _id "text" ] [ str m.Text ] ] ] div [ _class "pt-field-row" ] [ submit [] "save" s["Save Request"] ] @@ -78,9 +78,9 @@ let edit (m : EditRequest) today ctx vi = /// View for the request e-mail results page let email m vi = let s = I18N.localizer.Force () - let pageTitle = $"""{s["Prayer Requests"].Value} • {m.listGroup.name}""" - let prefs = m.listGroup.preferences - let addresses = String.Join (", ", m.recipients |> List.map (fun mbr -> $"{mbr.memberName} <{mbr.email}>")) + let pageTitle = $"""{s["Prayer Requests"].Value} • {m.SmallGroup.name}""" + let prefs = m.SmallGroup.preferences + let addresses = m.Recipients |> List.map (fun mbr -> $"{mbr.memberName} <{mbr.email}>") |> String.concat ", " [ p [ _style $"font-family:{prefs.listFonts};font-size:%i{prefs.textFontSize}pt;" ] [ locStr s["The request list was sent to the following people, via individual e-mails"] rawText ":" @@ -88,11 +88,11 @@ let email m vi = small [] [ str addresses ] ] span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ] - div [ _class "pt-email-canvas" ] [ rawText (m.asHtml s) ] + div [ _class "pt-email-canvas" ] [ rawText (m.AsHtml s) ] br [] br [] span [ _class "pt-email-heading" ] [ locStr s["Plain-Text Format"]; rawText ":" ] - div [ _class "pt-email-canvas" ] [ pre [] [ str (m.asText s) ] ] + div [ _class "pt-email-canvas" ] [ pre [] [ str (m.AsText s) ] ] ] |> Layout.Content.standard |> Layout.standard vi pageTitle @@ -101,7 +101,7 @@ let email m vi = /// View for a small group's public prayer request list let list (m : RequestList) vi = [ br [] - I18N.localizer.Force () |> (m.asHtml >> rawText) + I18N.localizer.Force () |> (m.AsHtml >> rawText) ] |> Layout.Content.standard |> Layout.standard vi "View Request List" @@ -154,24 +154,24 @@ let lists (groups : SmallGroup list) vi = /// View for the prayer request maintenance page -let maintain m (ctx : HttpContext) vi = - let s = I18N.localizer.Force () - let l = I18N.forView "Requests/Maintain" - use sw = new StringWriter () - let raw = rawLocText sw - let now = m.smallGroup.localDateNow (ctx.GetService ()) - let typs = ReferenceList.requestTypeList s |> Map.ofList +let maintain (m : MaintainRequests) (ctx : HttpContext) vi = + let s = I18N.localizer.Force () + let l = I18N.forView "Requests/Maintain" + use sw = new StringWriter () + let raw = rawLocText sw + let now = m.SmallGroup.localDateNow (ctx.GetService ()) + let types = ReferenceList.requestTypeList s |> Map.ofList let updReq (req : PrayerRequest) = - if req.updateRequired now m.smallGroup.preferences.daysToExpire m.smallGroup.preferences.longTermUpdateWeeks then + if req.updateRequired now m.SmallGroup.preferences.daysToExpire m.SmallGroup.preferences.longTermUpdateWeeks then "pt-request-update" else "" |> _class let reqExp (req : PrayerRequest) = - _class (if req.isExpired now m.smallGroup.preferences.daysToExpire then "pt-request-expired" else "") + _class (if req.isExpired now m.SmallGroup.preferences.daysToExpire then "pt-request-expired" else "") /// Iterate the sequence once, before we render, so we can get the count of it at the top of the table let requests = - m.requests - |> Seq.map (fun req -> + m.Requests + |> List.map (fun req -> let reqId = flatGuid req.prayerRequestId let reqText = htmlToPlainText req.text let delAction = $"/web/prayer-request/{reqId}/delete" @@ -187,7 +187,7 @@ let maintain m (ctx : HttpContext) vi = td [] [ a [ _href $"/web/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] [ icon "edit" ] - if req.isExpired now m.smallGroup.preferences.daysToExpire then + if req.isExpired now m.SmallGroup.preferences.daysToExpire then a [ _href $"/web/prayer-request/{reqId}/restore" _title l["Restore This Inactive Request"].Value ] [ icon "visibility" ] @@ -202,7 +202,7 @@ let maintain m (ctx : HttpContext) vi = td [ updReq req ] [ str (req.updatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture)) ] - td [] [ locStr typs[req.requestType] ] + td [] [ locStr types[req.requestType] ] td [ reqExp req ] [ str (match req.requestor with Some r -> r | None -> " ") ] td [] [ match reqText.Length with @@ -218,7 +218,7 @@ let maintain m (ctx : HttpContext) vi = rawText "       " a [ _href "/web/prayer-requests/view"; _title s["View Prayer Request List"].Value ] [ icon "list"; rawText "  "; locStr s["View Prayer Request List"] ] - match m.searchTerm with + match m.SearchTerm with | Some _ -> rawText "       " a [ _href "/web/prayer-requests"; _title l["Clear Search Criteria"].Value ] @@ -229,7 +229,7 @@ let maintain m (ctx : HttpContext) vi = input [ _type "text" _name "search" _placeholder l["Search requests..."].Value - _value (defaultArg m.searchTerm "") + _value (defaultArg m.SearchTerm "") ] space submit [] "search" s["Search"] @@ -253,54 +253,52 @@ let maintain m (ctx : HttpContext) vi = ] div [ _class "pt-center-text" ] [ br [] - match m.onlyActive with + match m.OnlyActive with | Some true -> raw l["Inactive requests are currently not shown"] br [] a [ _href "/web/prayer-requests/inactive" ] [ raw l["Show Inactive Requests"] ] | _ -> - match Option.isSome m.onlyActive with - | true -> + if defaultArg m.OnlyActive false then raw l["Inactive requests are currently shown"] br [] a [ _href "/web/prayer-requests" ] [ raw l["Do Not Show Inactive Requests"] ] br [] br [] - | false -> () - let srch = [ match m.searchTerm with Some s -> "search", s | None -> () ] - let pg = defaultArg m.pageNbr 1 - let url = - match m.onlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/web/prayer-requests%s" + let search = [ match m.SearchTerm with Some s -> "search", s | None -> () ] + let pg = defaultArg m.PageNbr 1 + let url = + match m.OnlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/web/prayer-requests%s" match pg with | 1 -> () | _ -> // button (_type "submit" :: attrs) [ icon ico; rawText "  "; locStr text ] - let withPage = match pg with 2 -> srch | _ -> ("page", string (pg - 1)) :: srch + let withPage = match pg with 2 -> search | _ -> ("page", string (pg - 1)) :: search a [ _href (makeUrl url withPage) ] [ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ] rawText "     " - match requests.Length = m.smallGroup.preferences.pageSize with + match requests.Length = m.SmallGroup.preferences.pageSize with | true -> - a [ _href (makeUrl url (("page", string (pg + 1)) :: srch)) ] + a [ _href (makeUrl url (("page", string (pg + 1)) :: search)) ] [ raw l["Next Page"]; space; icon "keyboard_arrow_right" ] | false -> () ] form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] ] |> Layout.Content.wide - |> Layout.standard vi (match m.searchTerm with Some _ -> "Search Results" | None -> "Maintain Requests") + |> Layout.standard vi (match m.SearchTerm with Some _ -> "Search Results" | None -> "Maintain Requests") /// View for the printable prayer request list let print m version = let s = I18N.localizer.Force () - let pageTitle = $"""{s["Prayer Requests"].Value} • {m.listGroup.name}""" + let pageTitle = $"""{s["Prayer Requests"].Value} • {m.SmallGroup.name}""" let imgAlt = $"""{s["PrayerTracker"].Value} {s["from Bit Badger Solutions"].Value}""" article [] [ - rawText (m.asHtml s) + rawText (m.AsHtml s) br [] hr [] - div [ _style $"font-size:70%%;font-family:{m.listGroup.preferences.listFonts};" ] [ + div [ _style $"font-size:70%%;font-family:{m.SmallGroup.preferences.listFonts};" ] [ img [ _src $"""/img/{s["footer_en"].Value}.png""" _style "vertical-align:text-bottom;" _alt imgAlt @@ -315,21 +313,21 @@ let print m version = /// View for the prayer request list let view m vi = let s = I18N.localizer.Force () - let pageTitle = $"""{s["Prayer Requests"].Value} • {m.listGroup.name}""" + let pageTitle = $"""{s["Prayer Requests"].Value} • {m.SmallGroup.name}""" let spacer = rawText "       " - let dtString = m.date.ToString "yyyy-MM-dd" + let dtString = m.Date.ToString "yyyy-MM-dd" [ div [ _class "pt-center-text" ] [ br [] a [ _class "pt-icon-link" _href $"/web/prayer-requests/print/{dtString}" _title s["View Printable"].Value ] [ icon "print"; rawText "  "; locStr s["View Printable"] ] - if m.canEmail then + if m.CanEmail then spacer - if m.date.DayOfWeek <> DayOfWeek.Sunday then + if m.Date.DayOfWeek <> DayOfWeek.Sunday then let rec findSunday (date : DateTime) = if date.DayOfWeek = DayOfWeek.Sunday then date else findSunday (date.AddDays 1.) - let sunday = findSunday m.date + let sunday = findSunday m.Date a [ _class "pt-icon-link" _href $"""/web/prayer-requests/view/{sunday.ToString "yyyy-MM-dd"}""" _title s["List for Next Sunday"].Value ] [ @@ -349,7 +347,7 @@ let view m vi = ] ] br [] - rawText (m.asHtml s) + rawText (m.AsHtml s) ] |> Layout.Content.standard |> Layout.standard vi pageTitle diff --git a/src/PrayerTracker.UI/PrayerTracker.UI.fsproj b/src/PrayerTracker.UI/PrayerTracker.UI.fsproj index 4253ebc..47d5a02 100644 --- a/src/PrayerTracker.UI/PrayerTracker.UI.fsproj +++ b/src/PrayerTracker.UI/PrayerTracker.UI.fsproj @@ -18,14 +18,15 @@
- + - + + diff --git a/src/PrayerTracker.UI/Resources/Common.es.resx b/src/PrayerTracker.UI/Resources/Common.es.resx index cfb46e0..dd12103 100644 --- a/src/PrayerTracker.UI/Resources/Common.es.resx +++ b/src/PrayerTracker.UI/Resources/Common.es.resx @@ -822,4 +822,7 @@ como de + + Estado o Provincia + \ No newline at end of file diff --git a/src/PrayerTracker.UI/SmallGroup.fs b/src/PrayerTracker.UI/SmallGroup.fs index a4d88e7..07188d9 100644 --- a/src/PrayerTracker.UI/SmallGroup.fs +++ b/src/PrayerTracker.UI/SmallGroup.fs @@ -10,13 +10,14 @@ open PrayerTracker.ViewModels /// View for the announcement page let announcement isAdmin ctx vi = let s = I18N.localizer.Force () + let m = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None } let reqTypes = ReferenceList.requestTypeList s [ form [ _action "/web/small-group/announcement/send"; _method "post"; _class "pt-center-columns" ] [ csrfToken ctx div [ _class "pt-field-row" ] [ div [ _class "pt-field pt-editor" ] [ label [ _for "text" ] [ locStr s["Announcement Text"] ] - textarea [ _name "text"; _id "text"; _autofocus ] [] + textarea [ _name (nameof m.Text); _id "text"; _autofocus ] [] ] ] if isAdmin then @@ -24,27 +25,27 @@ let announcement isAdmin ctx vi = div [ _class "pt-field" ] [ label [] [ locStr s["Send Announcement to"]; rawText ":" ] div [ _class "pt-center-text" ] [ - radio "sendToClass" "sendY" "Y" "Y" + radio (nameof m.SendToClass) "sendY" "Y" "Y" label [ _for "sendY" ] [ locStr s["This Group"]; rawText "     " ] - radio "sendToClass" "sendN" "N" "Y" + radio (nameof m.SendToClass) "sendN" "N" "Y" label [ _for "sendN" ] [ locStr s["All {0} Users", s["PrayerTracker"]] ] ] ] ] - else input [ _type "hidden"; _name "sendToClass"; _value "Y" ] + else input [ _type "hidden"; _name (nameof m.SendToClass); _value "Y" ] div [ _class "pt-field-row pt-fadeable pt-shown"; _id "divAddToList" ] [ div [ _class "pt-checkbox-field" ] [ - input [ _type "checkbox"; _name "addToRequestList"; _id "addToRequestList"; _value "True" ] + input [ _type "checkbox"; _name (nameof m.AddToRequestList); _id "addToRequestList"; _value "True" ] label [ _for "addToRequestList" ] [ locStr s["Add to Request List"] ] ] ] div [ _class "pt-field-row pt-fadeable"; _id "divCategory" ] [ div [ _class "pt-field" ] [ - label [ _for "requestType" ] [ locStr s["Request Type"] ] + label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ] reqTypes |> Seq.ofList |> Seq.map (fun (typ, desc) -> typ.code, desc.Value) - |> selectList "requestType" "Announcement" [] + |> selectList (nameof m.RequestType) Announcement.code [] ] ] div [ _class "pt-field-row" ] [ submit [] "send" s["Send Announcement"] ] @@ -59,12 +60,12 @@ let announcement isAdmin ctx vi = let announcementSent (m : Announcement) vi = let s = I18N.localizer.Force () [ span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ] - div [ _class "pt-email-canvas" ] [ rawText m.text ] + div [ _class "pt-email-canvas" ] [ rawText m.Text ] br [] br [] span [ _class "pt-email-heading" ] [ locStr s["Plain-Text Format"]; rawText ":" ] - div [ _class "pt-email-canvas" ] [ pre [] [ str (m.plainText ()) ] ] - ] + div [ _class "pt-email-canvas" ] [ pre [] [ str m.PlainText ] ] + ] |> Layout.Content.standard |> Layout.standard vi "Announcement Sent" @@ -72,24 +73,24 @@ let announcementSent (m : Announcement) vi = /// View for the small group add/edit page let edit (m : EditSmallGroup) (churches : Church list) ctx vi = let s = I18N.localizer.Force () - let pageTitle = match m.isNew () with true -> "Add a New Group" | false -> "Edit Group" + let pageTitle = if m.IsNew then "Add a New Group" else "Edit Group" form [ _action "/web/small-group/save"; _method "post"; _class "pt-center-columns" ] [ csrfToken ctx - input [ _type "hidden"; _name "smallGroupId"; _value (flatGuid m.smallGroupId) ] + input [ _type "hidden"; _name (nameof m.SmallGroupId); _value (flatGuid m.SmallGroupId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "name" ] [ locStr s["Group Name"] ] - input [ _type "text"; _name "name"; _id "name"; _value m.name; _required; _autofocus ] + input [ _type "text"; _name (nameof m.Name); _id "name"; _value m.Name; _required; _autofocus ] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "churchId" ] [ locStr s["Church"] ] + label [ _for (nameof m.ChurchId) ] [ locStr s["Church"] ] seq { "", selectDefault s["Select Church"].Value yield! churches |> List.map (fun c -> flatGuid c.churchId, c.name) } - |> selectList "churchId" (flatGuid m.churchId) [ _required ] + |> selectList (nameof m.ChurchId) (flatGuid m.ChurchId) [ _required ] ] ] div [ _class "pt-field-row" ] [ submit [] "save" s["Save Group"] ] @@ -100,29 +101,29 @@ let edit (m : EditSmallGroup) (churches : Church list) ctx vi = /// View for the member edit page -let editMember (m : EditMember) (typs : (string * LocalizedString) seq) ctx vi = +let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi = let s = I18N.localizer.Force () - let pageTitle = match m.isNew () with true -> "Add a New Group Member" | false -> "Edit Group Member" + let pageTitle = if m.IsNew then "Add a New Group Member" else "Edit Group Member" form [ _action "/web/small-group/member/save"; _method "post"; _class "pt-center-columns" ] [ - style [ _scoped ] [ rawText "#memberName { width: 15rem; } #emailAddress { width: 20rem; }" ] + style [ _scoped ] [ rawText "#name { width: 15rem; } #email { width: 20rem; }" ] csrfToken ctx - input [ _type "hidden"; _name "memberId"; _value (flatGuid m.memberId) ] + input [ _type "hidden"; _name (nameof m.MemberId); _value (flatGuid m.MemberId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "memberName" ] [ locStr s["Member Name"] ] - input [ _type "text"; _name "memberName"; _id "memberName"; _required; _autofocus; _value m.memberName ] + label [ _for "name" ] [ locStr s["Member Name"] ] + input [ _type "text"; _name (nameof m.Name); _id "name"; _required; _autofocus; _value m.Name ] ] div [ _class "pt-field" ] [ - label [ _for "emailAddress" ] [ locStr s["E-mail Address"] ] - input [ _type "email"; _name "emailAddress"; _id "emailAddress"; _required; _value m.emailAddress ] + label [ _for "email" ] [ locStr s["E-mail Address"] ] + input [ _type "email"; _name (nameof m.Email); _id "email"; _required; _value m.Email ] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "emailType" ] [ locStr s["E-mail Format"] ] - typs + label [ _for (nameof m.Format) ] [ locStr s["E-mail Format"] ] + types |> Seq.map (fun typ -> fst typ, (snd typ).Value) - |> selectList "emailType" m.emailType [] + |> selectList (nameof m.Format) m.Format [] ] ] div [ _class "pt-field-row" ] [ submit [] "save" s["Save"] ] @@ -133,32 +134,36 @@ let editMember (m : EditMember) (typs : (string * LocalizedString) seq) ctx vi = /// View for the small group log on page -let logOn (grps : SmallGroup list) grpId ctx vi = +let logOn (groups : SmallGroup list) grpId ctx vi = let s = I18N.localizer.Force () + let m = { SmallGroupId = System.Guid.Empty; Password = ""; RememberMe = None } [ form [ _action "/web/small-group/log-on/submit"; _method "post"; _class "pt-center-columns" ] [ csrfToken ctx div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "smallGroupId" ] [ locStr s["Group"] ] + label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ] seq { - match grps.Length with + match groups.Length with | 0 -> "", s["There are no classes with passwords defined"].Value | _ -> "", selectDefault s["Select Group"].Value yield! - grps + groups |> List.map (fun grp -> flatGuid grp.smallGroupId, $"{grp.church.name} | {grp.name}") } - |> selectList "smallGroupId" grpId [ _required ] + |> selectList (nameof m.SmallGroupId) grpId [ _required ] ] div [ _class "pt-field" ] [ label [ _for "password" ] [ locStr s["Password"] ] - input [ _type "password"; _name "password"; _id "password"; _required; + input [ _type "password" + _name (nameof m.Password) + _id "password" + _required; _placeholder (s["Case-Sensitive"].Value.ToLower ()) ] ] ] div [ _class "pt-checkbox-field" ] [ - input [ _type "checkbox"; _name "rememberMe"; _id "rememberMe"; _value "True" ] + input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ] label [ _for "rememberMe" ] [ locStr s["Remember Me"] ] br [] small [] [ em [] [ str (s["Requires Cookies"].Value.ToLower ()) ] ] @@ -172,10 +177,10 @@ let logOn (grps : SmallGroup list) grpId ctx vi = /// View for the small group maintenance page -let maintain (grps : SmallGroup list) ctx vi = +let maintain (groups : SmallGroup list) ctx vi = let s = I18N.localizer.Force () let grpTbl = - match grps with + match groups with | [] -> space | _ -> table [ _class "pt-table pt-action-table" ] [ @@ -187,7 +192,7 @@ let maintain (grps : SmallGroup list) ctx vi = th [] [ locStr s["Time Zone"] ] ] ] - grps + groups |> List.map (fun g -> let grpId = flatGuid g.smallGroupId let delAction = $"/web/small-group/{grpId}/delete" @@ -218,7 +223,7 @@ let maintain (grps : SmallGroup list) ctx vi = br [] br [] ] - tableSummary grps.Length s + tableSummary groups.Length s grpTbl form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] ] @@ -227,10 +232,10 @@ let maintain (grps : SmallGroup list) ctx vi = /// View for the member maintenance page -let members (mbrs : Member list) (emailTyps : Map) ctx vi = +let members (members : Member list) (emailTyps : Map) ctx vi = let s = I18N.localizer.Force () let mbrTbl = - match mbrs with + match members with | [] -> space | _ -> table [ _class "pt-table pt-action-table" ] [ @@ -242,7 +247,7 @@ let members (mbrs : Member list) (emailTyps : Map) ctx th [] [ locStr s["Format"] ] ] ] - mbrs + members |> List.map (fun mbr -> let mbrId = flatGuid mbr.memberId let delAction = $"/web/small-group/member/{mbrId}/delete" @@ -271,7 +276,7 @@ let members (mbrs : Member list) (emailTyps : Map) ctx br [] br [] ] - tableSummary mbrs.Length s + tableSummary members.Length s mbrTbl form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] ] @@ -283,7 +288,7 @@ let members (mbrs : Member list) (emailTyps : Map) ctx let overview m vi = let s = I18N.localizer.Force () let linkSpacer = rawText "  " - let typs = ReferenceList.requestTypeList s |> dict + let types = ReferenceList.requestTypeList s |> dict article [ _class "pt-overview" ] [ section [] [ header [ _role "heading" ] [ @@ -306,16 +311,16 @@ let overview m vi = ] div [] [ p [ _class "pt-center-text" ] [ - strong [] [ str (m.totalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ] + strong [] [ str (m.TotalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ] ] hr [] - for cat in m.activeReqsByCat do + for cat in m.ActiveReqsByType do str (cat.Value.ToString "N0") space - locStr typs[cat.Key] + locStr types[cat.Key] br [] br [] - str (m.allReqs.ToString "N0") + str (m.AllReqs.ToString "N0") space locStr s["Total Requests"] hr [] @@ -332,7 +337,7 @@ let overview m vi = locStr s["Group Members"] ] div [ _class "pt-center-text" ] [ - strong [] [ str (m.totalMbrs.ToString "N0"); space; locStr s["Members"] ] + strong [] [ str (m.TotalMembers.ToString "N0"); space; locStr s["Members"] ] hr [] a [ _href "/web/small-group/members" ] [ icon "email"; linkSpacer; locStr s["Maintain Group Members"] ] ] @@ -350,7 +355,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = use sw = new StringWriter () let raw = rawLocText sw [ form [ _action "/web/small-group/preferences/save"; _method "post"; _class "pt-center-columns" ] [ - style [ _scoped ] [ rawText "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; } #emailFromAddress { width: 20rem; } #listFonts { width: 40rem; } @media screen and (max-width: 40rem) { #listFonts { width: 100%; } }" ] + style [ _scoped ] + [ rawText "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; } #emailFromAddress { width: 20rem; } #fonts { width: 40rem; } @media screen and (max-width: 40rem) { #fonts { width: 100%; } }" ] csrfToken ctx fieldset [] [ legend [] [ strong [] [ icon "date_range"; rawText "  "; locStr s["Dates"] ] ] @@ -358,24 +364,37 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = div [ _class "pt-field" ] [ label [ _for "expireDays" ] [ locStr s["Requests Expire After"] ] span [] [ - input [ _type "number"; _name "expireDays"; _id "expireDays"; _min "1"; _max "30"; _required - _autofocus; _value (string m.expireDays) ] + input [ _type "number" + _name (nameof m.ExpireDays) + _id "expireDays" + _min "1"; _max "30" + _required + _autofocus + _value (string m.ExpireDays) ] space; str (s["Days"].Value.ToLower ()) ] ] div [ _class "pt-field" ] [ label [ _for "daysToKeepNew" ] [ locStr s["Requests “New” For"] ] span [] [ - input [ _type "number"; _name "daysToKeepNew"; _id "daysToKeepNew"; _min "1"; _max "30" - _required; _value (string m.daysToKeepNew) ] + input [ _type "number" + _name (nameof m.DaysToKeepNew) + _id "daysToKeepNew" + _min "1"; _max "30" + _required + _value (string m.DaysToKeepNew) ] space; str (s["Days"].Value.ToLower ()) ] ] div [ _class "pt-field" ] [ label [ _for "longTermUpdateWeeks" ] [ locStr s["Long-Term Requests Alerted for Update"] ] span [] [ - input [ _type "number"; _name "longTermUpdateWeeks"; _id "longTermUpdateWeeks"; _min "1" - _max "30"; _required; _value (string m.longTermUpdateWeeks) ] + input [ _type "number" + _name (nameof m.LongTermUpdateWeeks) + _id "longTermUpdateWeeks" + _min "1"; _max "30" + _required + _value (string m.LongTermUpdateWeeks) ] space; str (s["Weeks"].Value.ToLower ()) ] ] @@ -383,10 +402,10 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = ] fieldset [] [ legend [] [ strong [] [ icon "sort"; rawText "  "; locStr s["Request Sorting"] ] ] - radio "requestSort" "requestSort_D" "D" m.requestSort + radio (nameof m.RequestSort) "requestSort_D" "D" m.RequestSort label [ _for "requestSort_D" ] [ locStr s["Sort by Last Updated Date"] ] rawText "   " - radio "requestSort" "requestSort_R" "R" m.requestSort + radio (nameof m.RequestSort) "requestSort_R" "R" m.RequestSort label [ _for "requestSort_R" ] [ locStr s["Sort by Requestor Name"] ] ] fieldset [] [ @@ -394,17 +413,24 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "emailFromName" ] [ locStr s["From Name"] ] - input [ _type "text"; _name "emailFromName"; _id "emailFromName"; _required; _value m.emailFromName ] + input [ _type "text" + _name (nameof m.EmailFromName) + _id "emailFromName" + _required + _value m.EmailFromName ] ] div [ _class "pt-field" ] [ label [ _for "emailFromAddress" ] [ locStr s["From Address"] ] - input [ _type "email"; _name "emailFromAddress"; _id "emailFromAddress"; _required - _value m.emailFromAddress ] + input [ _type "email" + _name (nameof m.EmailFromAddress) + _id "emailFromAddress" + _required + _value m.EmailFromAddress ] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "defaultEmailType" ] [ locStr s["E-mail Format"] ] + label [ _for (nameof m.DefaultEmailType) ] [ locStr s["E-mail Format"] ] seq { "", selectDefault s["Select"].Value yield! @@ -412,7 +438,7 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = |> Seq.skip 1 |> Seq.map (fun typ -> fst typ, (snd typ).Value) } - |> selectList "defaultEmailType" m.defaultEmailType [ _required ] + |> selectList (nameof m.DefaultEmailType) m.DefaultEmailType [ _required ] ] ] ] @@ -422,19 +448,19 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = div [ _class "pt-field" ] [ label [ _class "pt-center-text" ] [ locStr s["Color of Heading Lines"] ] span [] [ - radio "headingLineType" "headingLineType_Name" "Name" m.headingLineType - label [ _for "headingLineType_Name" ] [ locStr s["Named Color"] ] - namedColorList "headingLineColor" m.headingLineColor - [ _id "headingLineColor_Select" - match m.headingLineColor.StartsWith "#" with true -> _disabled | false -> () ] s + radio (nameof m.LineColorType) "lineColorType_Name" "Name" m.LineColorType + label [ _for "lineColorType_Name" ] [ locStr s["Named Color"] ] + namedColorList (nameof m.LineColor) m.LineColor + [ _id "lineColor_Select" + if m.LineColor.StartsWith "#" then _disabled ] s rawText "    "; str (s["or"].Value.ToUpper ()) - radio "headingLineType" "headingLineType_RGB" "RGB" m.headingLineType - label [ _for "headingLineType_RGB" ] [ locStr s["Custom Color"] ] + radio (nameof m.LineColorType) "lineColorType_RGB" "RGB" m.LineColorType + label [ _for "lineColorType_RGB" ] [ locStr s["Custom Color"] ] input [ _type "color" - _name "headingLineColor" - _id "headingLineColor_Color" - _value m.headingLineColor - match m.headingLineColor.StartsWith "#" with true -> () | false -> _disabled ] + _name (nameof m.LineColor) + _id "lineColor_Color" + _value m.LineColor // TODO: convert to hex or skip if named + if not (m.LineColor.StartsWith "#") then _disabled ] ] ] ] @@ -442,19 +468,19 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = div [ _class "pt-field" ] [ label [ _class "pt-center-text" ] [ locStr s["Color of Heading Text"] ] span [] [ - radio "headingTextType" "headingTextType_Name" "Name" m.headingTextType - label [ _for "headingTextType_Name" ] [ locStr s["Named Color"] ] - namedColorList "headingTextColor" m.headingTextColor - [ _id "headingTextColor_Select" - match m.headingTextColor.StartsWith "#" with true -> _disabled | false -> () ] s + radio (nameof m.HeadingColorType) "headingColorType_Name" "Name" m.HeadingColorType + label [ _for "headingColorType_Name" ] [ locStr s["Named Color"] ] + namedColorList (nameof m.HeadingColor) m.HeadingColor + [ _id "headingColor_Select" + if m.HeadingColor.StartsWith "#" then _disabled ] s rawText "    "; str (s["or"].Value.ToUpper ()) - radio "headingTextType" "headingTextType_RGB" "RGB" m.headingTextType - label [ _for "headingTextType_RGB" ] [ locStr s["Custom Color"] ] + radio (nameof m.HeadingColorType) "headingColorType_RGB" "RGB" m.HeadingColorType + label [ _for "headingColorType_RGB" ] [ locStr s["Custom Color"] ] input [ _type "color" - _name "headingTextColor" - _id "headingTextColor_Color" - _value m.headingTextColor - match m.headingTextColor.StartsWith "#" with true -> () | false -> _disabled ] + _name (nameof m.HeadingColor) + _id "headingColor_Color" + _value m.HeadingColor // TODO: convert to hex or skip if named + if not (m.HeadingColor.StartsWith "#") then _disabled ] ] ] ] @@ -462,19 +488,27 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = fieldset [] [ legend [] [ strong [] [ icon "font_download"; rawText "  "; locStr s["Fonts"] ] ] div [ _class "pt-field" ] [ - label [ _for "listFonts" ] [ locStr s["Fonts** for List"] ] - input [ _type "text"; _name "listFonts"; _id "listFonts"; _required; _value m.listFonts ] + label [ _for "fonts" ] [ locStr s["Fonts** for List"] ] + input [ _type "text"; _name (nameof m.Fonts); _id "fonts"; _required; _value m.Fonts ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "headingFontSize" ] [ locStr s["Heading Text Size"] ] - input [ _type "number"; _name "headingFontSize"; _id "headingFontSize"; _min "8"; _max "24" - _required; _value (string m.headingFontSize) ] + input [ _type "number" + _name (nameof m.HeadingFontSize) + _id "headingFontSize" + _min "8"; _max "24" + _required + _value (string m.HeadingFontSize) ] ] div [ _class "pt-field" ] [ label [ _for "listFontSize" ] [ locStr s["List Text Size"] ] - input [ _type "number"; _name "listFontSize"; _id "listFontSize"; _min "8"; _max "24"; _required - _value (string m.listFontSize) ] + input [ _type "number" + _name (nameof m.ListFontSize) + _id "listFontSize" + _min "8"; _max "24" + _required + _value (string m.ListFontSize) ] ] ] ] @@ -482,48 +516,54 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = legend [] [ strong [] [ icon "settings"; rawText "  "; locStr s["Other Settings"] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "timeZone" ] [ locStr s["Time Zone"] ] + label [ _for (nameof m.TimeZone) ] [ locStr s["Time Zone"] ] seq { "", selectDefault s["Select"].Value yield! tzs |> List.map (fun tz -> tz.timeZoneId, (TimeZones.name tz.timeZoneId s).Value) } - |> selectList "timeZone" m.timeZone [ _required ] + |> selectList (nameof m.TimeZone) m.TimeZone [ _required ] ] ] div [ _class "pt-field" ] [ label [] [ locStr s["Request List Visibility"] ] span [] [ - radio "listVisibility" "viz_Public" (string RequestVisibility.``public``) (string m.listVisibility) + radio (nameof m.Visibility) "viz_Public" (string RequestVisibility.``public``) (string m.Visibility) label [ _for "viz_Public" ] [ locStr s["Public"] ] rawText "  " - radio "listVisibility" "viz_Private" (string RequestVisibility.``private``) - (string m.listVisibility) + radio (nameof m.Visibility) "viz_Private" (string RequestVisibility.``private``) + (string m.Visibility) label [ _for "viz_Private" ] [ locStr s["Private"] ] rawText "  " - radio "listVisibility" "viz_Password" (string RequestVisibility.passwordProtected) - (string m.listVisibility) + radio (nameof m.Visibility) "viz_Password" (string RequestVisibility.passwordProtected) + (string m.Visibility) label [ _for "viz_Password" ] [ locStr s["Password Protected"] ] ] ] - let classSuffix = if m.listVisibility = RequestVisibility.passwordProtected then " pt-show" else "" + let classSuffix = if m.Visibility = RequestVisibility.passwordProtected then " pt-show" else "" div [ _id "divClassPassword"; _class $"pt-field-row pt-fadeable{classSuffix}" ] [ div [ _class "pt-field" ] [ label [ _for "groupPassword" ] [ locStr s["Group Password (Used to Read Online)"] ] - input [ _type "text"; _name "groupPassword"; _id "groupPassword"; - _value (match m.groupPassword with Some x -> x | None -> "") ] + input [ _type "text" + _name (nameof m.GroupPassword) + _id "groupPassword" + _value (defaultArg m.GroupPassword "") ] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "pageSize" ] [ locStr s["Page Size"] ] - input [ _type "number"; _name "pageSize"; _id "pageSize"; _min "10"; _max "255"; _required - _value (string m.pageSize) ] + input [ _type "number" + _name (nameof m.PageSize) + _id "pageSize" + _min "10"; _max "255" + _required + _value (string m.PageSize) ] ] div [ _class "pt-field" ] [ - label [ _for "asOfDate" ] [ locStr s["“As of” Date Display"] ] + label [ _for (nameof m.AsOfDate) ] [ locStr s["“As of” Date Display"] ] ReferenceList.asOfDateList s |> List.map (fun (code, desc) -> code, desc.Value) - |> selectList "asOfDate" m.asOfDate [ _required ] + |> selectList (nameof m.AsOfDate) m.AsOfDate [ _required ] ] ] ] diff --git a/src/PrayerTracker.UI/User.fs b/src/PrayerTracker.UI/User.fs index b8115a9..ee954cc 100644 --- a/src/PrayerTracker.UI/User.fs +++ b/src/PrayerTracker.UI/User.fs @@ -7,11 +7,11 @@ open PrayerTracker.ViewModels /// View for the group assignment page let assignGroups m groups curGroups ctx vi = let s = I18N.localizer.Force () - let pageTitle = sprintf "%s • %A" m.userName s["Assign Groups"] + let pageTitle = sprintf "%s • %A" m.UserName s["Assign Groups"] form [ _action "/web/user/small-groups/save"; _method "post"; _class "pt-center-columns" ] [ csrfToken ctx - input [ _type "hidden"; _name "userId"; _value (flatGuid m.userId) ] - input [ _type "hidden"; _name "userName"; _value m.userName ] + input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ] + input [ _type "hidden"; _name (nameof m.UserName); _value m.UserName ] table [ _class "pt-table" ] [ thead [] [ tr [] [ @@ -25,10 +25,10 @@ let assignGroups m groups curGroups ctx vi = tr [] [ td [] [ input [ _type "checkbox" - _name "smallGroups" + _name (nameof m.SmallGroups) _id inputId _value grpId - match curGroups |> List.contains grpId with true -> _checked | false -> () ] + if List.contains grpId curGroups then _checked ] ] td [] [ label [ _for inputId ] [ str grpName ] ] ]) @@ -44,6 +44,7 @@ let assignGroups m groups curGroups ctx vi = /// View for the password change page let changePassword ctx vi = let s = I18N.localizer.Force () + let m = { OldPassword = ""; NewPassword = ""; NewPasswordConfirm = "" } [ p [ _class "pt-center-text" ] [ locStr s["To change your password, enter your current password in the specified box below, then enter your new password twice."] ] @@ -55,17 +56,17 @@ let changePassword ctx vi = div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "oldPassword" ] [ locStr s["Current Password"] ] - input [ _type "password"; _name "oldPassword"; _id "oldPassword"; _required; _autofocus ] + input [ _type "password"; _name (nameof m.OldPassword); _id "oldPassword"; _required; _autofocus ] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "newPassword" ] [ locStr s["New Password Twice"] ] - input [ _type "password"; _name "newPassword"; _id "newPassword"; _required ] + input [ _type "password"; _name (nameof m.NewPassword); _id "newPassword"; _required ] ] div [ _class "pt-field" ] [ label [] [ rawText " " ] - input [ _type "password"; _name "newPasswordConfirm"; _id "newPasswordConfirm"; _required ] + input [ _type "password"; _name (nameof m.NewPasswordConfirm); _id "newPasswordConfirm"; _required ] ] ] div [ _class "pt-field-row" ] [ @@ -81,49 +82,57 @@ let changePassword ctx vi = /// View for the edit user page let edit (m : EditUser) ctx vi = let s = I18N.localizer.Force () - let pageTitle = if m.isNew () then "Add a New User" else "Edit User" - let pwPlaceholder = s[if m.isNew () then "" else "No change"].Value + let pageTitle = if m.IsNew then "Add a New User" else "Edit User" + let pwPlaceholder = s[if m.IsNew then "" else "No change"].Value [ form [ _action "/web/user/edit/save"; _method "post"; _class "pt-center-columns" _onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')""" ] [ style [ _scoped ] - [ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #emailAddress { width: 20rem; } " ] + [ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #email { width: 20rem; } " ] csrfToken ctx - input [ _type "hidden"; _name "userId"; _value (flatGuid m.userId) ] + input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "firstName" ] [ locStr s["First Name"] ] - input [ _type "text"; _name "firstName"; _id "firstName"; _value m.firstName; _required; _autofocus ] + input [ _type "text" + _name (nameof m.FirstName) + _id "firstName" + _value m.FirstName + _required + _autofocus ] ] div [ _class "pt-field" ] [ label [ _for "lastName" ] [ locStr s["Last Name"] ] - input [ _type "text"; _name "lastName"; _id "lastName"; _value m.lastName; _required ] + input [ _type "text"; _name (nameof m.LastName); _id "lastName"; _value m.LastName; _required ] ] div [ _class "pt-field" ] [ - label [ _for "emailAddress" ] [ locStr s["E-mail Address"] ] - input [ _type "email"; _name "emailAddress"; _id "emailAddress"; _value m.emailAddress; _required ] + label [ _for "email" ] [ locStr s["E-mail Address"] ] + input [ _type "email"; _name (nameof m.Email); _id "email"; _value m.Email; _required ] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "password" ] [ locStr s["Password"] ] - input [ _type "password"; _name "password"; _id "password"; _placeholder pwPlaceholder ] + input [ _type "password"; _name (nameof m.Password); _id "password"; _placeholder pwPlaceholder ] ] div [ _class "pt-field" ] [ label [ _for "passwordConfirm" ] [ locStr s["Password Again"] ] - input [ _type "password"; _name "passwordConfirm"; _id "passwordConfirm"; _placeholder pwPlaceholder ] + input [ _type "password" + _name (nameof m.PasswordConfirm) + _id "passwordConfirm" + _placeholder pwPlaceholder ] ] ] div [ _class "pt-checkbox-field" ] [ input [ _type "checkbox" - _name "isAdmin" + _name (nameof m.IsAdmin) _id "isAdmin" _value "True" - match m.isAdmin with Some x when x -> _checked | _ -> () ] + if defaultArg m.IsAdmin false then _checked ] label [ _for "isAdmin" ] [ locStr s["This user is a PrayerTracker administrator"] ] ] div [ _class "pt-field-row" ] [ submit [] "save" s["Save User"] ] ] - script [] [ rawText $"PT.onLoad(PT.user.edit.onPageLoad({(string (m.isNew ())).ToLower ()}))" ] + script [] [ rawText $"PT.onLoad(PT.user.edit.onPageLoad({(string m.IsNew).ToLowerInvariant ()}))" ] ] |> Layout.Content.standard |> Layout.standard vi pageTitle @@ -133,33 +142,35 @@ let edit (m : EditUser) ctx vi = let logOn (m : UserLogOn) groups ctx vi = let s = I18N.localizer.Force () form [ _action "/web/user/log-on"; _method "post"; _class "pt-center-columns" ] [ - style [ _scoped ] [ rawText "#emailAddress { width: 20rem; }" ] + style [ _scoped ] [ rawText "#email { width: 20rem; }" ] csrfToken ctx - input [ _type "hidden"; _name "redirectUrl"; _value (defaultArg m.redirectUrl "") ] + input [ _type "hidden"; _name (nameof m.RedirectUrl); _value (defaultArg m.RedirectUrl "") ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "emailAddress"] [ locStr s["E-mail Address"] ] - input [ _type "email"; _name "emailAddress"; _id "emailAddress"; _value m.emailAddress; _required - _autofocus ] + label [ _for "email"] [ locStr s["E-mail Address"] ] + input [ _type "email"; _name (nameof m.Email); _id "email"; _value m.Email; _required; _autofocus ] ] div [ _class "pt-field" ] [ label [ _for "password" ] [ locStr s["Password"] ] - input [ _type "password"; _name "password"; _id "password"; _required; + input [ _type "password" + _name (nameof m.Password) + _id "password" + _required; _placeholder (sprintf "(%s)" (s["Case-Sensitive"].Value.ToLower ())) ] ] ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ - label [ _for "smallGroupId" ] [ locStr s["Group"] ] + label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ] seq { "", selectDefault s["Select Group"].Value yield! groups } - |> selectList "smallGroupId" "" [ _required ] + |> selectList (nameof m.SmallGroupId) "" [ _required ] ] ] div [ _class "pt-checkbox-field" ] [ - input [ _type "checkbox"; _name "rememberMe"; _id "rememberMe"; _value "True" ] + input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ] label [ _for "rememberMe" ] [ locStr s["Remember Me"] ] br [] small [] [ em [] [ rawText "("; str (s["Requires Cookies"].Value.ToLower ()); rawText ")" ] ] diff --git a/src/PrayerTracker.UI/Utils.fs b/src/PrayerTracker.UI/Utils.fs index 1a7dd72..5940c86 100644 --- a/src/PrayerTracker.UI/Utils.fs +++ b/src/PrayerTracker.UI/Utils.fs @@ -32,12 +32,7 @@ module String = let replaceFirst (needle : string) replacement (haystack : string) = match haystack.IndexOf needle with | -1 -> haystack - | idx -> - [ haystack[0..idx - 1] - replacement - haystack[idx + needle.Length..] - ] - |> String.concat "" + | idx -> String.concat "" [ haystack[0..idx - 1]; replacement; haystack[idx + needle.Length..] ] open System.Text.RegularExpressions @@ -49,14 +44,15 @@ let stripTags allowedTags input = let mutable output = input for tag in stripHtmlExp.Matches input do let htmlTag = tag.Value.ToLower () - let isAllowed = + let shouldReplace = allowedTags |> List.fold (fun acc t -> acc || htmlTag.IndexOf $"<{t}>" = 0 || htmlTag.IndexOf $"<{t} " = 0 || htmlTag.IndexOf $" not + if shouldReplace then output <- String.replaceFirst tag.Value "" output output @@ -88,9 +84,9 @@ let wordWrap charPerLine (input : string) = remaining <- remaining[spaceIdx + 1..] // Leftovers - yum! match remaining.Length with 0 -> () | _ -> yield remaining + yield "" } - |> Seq.fold (fun (acc : StringBuilder) -> acc.AppendLine) (StringBuilder ()) - |> string + |> String.concat "\n" /// Modify the text returned by CKEditor into the format we need for request and announcement text let ckEditorToText (text : string) = diff --git a/src/PrayerTracker.UI/ViewModels.fs b/src/PrayerTracker.UI/ViewModels.fs index 5ccf329..ddaa727 100644 --- a/src/PrayerTracker.UI/ViewModels.fs +++ b/src/PrayerTracker.UI/ViewModels.fs @@ -43,19 +43,40 @@ module ReferenceList = Announcement, s["Announcements"] ] -// fsharplint:disable RecordFieldNames MemberNames + +/// A user message level +type MessageLevel = + /// An informational message to the user + | Info + /// A message with information the user should consider + | Warning + /// A message indicating that something went wrong + | Error + +/// Support for the MessageLevel type +module MessageLevel = + + /// Convert a message level to its string representation + let toString = + function + | Info -> "Info" + | Warning -> "WARNING" + | Error -> "ERROR" + + let toCssClass level = (toString level).ToLowerInvariant () + /// This is used to create a message that is displayed to the user [] type UserMessage = { /// The type - level : string + Level : MessageLevel /// The actual message - text : HtmlString + Text : HtmlString /// The description (further information) - description : HtmlString option + Description : HtmlString option } /// Support for the UserMessage type @@ -63,23 +84,23 @@ module UserMessage = /// Error message template let error = - { level = "ERROR" - text = HtmlString.Empty - description = None + { Level = Error + Text = HtmlString.Empty + Description = None } /// Warning message template let warning = - { level = "WARNING" - text = HtmlString.Empty - description = None + { Level = Warning + Text = HtmlString.Empty + Description = None } /// Info message template let info = - { level = "Info" - text = HtmlString.Empty - description = None + { Level = Info + Text = HtmlString.Empty + Description = None } @@ -89,28 +110,28 @@ open System [] type AppViewInfo = { /// CSS files for the page - style : string list + Style : string list /// JavaScript files for the page - script : string list + Script : string list /// The link for help on this page - helpLink : string option + HelpLink : string option /// Messages to be displayed to the user - messages : UserMessage list + Messages : UserMessage list /// The current version of PrayerTracker - version : string + Version : string /// The ticks when the request started - requestStart : int64 + RequestStart : int64 /// The currently logged on user, if there is one - user : User option + User : User option /// The currently logged on small group, if there is one - group : SmallGroup option + Group : SmallGroup option } /// Support for the AppViewInfo type @@ -118,14 +139,14 @@ module AppViewInfo = /// A fresh version that can be populated to process the current request let fresh = - { style = [] - script = [] - helpLink = None - messages = [] - version = "" - requestStart = DateTime.Now.Ticks - user = None - group = None + { Style = [] + Script = [] + HelpLink = None + Messages = [] + Version = "" + RequestStart = DateTime.Now.Ticks + User = None + Group = None } @@ -133,34 +154,35 @@ module AppViewInfo = [] type Announcement = { /// Whether the announcement should be sent to the class or to PrayerTracker users - sendToClass : string + SendToClass : string /// The text of the announcement - text : string + Text : string /// Whether this announcement should be added to the "Announcements" of the prayer list - addToRequestList : bool option + AddToRequestList : bool option /// The ID of the request type to which this announcement should be added - requestType : string option + RequestType : string option } with /// The text of the announcement, in plain text - member this.plainText () = (htmlToPlainText >> wordWrap 74) this.text + member this.PlainText + with get () = (htmlToPlainText >> wordWrap 74) this.Text /// Form for assigning small groups to a user [] type AssignGroups = { /// The Id of the user being assigned - userId : UserId + UserId : UserId /// The full name of the user being assigned - userName : string + UserName : string /// The Ids of the small groups to which the user is authorized - smallGroups : string + SmallGroups : string } /// Support for the AssignGroups type @@ -168,9 +190,9 @@ module AssignGroups = /// Create an instance of this form from an existing user let fromUser (u : User) = - { userId = u.userId - userName = u.fullName - smallGroups = "" + { UserId = u.userId + UserName = u.fullName + SmallGroups = "" } @@ -178,11 +200,13 @@ module AssignGroups = [] type ChangePassword = { /// The user's current password - oldPassword : string + OldPassword : string + /// The user's new password - newPassword : string + NewPassword : string + /// The user's new password, confirmed - newPasswordConfirm : string + NewPasswordConfirm : string } @@ -190,36 +214,37 @@ type ChangePassword = [] type EditChurch = { /// The Id of the church - churchId : ChurchId + ChurchId : ChurchId /// The name of the church - name : string + Name : string /// The city for the church - city : string + City : string - /// The state for the church - st : string + /// The state or province for the church + State : string - /// Whether the church has an active VPR interface - hasInterface : bool option + /// Whether the church has an active Virtual Prayer Room interface + HasInterface : bool option /// The address for the interface - interfaceAddress : string option + InterfaceAddress : string option } with /// Is this a new church? - member this.isNew () = Guid.Empty = this.churchId + member this.IsNew + with get () = Guid.Empty = this.ChurchId /// Populate a church from this form - member this.populateChurch (church : Church) = + member this.PopulateChurch (church : Church) = { church with - name = this.name - city = this.city - st = this.st - hasInterface = match this.hasInterface with Some x -> x | None -> false - interfaceAddress = match this.hasInterface with Some x when x -> this.interfaceAddress | _ -> None + name = this.Name + city = this.City + st = this.State + hasInterface = match this.HasInterface with Some x -> x | None -> false + interfaceAddress = match this.HasInterface with Some x when x -> this.InterfaceAddress | _ -> None } /// Support for the EditChurch type @@ -227,22 +252,22 @@ module EditChurch = /// Create an instance from an existing church let fromChurch (ch : Church) = - { churchId = ch.churchId - name = ch.name - city = ch.city - st = ch.st - hasInterface = match ch.hasInterface with true -> Some true | false -> None - interfaceAddress = ch.interfaceAddress + { ChurchId = ch.churchId + Name = ch.name + City = ch.city + State = ch.st + HasInterface = match ch.hasInterface with true -> Some true | false -> None + InterfaceAddress = ch.interfaceAddress } /// An instance to use for adding churches let empty = - { churchId = Guid.Empty - name = "" - city = "" - st = "" - hasInterface = None - interfaceAddress = None + { ChurchId = Guid.Empty + Name = "" + City = "" + State = "" + HasInterface = None + InterfaceAddress = None } @@ -250,39 +275,40 @@ module EditChurch = [] type EditMember = { /// The Id for this small group member (not user-entered) - memberId : MemberId + MemberId : MemberId /// The name of the member - memberName : string + Name : string /// The e-mail address - emailAddress : string + Email : string /// The e-mail format - emailType : string + Format : string } with /// Is this a new member? - member this.isNew () = Guid.Empty = this.memberId + member this.IsNew + with get () = Guid.Empty = this.MemberId /// Support for the EditMember type module EditMember = /// Create an instance from an existing member let fromMember (m : Member) = - { memberId = m.memberId - memberName = m.memberName - emailAddress = m.email - emailType = match m.format with Some f -> f | None -> "" + { MemberId = m.memberId + Name = m.memberName + Email = m.email + Format = match m.format with Some f -> f | None -> "" } /// An empty instance let empty = - { memberId = Guid.Empty - memberName = "" - emailAddress = "" - emailType = "" + { MemberId = Guid.Empty + Name = "" + Email = "" + Format = "" } @@ -290,90 +316,90 @@ module EditMember = [] type EditPreferences = { /// The number of days after which requests are automatically expired - expireDays : int + ExpireDays : int /// The number of days requests are considered "new" - daysToKeepNew : int + DaysToKeepNew : int /// The number of weeks after which a long-term requests is flagged as requiring an update - longTermUpdateWeeks : int + LongTermUpdateWeeks : int /// Whether to sort by updated date or requestor/subject - requestSort : string + RequestSort : string /// The name from which e-mail will be sent - emailFromName : string + EmailFromName : string /// The e-mail address from which e-mail will be sent - emailFromAddress : string + EmailFromAddress : string /// The default e-mail type for this group - defaultEmailType : string + DefaultEmailType : string /// Whether the heading line color uses named colors or R/G/B - headingLineType : string + LineColorType : string /// The named color for the heading lines - headingLineColor : string + LineColor : string /// Whether the heading text color uses named colors or R/G/B - headingTextType : string + HeadingColorType : string /// The named color for the heading text - headingTextColor : string + HeadingColor : string /// The fonts to use for the list - listFonts : string + Fonts : string /// The font size for the heading text - headingFontSize : int + HeadingFontSize : int /// The font size for the list text - listFontSize : int + ListFontSize : int /// The time zone for the class - timeZone : string + TimeZone : string /// The list visibility - listVisibility : int + Visibility : int /// The small group password - groupPassword : string option + GroupPassword : string option /// The page size for search / inactive requests - pageSize : int + PageSize : int /// How the as-of date should be displayed - asOfDate : string + AsOfDate : string } with /// Set the properties of a small group based on the form's properties - member this.populatePreferences (prefs : ListPreferences) = + member this.PopulatePreferences (prefs : ListPreferences) = let isPublic, grpPw = - match this.listVisibility with + match this.Visibility with | RequestVisibility.``public`` -> true, "" - | RequestVisibility.passwordProtected -> false, (defaultArg this.groupPassword "") + | RequestVisibility.passwordProtected -> false, (defaultArg this.GroupPassword "") | RequestVisibility.``private`` | _ -> false, "" { prefs with - daysToExpire = this.expireDays - daysToKeepNew = this.daysToKeepNew - longTermUpdateWeeks = this.longTermUpdateWeeks - requestSort = RequestSort.fromCode this.requestSort - emailFromName = this.emailFromName - emailFromAddress = this.emailFromAddress - defaultEmailType = EmailFormat.fromCode this.defaultEmailType - lineColor = this.headingLineColor - headingColor = this.headingTextColor - listFonts = this.listFonts - headingFontSize = this.headingFontSize - textFontSize = this.listFontSize - timeZoneId = this.timeZone + daysToExpire = this.ExpireDays + daysToKeepNew = this.DaysToKeepNew + longTermUpdateWeeks = this.LongTermUpdateWeeks + requestSort = RequestSort.fromCode this.RequestSort + emailFromName = this.EmailFromName + emailFromAddress = this.EmailFromAddress + defaultEmailType = EmailFormat.fromCode this.DefaultEmailType + lineColor = this.LineColor + headingColor = this.HeadingColor + listFonts = this.Fonts + headingFontSize = this.HeadingFontSize + textFontSize = this.ListFontSize + timeZoneId = this.TimeZone isPublic = isPublic groupPassword = grpPw - pageSize = this.pageSize - asOfDateDisplay = AsOfDateDisplay.fromCode this.asOfDate + pageSize = this.PageSize + asOfDateDisplay = AsOfDateDisplay.fromCode this.AsOfDate } /// Support for the EditPreferences type @@ -381,25 +407,25 @@ module EditPreferences = /// Populate an edit form from existing preferences let fromPreferences (prefs : ListPreferences) = let setType (x : string) = match x.StartsWith "#" with true -> "RGB" | false -> "Name" - { expireDays = prefs.daysToExpire - daysToKeepNew = prefs.daysToKeepNew - longTermUpdateWeeks = prefs.longTermUpdateWeeks - requestSort = prefs.requestSort.code - emailFromName = prefs.emailFromName - emailFromAddress = prefs.emailFromAddress - defaultEmailType = prefs.defaultEmailType.code - headingLineType = setType prefs.lineColor - headingLineColor = prefs.lineColor - headingTextType = setType prefs.headingColor - headingTextColor = prefs.headingColor - listFonts = prefs.listFonts - headingFontSize = prefs.headingFontSize - listFontSize = prefs.textFontSize - timeZone = prefs.timeZoneId - groupPassword = Some prefs.groupPassword - pageSize = prefs.pageSize - asOfDate = prefs.asOfDateDisplay.code - listVisibility = + { ExpireDays = prefs.daysToExpire + DaysToKeepNew = prefs.daysToKeepNew + LongTermUpdateWeeks = prefs.longTermUpdateWeeks + RequestSort = prefs.requestSort.code + EmailFromName = prefs.emailFromName + EmailFromAddress = prefs.emailFromAddress + DefaultEmailType = prefs.defaultEmailType.code + LineColorType = setType prefs.lineColor + LineColor = prefs.lineColor + HeadingColorType = setType prefs.headingColor + HeadingColor = prefs.headingColor + Fonts = prefs.listFonts + HeadingFontSize = prefs.headingFontSize + ListFontSize = prefs.textFontSize + TimeZone = prefs.timeZoneId + GroupPassword = Some prefs.groupPassword + PageSize = prefs.pageSize + AsOfDate = prefs.asOfDateDisplay.code + Visibility = match true with | _ when prefs.isPublic -> RequestVisibility.``public`` | _ when prefs.groupPassword = "" -> RequestVisibility.``private`` @@ -411,53 +437,54 @@ module EditPreferences = [] type EditRequest = { /// The Id of the request - requestId : PrayerRequestId + RequestId : PrayerRequestId /// The type of the request - requestType : string + RequestType : string /// The date of the request - enteredDate : DateTime option + EnteredDate : DateTime option /// Whether to update the date or not - skipDateUpdate : bool option + SkipDateUpdate : bool option /// The requestor or subject - requestor : string option + Requestor : string option /// How this request is expired - expiration : string + Expiration : string /// The text of the request - text : string + Text : string } with /// Is this a new request? - member this.isNew () = Guid.Empty = this.requestId + member this.IsNew + with get () = Guid.Empty = this.RequestId /// Support for the EditRequest type module EditRequest = /// An empty instance to use for new requests let empty = - { requestId = Guid.Empty - requestType = CurrentRequest.code - enteredDate = None - skipDateUpdate = None - requestor = None - expiration = Automatic.code - text = "" + { RequestId = Guid.Empty + RequestType = CurrentRequest.code + EnteredDate = None + SkipDateUpdate = None + Requestor = None + Expiration = Automatic.code + Text = "" } /// Create an instance from an existing request let fromRequest req = { empty with - requestId = req.prayerRequestId - requestType = req.requestType.code - requestor = req.requestor - expiration = req.expiration.code - text = req.text + RequestId = req.prayerRequestId + RequestType = req.requestType.code + Requestor = req.requestor + Expiration = req.expiration.code + Text = req.text } @@ -465,41 +492,42 @@ module EditRequest = [] type EditSmallGroup = { /// The Id of the small group - smallGroupId : SmallGroupId + SmallGroupId : SmallGroupId /// The name of the small group - name : string + Name : string /// The Id of the church to which this small group belongs - churchId : ChurchId + ChurchId : ChurchId } with /// Is this a new small group? - member this.isNew () = Guid.Empty = this.smallGroupId + member this.IsNew + with get () = Guid.Empty = this.SmallGroupId /// Populate a small group from this form member this.populateGroup (grp : SmallGroup) = { grp with - name = this.name - churchId = this.churchId - } + name = this.Name + churchId = this.ChurchId + } /// Support for the EditSmallGroup type module EditSmallGroup = /// Create an instance from an existing small group let fromGroup (g : SmallGroup) = - { smallGroupId = g.smallGroupId - name = g.name - churchId = g.churchId + { SmallGroupId = g.smallGroupId + Name = g.name + ChurchId = g.churchId } /// An empty instance (used when adding a new group) let empty = - { smallGroupId = Guid.Empty - name = "" - churchId = Guid.Empty + { SmallGroupId = Guid.Empty + Name = "" + ChurchId = Guid.Empty } @@ -507,65 +535,66 @@ module EditSmallGroup = [] type EditUser = { /// The Id of the user - userId : UserId + UserId : UserId /// The first name of the user - firstName : string + FirstName : string /// The last name of the user - lastName : string + LastName : string /// The e-mail address for the user - emailAddress : string + Email : string /// The password for the user - password : string + Password : string /// The password hash for the user a second time - passwordConfirm : string + PasswordConfirm : string /// Is this user a PrayerTracker administrator? - isAdmin : bool option + IsAdmin : bool option } with /// Is this a new user? - member this.isNew () = Guid.Empty = this.userId + member this.IsNew + with get () = Guid.Empty = this.UserId /// Populate a user from the form - member this.populateUser (user : User) hasher = + member this.PopulateUser (user : User) hasher = { user with - firstName = this.firstName - lastName = this.lastName - emailAddress = this.emailAddress - isAdmin = match this.isAdmin with Some x -> x | None -> false + firstName = this.FirstName + lastName = this.LastName + emailAddress = this.Email + isAdmin = defaultArg this.IsAdmin false } |> function - | u when isNull this.password || this.password = "" -> u - | u -> { u with passwordHash = hasher this.password } + | u when isNull this.Password || this.Password = "" -> u + | u -> { u with passwordHash = hasher this.Password } /// Support for the EditUser type module EditUser = /// An empty instance let empty = - { userId = Guid.Empty - firstName = "" - lastName = "" - emailAddress = "" - password = "" - passwordConfirm = "" - isAdmin = None + { UserId = Guid.Empty + FirstName = "" + LastName = "" + Email = "" + Password = "" + PasswordConfirm = "" + IsAdmin = None } /// Create an instance from an existing user let fromUser (user : User) = { empty with - userId = user.userId - firstName = user.firstName - lastName = user.lastName - emailAddress = user.emailAddress - isAdmin = match user.isAdmin with true -> Some true | false -> None + UserId = user.userId + FirstName = user.firstName + LastName = user.lastName + Email = user.emailAddress + IsAdmin = if user.isAdmin then Some true else None } @@ -573,13 +602,13 @@ module EditUser = [] type GroupLogOn = { /// The ID of the small group to which the user is logging on - smallGroupId : SmallGroupId + SmallGroupId : SmallGroupId /// The password entered - password : string + Password : string /// Whether to remember the login - rememberMe : bool option + RememberMe : bool option } /// Support for the GroupLogOn type @@ -587,9 +616,9 @@ module GroupLogOn = /// An empty instance let empty = - { smallGroupId = Guid.Empty - password = "" - rememberMe = None + { SmallGroupId = Guid.Empty + Password = "" + RememberMe = None } @@ -597,19 +626,19 @@ module GroupLogOn = [] type MaintainRequests = { /// The requests to be displayed - requests : PrayerRequest seq + Requests : PrayerRequest list /// The small group to which the requests belong - smallGroup : SmallGroup + SmallGroup : SmallGroup /// Whether only active requests are included - onlyActive : bool option + OnlyActive : bool option /// The search term for the requests - searchTerm : string option + SearchTerm : string option /// The page number of the results - pageNbr : int option + PageNbr : int option } /// Support for the MaintainRequests type @@ -617,11 +646,11 @@ module MaintainRequests = /// An empty instance let empty = - { requests = Seq.empty - smallGroup = SmallGroup.empty - onlyActive = None - searchTerm = None - pageNbr = None + { Requests = [] + SmallGroup = SmallGroup.empty + OnlyActive = None + SearchTerm = None + PageNbr = None } @@ -629,16 +658,16 @@ module MaintainRequests = [] type Overview = { /// The total number of active requests - totalActiveReqs : int + TotalActiveReqs : int - /// The numbers of active requests by category - activeReqsByCat : Map + /// The numbers of active requests by request type + ActiveReqsByType : Map /// A count of all requests - allReqs : int + AllReqs : int /// A count of all members - totalMbrs : int + TotalMembers : int } @@ -646,19 +675,19 @@ type Overview = [] type UserLogOn = { /// The e-mail address of the user - emailAddress : string + Email : string /// The password entered - password : string + Password : string /// The ID of the small group to which the user is logging on - smallGroupId : SmallGroupId + SmallGroupId : SmallGroupId /// Whether to remember the login - rememberMe : bool option + RememberMe : bool option /// The URL to which the user should be redirected once login is successful - redirectUrl : string option + RedirectUrl : string option } /// Support for the UserLogOn type @@ -666,11 +695,11 @@ module UserLogOn = /// An empty instance let empty = - { emailAddress = "" - password = "" - smallGroupId = Guid.Empty - rememberMe = None - redirectUrl = None + { Email = "" + Password = "" + SmallGroupId = Guid.Empty + RememberMe = None + RedirectUrl = None } @@ -679,64 +708,64 @@ open Giraffe.ViewEngine /// This represents a list of requests type RequestList = { /// The prayer request list - requests : PrayerRequest list + Requests : PrayerRequest list /// The date for which this list is being generated - date : DateTime + Date : DateTime /// The small group to which this list belongs - listGroup : SmallGroup + SmallGroup : SmallGroup /// Whether to show the class header - showHeader : bool + ShowHeader : bool /// The list of recipients (populated if requests are e-mailed) - recipients : Member list + Recipients : Member list /// Whether the user can e-mail this list - canEmail : bool + CanEmail : bool } with /// Group requests by their type, along with the type and its localized string - member private this.requestsByType (s : IStringLocalizer) = + member this.RequestsByType (s : IStringLocalizer) = ReferenceList.requestTypeList s - |> List.map (fun (typ, name) -> typ, name, this.requests |> List.filter (fun req -> req.requestType = typ)) + |> List.map (fun (typ, name) -> + let sort = + match this.SmallGroup.preferences.requestSort with + | SortByDate -> Seq.sortByDescending (fun req -> req.updatedDate) + | SortByRequestor -> Seq.sortBy (fun req -> req.requestor) + let reqs = + this.Requests + |> Seq.ofList + |> Seq.filter (fun req -> req.requestType = typ) + |> sort + |> List.ofSeq + typ, name, reqs) |> List.filter (fun (_, _, reqs) -> not (List.isEmpty reqs)) - /// Get the requests for a specified type - member this.requestsInCategory cat = - let reqs = - this.requests - |> Seq.ofList - |> Seq.filter (fun req -> req.requestType = cat) - match this.listGroup.preferences.requestSort with - | SortByDate -> reqs |> Seq.sortByDescending (fun req -> req.updatedDate) - | SortByRequestor -> reqs |> Seq.sortBy (fun req -> req.requestor) - |> List.ofSeq - /// Is this request new? - member this.isNew (req : PrayerRequest) = - (this.date - req.updatedDate).Days <= this.listGroup.preferences.daysToKeepNew + member this.IsNew (req : PrayerRequest) = + (this.Date - req.updatedDate).Days <= this.SmallGroup.preferences.daysToKeepNew /// Generate this list as HTML - member this.asHtml (s : IStringLocalizer) = - let prefs = this.listGroup.preferences + member this.AsHtml (s : IStringLocalizer) = + let prefs = this.SmallGroup.preferences let asOfSize = Math.Round (float prefs.textFontSize * 0.8, 2) - [ if this.showHeader then + [ if this.ShowHeader then div [ _style $"text-align:center;font-family:{prefs.listFonts}" ] [ span [ _style $"font-size:%i{prefs.headingFontSize}pt;" ] [ strong [] [ str s["Prayer Requests"].Value ] ] br [] span [ _style $"font-size:%i{prefs.textFontSize}pt;" ] [ - strong [] [ str this.listGroup.name ] + strong [] [ str this.SmallGroup.name ] br [] - str (this.date.ToString s["MMMM d, yyyy"].Value) + str (this.Date.ToString s["MMMM d, yyyy"].Value) ] ] br [] - for _, name, reqs in this.requestsByType s do + for _, name, reqs in this.RequestsByType s do div [ _style "padding-left:10px;padding-bottom:.5em;" ] [ table [ _style $"font-family:{prefs.listFonts};page-break-inside:avoid;" ] [ tr [] [ @@ -748,7 +777,7 @@ with ] reqs |> List.map (fun req -> - let bullet = if this.isNew req then "circle" else "disc" + let bullet = if this.IsNew req then "circle" else "disc" li [ _style $"list-style-type:{bullet};font-family:{prefs.listFonts};font-size:%i{prefs.textFontSize}pt;padding-bottom:.25em;" ] [ match req.requestor with | Some r when r <> "" -> @@ -776,25 +805,25 @@ with |> RenderView.AsString.htmlNodes /// Generate this list as plain text - member this.asText (s : IStringLocalizer) = + member this.AsText (s : IStringLocalizer) = seq { - this.listGroup.name + this.SmallGroup.name s["Prayer Requests"].Value - this.date.ToString s["MMMM d, yyyy"].Value + this.Date.ToString s["MMMM d, yyyy"].Value " " - for _, name, reqs in this.requestsByType s do + for _, name, reqs in this.RequestsByType s do let dashes = String.replicate (name.Value.Length + 4) "-" dashes $" {name.Value.ToUpper ()}" dashes for req in reqs do - let bullet = if this.isNew req then "+" else "-" + let bullet = if this.IsNew req then "+" else "-" let requestor = match req.requestor with Some r -> $"{r} - " | None -> "" - match this.listGroup.preferences.asOfDateDisplay with + match this.SmallGroup.preferences.asOfDateDisplay with | NoDisplay -> "" | _ -> let dt = - match this.listGroup.preferences.asOfDateDisplay with + match this.SmallGroup.preferences.asOfDateDisplay with | ShortDate -> req.updatedDate.ToShortDateString () | LongDate -> req.updatedDate.ToLongDateString () | _ -> "" diff --git a/src/PrayerTracker/Church.fs b/src/PrayerTracker/Church.fs index 7bf5bf6..92aee52 100644 --- a/src/PrayerTracker/Church.fs +++ b/src/PrayerTracker/Church.fs @@ -70,17 +70,17 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c match! ctx.TryBindFormAsync () with | Ok m -> let! church = - if m.isNew () then Task.FromResult (Some { Church.empty with churchId = Guid.NewGuid () }) - else ctx.db.TryChurchById m.churchId + if m.IsNew then Task.FromResult (Some { Church.empty with churchId = Guid.NewGuid () }) + else ctx.db.TryChurchById m.ChurchId match church with | Some ch -> - m.populateChurch ch - |> (if m.isNew () then ctx.db.AddEntry else ctx.db.UpdateEntry) + m.PopulateChurch ch + |> (if m.IsNew then ctx.db.AddEntry else ctx.db.UpdateEntry) let! _ = ctx.db.SaveChangesAsync () let s = Views.I18N.localizer.Force () - let act = s[if m.isNew () then "Added" else "Updated"].Value.ToLower () - addInfo ctx s["Successfully {0} church “{1}”", act, m.name] + let act = s[if m.IsNew then "Added" else "Updated"].Value.ToLower () + addInfo ctx s["Successfully {0} church “{1}”", act, m.Name] return! redirectTo false "/web/churches" next ctx | None -> return! fourOhFour next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } diff --git a/src/PrayerTracker/CommonFunctions.fs b/src/PrayerTracker/CommonFunctions.fs index ba54e2f..e3ad1d6 100644 --- a/src/PrayerTracker/CommonFunctions.fs +++ b/src/PrayerTracker/CommonFunctions.fs @@ -85,11 +85,11 @@ let viewInfo (ctx : HttpContext) startTicks = HttpOnly = true)) | None -> () { AppViewInfo.fresh with - version = appVersion - messages = msg - requestStart = startTicks - user = ctx.Session.user - group = ctx.Session.smallGroup + Version = appVersion + Messages = msg + RequestStart = startTicks + User = ctx.Session.user + Group = ctx.Session.smallGroup } /// The view is the last parameter, so it can be composed @@ -130,19 +130,19 @@ let htmlString (x : LocalizedString) = /// Add an error message to the session let addError ctx msg = - addUserMessage ctx { UserMessage.error with text = htmlLocString msg } + addUserMessage ctx { UserMessage.error with Text = htmlLocString msg } /// Add an informational message to the session let addInfo ctx msg = - addUserMessage ctx { UserMessage.info with text = htmlLocString msg } + addUserMessage ctx { UserMessage.info with Text = htmlLocString msg } /// Add an informational HTML message to the session let addHtmlInfo ctx msg = - addUserMessage ctx { UserMessage.info with text = htmlString msg } + addUserMessage ctx { UserMessage.info with Text = htmlString msg } /// Add a warning message to the session let addWarning ctx msg = - addUserMessage ctx { UserMessage.warning with text = htmlLocString msg } + addUserMessage ctx { UserMessage.warning with Text = htmlLocString msg } /// A level of required access diff --git a/src/PrayerTracker/Email.fs b/src/PrayerTracker/Email.fs index b595605..c9a1e78 100644 --- a/src/PrayerTracker/Email.fs +++ b/src/PrayerTracker/Email.fs @@ -21,7 +21,7 @@ let getConnection () = task { /// Create a mail message object, filled with everything but the body content let createMessage (grp : SmallGroup) subj = - let msg = MimeMessage () + let msg = new MimeMessage () msg.From.Add (MailboxAddress (grp.preferences.emailFromName, fromAddress)) msg.Subject <- subj msg.ReplyTo.Add (MailboxAddress (grp.preferences.emailFromName, grp.preferences.emailFromAddress)) @@ -40,7 +40,7 @@ let createHtmlMessage grp subj body (s : IStringLocalizer) = ] |> String.concat "" let msg = createMessage grp subj - msg.Body <- TextPart (TextFormat.Html, Text = bodyText) + msg.Body <- new TextPart (TextFormat.Html, Text = bodyText) msg /// Create a plain-text-format e-mail message @@ -54,13 +54,13 @@ let createTextMessage grp subj body (s : IStringLocalizer) = ] |> String.concat "" let msg = createMessage grp subj - msg.Body <- TextPart (TextFormat.Plain, Text = bodyText) + msg.Body <- new TextPart (TextFormat.Plain, Text = bodyText) msg /// Send e-mails to a class let sendEmails (client : SmtpClient) (recipients : Member list) grp subj html text s = task { - let htmlMsg = createHtmlMessage grp subj html s - let plainTextMsg = createTextMessage grp subj text s + use htmlMsg = createHtmlMessage grp subj html s + use plainTextMsg = createTextMessage grp subj text s for mbr in recipients do let emailType = @@ -71,10 +71,10 @@ let sendEmails (client : SmtpClient) (recipients : Member list) grp subj html te match emailType with | HtmlFormat -> htmlMsg.To.Add emailTo - do! client.SendAsync htmlMsg + let! _ = client.SendAsync htmlMsg htmlMsg.To.Clear () | PlainTextFormat -> plainTextMsg.To.Add emailTo - do! client.SendAsync plainTextMsg + let! _ = client.SendAsync plainTextMsg plainTextMsg.To.Clear () } diff --git a/src/PrayerTracker/PrayerRequest.fs b/src/PrayerTracker/PrayerRequest.fs index 9b5d14a..a63112a 100644 --- a/src/PrayerTracker/PrayerRequest.fs +++ b/src/PrayerTracker/PrayerRequest.fs @@ -16,8 +16,8 @@ let private findRequest (ctx : HttpContext) reqId = task { | Some _ -> let s = Views.I18N.localizer.Force () addError ctx s["The prayer request you tried to access is not assigned to your group"] - return Error (redirectTo false "/web/unauthorized") - | None -> return Error fourOhFour + return Result.Error (redirectTo false "/web/unauthorized") + | None -> return Result.Error fourOhFour } /// Generate a list of requests for the given date @@ -27,12 +27,12 @@ let private generateRequestList ctx date = task { let listDate = match date with Some d -> d | None -> grp.localDateNow clock let! reqs = ctx.db.AllRequestsForSmallGroup grp clock (Some listDate) true 0 return - { requests = reqs |> List.ofSeq - date = listDate - listGroup = grp - showHeader = true - canEmail = ctx.Session.user |> Option.isSome - recipients = [] + { Requests = reqs + Date = listDate + SmallGroup = grp + ShowHeader = true + CanEmail = Option.isSome ctx.Session.user + Recipients = [] } } @@ -50,7 +50,7 @@ let edit (reqId : PrayerRequestId) : HttpHandler = requireAccess [ User ] >=> fu let now = grp.localDateNow (ctx.GetService ()) if reqId = Guid.Empty then return! - { viewInfo ctx startTicks with script = [ "ckeditor/ckeditor" ]; helpLink = Some Help.editRequest } + { viewInfo ctx startTicks with Script = [ "ckeditor/ckeditor" ]; HelpLink = Some Help.editRequest } |> Views.PrayerRequest.edit EditRequest.empty (now.ToString "yyyy-MM-dd") ctx |> renderHtml next ctx else @@ -59,18 +59,18 @@ let edit (reqId : PrayerRequestId) : HttpHandler = requireAccess [ User ] >=> fu let s = Views.I18N.localizer.Force () if req.isExpired now grp.preferences.daysToExpire then { UserMessage.warning with - text = htmlLocString s["This request is expired."] - description = + Text = htmlLocString s["This request is expired."] + Description = s["To make it active again, update it as necessary, leave “{0}” and “{1}” unchecked, and it will return as an active request.", s["Expire Immediately"], s["Check to not update the date"]] |> (htmlLocString >> Some) } |> addUserMessage ctx return! - { viewInfo ctx startTicks with script = [ "ckeditor/ckeditor" ]; helpLink = Some Help.editRequest } + { viewInfo ctx startTicks with Script = [ "ckeditor/ckeditor" ]; HelpLink = Some Help.editRequest } |> Views.PrayerRequest.edit (EditRequest.fromRequest req) "" ctx |> renderHtml next ctx - | Error e -> return! e next ctx + | Result.Error e -> return! e next ctx } @@ -84,11 +84,11 @@ let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let! recipients = ctx.db.AllMembersForSmallGroup grp.smallGroupId use! client = Email.getConnection () do! Email.sendEmails client recipients - grp s["Prayer Requests for {0} - {1:MMMM d, yyyy}", grp.name, list.date].Value - (list.asHtml s) (list.asText s) s + grp s["Prayer Requests for {0} - {1:MMMM d, yyyy}", grp.name, list.Date].Value + (list.AsHtml s) (list.AsText s) s return! viewInfo ctx startTicks - |> Views.PrayerRequest.email { list with recipients = recipients } + |> Views.PrayerRequest.email { list with Recipients = recipients } |> renderHtml next ctx } @@ -102,7 +102,7 @@ let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun let! _ = ctx.db.SaveChangesAsync () addInfo ctx s["The prayer request was deleted successfully"] return! redirectTo false "/web/prayer-requests" next ctx - | Error e -> return! e next ctx + | Result.Error e -> return! e next ctx } @@ -115,7 +115,7 @@ let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task let! _ = ctx.db.SaveChangesAsync () addInfo ctx s["Successfully {0} prayer request", s["Expired"].Value.ToLower ()] return! redirectTo false "/web/prayer-requests" next ctx - | Error e -> return! e next ctx + | Result.Error e -> return! e next ctx } @@ -129,12 +129,12 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne return! viewInfo ctx startTicks |> Views.PrayerRequest.list - { requests = reqs - date = grp.localDateNow clock - listGroup = grp - showHeader = true - canEmail = ctx.Session.user |> Option.isSome - recipients = [] + { Requests = reqs + Date = grp.localDateNow clock + SmallGroup = grp + ShowHeader = true + CanEmail = Option.isSome ctx.Session.user + Recipients = [] } |> renderHtml next ctx | Some _ -> @@ -165,29 +165,29 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx let pageNbr = match ctx.GetQueryStringValue "page" with | Ok pg -> match Int32.TryParse pg with true, p -> p | false, _ -> 1 - | Error _ -> 1 + | Result.Error _ -> 1 let! m = backgroundTask { match ctx.GetQueryStringValue "search" with | Ok search -> let! reqs = ctx.db.SearchRequestsForSmallGroup grp search pageNbr return { MaintainRequests.empty with - requests = reqs - searchTerm = Some search - pageNbr = Some pageNbr + Requests = reqs + SearchTerm = Some search + PageNbr = Some pageNbr } - | Error _ -> + | Result.Error _ -> let! reqs = ctx.db.AllRequestsForSmallGroup grp (ctx.GetService ()) None onlyActive pageNbr return { MaintainRequests.empty with - requests = reqs - onlyActive = Some onlyActive - pageNbr = match onlyActive with true -> None | false -> Some pageNbr + Requests = reqs + OnlyActive = Some onlyActive + PageNbr = if onlyActive then None else Some pageNbr } } return! - { viewInfo ctx startTicks with helpLink = Some Help.maintainRequests } - |> Views.PrayerRequest.maintain { m with smallGroup = grp } ctx + { viewInfo ctx startTicks with HelpLink = Some Help.maintainRequests } + |> Views.PrayerRequest.maintain { m with SmallGroup = grp } ctx |> renderHtml next ctx } @@ -210,7 +210,7 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas let! _ = ctx.db.SaveChangesAsync () addInfo ctx s["Successfully {0} prayer request", s["Restored"].Value.ToLower ()] return! redirectTo false "/web/prayer-requests" next ctx - | Error e -> return! e next ctx + | Result.Error e -> return! e next ctx } @@ -219,38 +219,38 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ct match! ctx.TryBindFormAsync () with | Ok m -> let! req = - if m.isNew () then Task.FromResult (Some { PrayerRequest.empty with prayerRequestId = Guid.NewGuid () }) - else ctx.db.TryRequestById m.requestId + if m.IsNew then Task.FromResult (Some { PrayerRequest.empty with prayerRequestId = Guid.NewGuid () }) + else ctx.db.TryRequestById m.RequestId match req with | Some pr -> let upd8 = { pr with - requestType = PrayerRequestType.fromCode m.requestType - requestor = match m.requestor with Some x when x.Trim () = "" -> None | x -> x - text = ckEditorToText m.text - expiration = Expiration.fromCode m.expiration + requestType = PrayerRequestType.fromCode m.RequestType + requestor = match m.Requestor with Some x when x.Trim () = "" -> None | x -> x + text = ckEditorToText m.Text + expiration = Expiration.fromCode m.Expiration } let grp = currentGroup ctx let now = grp.localDateNow (ctx.GetService ()) - match m.isNew () with + match m.IsNew with | true -> - let dt = match m.enteredDate with Some x -> x | None -> now + let dt = defaultArg m.EnteredDate now { upd8 with smallGroupId = grp.smallGroupId userId = (currentUser ctx).userId enteredDate = dt updatedDate = dt } - | false when Option.isSome m.skipDateUpdate && Option.get m.skipDateUpdate -> upd8 + | false when defaultArg m.SkipDateUpdate false -> upd8 | false -> { upd8 with updatedDate = now } - |> (if m.isNew () then ctx.db.AddEntry else ctx.db.UpdateEntry) + |> if m.IsNew then ctx.db.AddEntry else ctx.db.UpdateEntry let! _ = ctx.db.SaveChangesAsync () let s = Views.I18N.localizer.Force () - let act = if m.isNew () then "Added" else "Updated" - addInfo ctx s["Successfully {0} prayer request", s.[act].Value.ToLower ()] + let act = if m.IsNew then "Added" else "Updated" + addInfo ctx s["Successfully {0} prayer request", s[act].Value.ToLower ()] return! redirectTo false "/web/prayer-requests" next ctx | None -> return! fourOhFour next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } @@ -260,6 +260,6 @@ let view date : HttpHandler = requireAccess [ User; Group ] >=> fun next ctx -> let! list = generateRequestList ctx (parseListDate date) return! viewInfo ctx startTicks - |> Views.PrayerRequest.view { list with showHeader = false } + |> Views.PrayerRequest.view { list with ShowHeader = false } |> renderHtml next ctx } diff --git a/src/PrayerTracker/PrayerTracker.fsproj b/src/PrayerTracker/PrayerTracker.fsproj index 21f3089..fc4786d 100644 --- a/src/PrayerTracker/PrayerTracker.fsproj +++ b/src/PrayerTracker/PrayerTracker.fsproj @@ -1,7 +1,9 @@  - net6.0 + Exe + True + False @@ -23,9 +25,9 @@ - - - + + + diff --git a/src/PrayerTracker/SmallGroup.fs b/src/PrayerTracker/SmallGroup.fs index 07171a8..be21d04 100644 --- a/src/PrayerTracker/SmallGroup.fs +++ b/src/PrayerTracker/SmallGroup.fs @@ -21,7 +21,7 @@ let private setGroupCookie (ctx : HttpContext) pwHash = /// GET /small-group/announcement let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx -> - { viewInfo ctx DateTime.Now.Ticks with helpLink = Some Help.sendAnnouncement; script = [ "ckeditor/ckeditor" ] } + { viewInfo ctx DateTime.Now.Ticks with HelpLink = Some Help.sendAnnouncement; Script = [ "ckeditor/ckeditor" ] } |> Views.SmallGroup.announcement (currentUser ctx).isAdmin ctx |> renderHtml next ctx @@ -31,13 +31,13 @@ let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> let s = Views.I18N.localizer.Force () match! ctx.db.TryGroupById groupId with | Some grp -> - let! reqs = ctx.db.CountRequestsBySmallGroup groupId - let! usrs = ctx.db.CountUsersBySmallGroup groupId + let! reqs = ctx.db.CountRequestsBySmallGroup groupId + let! users = ctx.db.CountUsersBySmallGroup groupId ctx.db.RemoveEntry grp let! _ = ctx.db.SaveChangesAsync () addInfo ctx s["The group {0} and its {1} prayer request(s) were deleted successfully; revoked access from {2} user(s)", - grp.name, reqs, usrs] + grp.name, reqs, users] return! redirectTo false "/web/small-groups" next ctx | None -> return! fourOhFour next ctx } @@ -82,18 +82,18 @@ let editMember (memberId : MemberId) : HttpHandler = requireAccess [ User ] >=> let startTicks = DateTime.Now.Ticks let s = Views.I18N.localizer.Force () let grp = currentGroup ctx - let typs = ReferenceList.emailTypeList grp.preferences.defaultEmailType s + let types = ReferenceList.emailTypeList grp.preferences.defaultEmailType s if memberId = Guid.Empty then return! viewInfo ctx startTicks - |> Views.SmallGroup.editMember EditMember.empty typs ctx + |> Views.SmallGroup.editMember EditMember.empty types ctx |> renderHtml next ctx else match! ctx.db.TryMemberById memberId with | Some mbr when mbr.smallGroupId = grp.smallGroupId -> return! viewInfo ctx startTicks - |> Views.SmallGroup.editMember (EditMember.fromMember mbr) typs ctx + |> Views.SmallGroup.editMember (EditMember.fromMember mbr) types ctx |> renderHtml next ctx | Some _ | None -> return! fourOhFour next ctx @@ -102,12 +102,12 @@ let editMember (memberId : MemberId) : HttpHandler = requireAccess [ User ] >=> /// GET /small-group/log-on/[group-id?] let logOn (groupId : SmallGroupId option) : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { - let startTicks = DateTime.Now.Ticks - let! grps = ctx.db.ProtectedGroups () - let grpId = match groupId with Some gid -> flatGuid gid | None -> "" + let startTicks = DateTime.Now.Ticks + let! groups = ctx.db.ProtectedGroups () + let grpId = match groupId with Some gid -> flatGuid gid | None -> "" return! - { viewInfo ctx startTicks with helpLink = Some Help.logOn } - |> Views.SmallGroup.logOn grps grpId ctx + { viewInfo ctx startTicks with HelpLink = Some Help.logOn } + |> Views.SmallGroup.logOn groups grpId ctx |> renderHtml next ctx } @@ -117,28 +117,26 @@ let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validat match! ctx.TryBindFormAsync () with | Ok m -> let s = Views.I18N.localizer.Force () - match! ctx.db.TryGroupLogOnByPassword m.smallGroupId m.password with + match! ctx.db.TryGroupLogOnByPassword m.SmallGroupId m.Password with | Some grp -> ctx.Session.smallGroup <- Some grp - match m.rememberMe with - | Some x when x -> (setGroupCookie ctx << sha1Hash) m.password - | _ -> () + if defaultArg m.RememberMe false then (setGroupCookie ctx << sha1Hash) m.Password addInfo ctx s["Log On Successful • Welcome to {0}", s["PrayerTracker"]] return! redirectTo false "/web/prayer-requests/view" next ctx | None -> addError ctx s["Password incorrect - login unsuccessful"] - return! redirectTo false $"/web/small-group/log-on/{flatGuid m.smallGroupId}" next ctx - | Error e -> return! bindError e next ctx + return! redirectTo false $"/web/small-group/log-on/{flatGuid m.SmallGroupId}" next ctx + | Result.Error e -> return! bindError e next ctx } /// GET /small-groups let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let startTicks = DateTime.Now.Ticks - let! grps = ctx.db.AllGroups () + let! groups = ctx.db.AllGroups () return! viewInfo ctx startTicks - |> Views.SmallGroup.maintain grps ctx + |> Views.SmallGroup.maintain groups ctx |> renderHtml next ctx } @@ -148,11 +146,11 @@ let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let startTicks = DateTime.Now.Ticks let grp = currentGroup ctx let s = Views.I18N.localizer.Force () - let! mbrs = ctx.db.AllMembersForSmallGroup grp.smallGroupId - let typs = ReferenceList.emailTypeList grp.preferences.defaultEmailType s |> Map.ofSeq + let! members = ctx.db.AllMembersForSmallGroup grp.smallGroupId + let types = ReferenceList.emailTypeList grp.preferences.defaultEmailType s |> Map.ofSeq return! - { viewInfo ctx startTicks with helpLink = Some Help.maintainGroupMembers } - |> Views.SmallGroup.members mbrs typs ctx + { viewInfo ctx startTicks with HelpLink = Some Help.maintainGroupMembers } + |> Views.SmallGroup.members members types ctx |> renderHtml next ctx } @@ -165,10 +163,10 @@ let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let! reqCount = ctx.db.CountRequestsBySmallGroup (currentGroup ctx).smallGroupId let! mbrCount = ctx.db.CountMembersForSmallGroup (currentGroup ctx).smallGroupId let m = - { totalActiveReqs = List.length reqs - allReqs = reqCount - totalMbrs = mbrCount - activeReqsByCat = + { TotalActiveReqs = List.length reqs + AllReqs = reqCount + TotalMembers = mbrCount + ActiveReqsByType = (reqs |> Seq.ofList |> Seq.map (fun req -> req.requestType) @@ -188,7 +186,7 @@ let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task let startTicks = DateTime.Now.Ticks let! tzs = ctx.db.AllTimeZones () return! - { viewInfo ctx startTicks with helpLink = Some Help.groupPreferences } + { viewInfo ctx startTicks with HelpLink = Some Help.groupPreferences } |> Views.SmallGroup.preferences (EditPreferences.fromPreferences (currentGroup ctx).preferences) tzs ctx |> renderHtml next ctx } @@ -200,22 +198,22 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c | Ok m -> let s = Views.I18N.localizer.Force () let! group = - if m.isNew () then Task.FromResult (Some { SmallGroup.empty with smallGroupId = Guid.NewGuid () }) - else ctx.db.TryGroupById m.smallGroupId + if m.IsNew then Task.FromResult (Some { SmallGroup.empty with smallGroupId = Guid.NewGuid () }) + else ctx.db.TryGroupById m.SmallGroupId match group with | Some grp -> m.populateGroup grp |> function - | grp when m.isNew () -> + | grp when m.IsNew -> ctx.db.AddEntry grp ctx.db.AddEntry { grp.preferences with smallGroupId = grp.smallGroupId } | grp -> ctx.db.UpdateEntry grp let! _ = ctx.db.SaveChangesAsync () - let act = s[if m.isNew () then "Added" else "Updated"].Value.ToLower () - addHtmlInfo ctx s["Successfully {0} group “{1}”", act, m.name] + let act = s[if m.IsNew then "Added" else "Updated"].Value.ToLower () + addHtmlInfo ctx s["Successfully {0} group “{1}”", act, m.Name] return! redirectTo false "/web/small-groups" next ctx | None -> return! fourOhFour next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } @@ -225,25 +223,25 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun n | Ok m -> let grp = currentGroup ctx let! mMbr = - if m.isNew () then + if m.IsNew then Task.FromResult (Some { Member.empty with memberId = Guid.NewGuid (); smallGroupId = grp.smallGroupId }) - else ctx.db.TryMemberById m.memberId + else ctx.db.TryMemberById m.MemberId match mMbr with | Some mbr when mbr.smallGroupId = grp.smallGroupId -> { mbr with - memberName = m.memberName - email = m.emailAddress - format = match m.emailType with "" | null -> None | _ -> Some m.emailType + memberName = m.Name + email = m.Email + format = match m.Format with "" | null -> None | _ -> Some m.Format } - |> (if m.isNew () then ctx.db.AddEntry else ctx.db.UpdateEntry) + |> if m.IsNew then ctx.db.AddEntry else ctx.db.UpdateEntry let! _ = ctx.db.SaveChangesAsync () let s = Views.I18N.localizer.Force () - let act = s[if m.isNew () then "Added" else "Updated"].Value.ToLower () + let act = s[if m.IsNew then "Added" else "Updated"].Value.ToLower () addInfo ctx s["Successfully {0} group member", act] return! redirectTo false "/web/small-group/members" next ctx | Some _ | None -> return! fourOhFour next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } @@ -256,7 +254,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> // database values, not the then out-of-sync session ones. match! ctx.db.TryGroupById (currentGroup ctx).smallGroupId with | Some grp -> - let prefs = m.populatePreferences grp.preferences + let prefs = m.PopulatePreferences grp.preferences ctx.db.UpdateEntry prefs let! _ = ctx.db.SaveChangesAsync () // Refresh session instance @@ -265,7 +263,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> addInfo ctx s["Group preferences updated successfully"] return! redirectTo false "/web/small-group/preferences" next ctx | None -> return! fourOhFour next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } @@ -279,7 +277,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> let now = grp.localTimeNow (ctx.GetService ()) let s = Views.I18N.localizer.Force () // Reformat the text to use the class's font stylings - let requestText = ckEditorToText m.text + let requestText = ckEditorToText m.Text let htmlText = p [ _style $"font-family:{grp.preferences.listFonts};font-size:%d{grp.preferences.textFontSize}pt;" ] [ rawText requestText ] @@ -287,7 +285,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> let plainText = (htmlToPlainText >> wordWrap 74) htmlText // Send the e-mails let! recipients = - match m.sendToClass with + match m.SendToClass with | "N" when usr.isAdmin -> ctx.db.AllUsersAsMembers () | _ -> ctx.db.AllMembersForSmallGroup grp.smallGroupId use! client = Email.getConnection () @@ -296,7 +294,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> (now.ToString "h:mm tt").ToLower ()].Value htmlText plainText s // Add to the request list if desired - match m.sendToClass, m.addToRequestList with + match m.SendToClass, m.AddToRequestList with | "N", _ | _, None -> () | _, Some x when not x -> () @@ -305,7 +303,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> prayerRequestId = Guid.NewGuid () smallGroupId = grp.smallGroupId userId = usr.userId - requestType = (Option.get >> PrayerRequestType.fromCode) m.requestType + requestType = (Option.get >> PrayerRequestType.fromCode) m.RequestType text = requestText enteredDate = now updatedDate = now @@ -315,14 +313,14 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> () // Tell 'em what they've won, Johnny! let toWhom = - match m.sendToClass with + match m.SendToClass with | "N" -> s["{0} users", s["PrayerTracker"]].Value | _ -> s["Group Members"].Value.ToLower () - let andAdded = match m.addToRequestList with Some x when x -> "and added it to the request list" | _ -> "" + let andAdded = match m.AddToRequestList with Some x when x -> "and added it to the request list" | _ -> "" addInfo ctx s["Successfully sent announcement to all {0} {1}", toWhom, s[andAdded]] return! viewInfo ctx startTicks - |> Views.SmallGroup.announcementSent { m with text = htmlText } + |> Views.SmallGroup.announcementSent { m with Text = htmlText } |> renderHtml next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } diff --git a/src/PrayerTracker/User.fs b/src/PrayerTracker/User.fs index f73d783..1e7eef3 100644 --- a/src/PrayerTracker/User.fs +++ b/src/PrayerTracker/User.fs @@ -23,18 +23,18 @@ let private setUserCookie (ctx : HttpContext) pwHash = /// Retrieve a user from the database by password // If the hashes do not match, determine if it matches a previous scheme, and upgrade them if it does let private findUserByPassword m (db : AppDbContext) = task { - match! db.TryUserByEmailAndGroup m.emailAddress m.smallGroupId with + match! db.TryUserByEmailAndGroup m.Email m.SmallGroupId with | Some u when Option.isSome u.salt -> // Already upgraded; match = success - let pwHash = pbkdf2Hash (Option.get u.salt) m.password + let pwHash = pbkdf2Hash (Option.get u.salt) m.Password if u.passwordHash = pwHash then return Some { u with passwordHash = ""; salt = None; smallGroups = List() }, pwHash else return None, "" - | Some u when u.passwordHash = sha1Hash m.password -> + | Some u when u.passwordHash = sha1Hash m.Password -> // Not upgraded, but password is good; upgrade 'em! // Upgrade 'em! let salt = Guid.NewGuid () - let pwHash = pbkdf2Hash salt m.password + let pwHash = pbkdf2Hash salt m.Password let upgraded = { u with salt = Some salt; passwordHash = pwHash } db.UpdateEntry upgraded let! _ = db.SaveChangesAsync () @@ -54,16 +54,16 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> f match dbUsr with | Some usr -> // Check the old password against a possibly non-salted hash - (match usr.salt with Some salt -> pbkdf2Hash salt | None -> sha1Hash) m.oldPassword + (match usr.salt with Some salt -> pbkdf2Hash salt | None -> sha1Hash) m.OldPassword |> ctx.db.TryUserLogOnByCookie curUsr.userId (currentGroup ctx).smallGroupId | _ -> Task.FromResult None match user with - | Some _ when m.newPassword = m.newPasswordConfirm -> + | Some _ when m.NewPassword = m.NewPasswordConfirm -> match dbUsr with | Some usr -> // Generate new salt whenever the password is changed let salt = Guid.NewGuid () - ctx.db.UpdateEntry { usr with passwordHash = pbkdf2Hash salt m.newPassword; salt = Some salt } + ctx.db.UpdateEntry { usr with passwordHash = pbkdf2Hash salt m.NewPassword; salt = Some salt } let! _ = ctx.db.SaveChangesAsync () // If the user is remembered, update the cookie with the new hash if ctx.Request.Cookies.Keys.Contains Key.Cookie.user then setUserCookie ctx usr.passwordHash @@ -76,7 +76,7 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> f | None -> addError ctx s["The old password was incorrect - your password was NOT changed"] return! redirectTo false "/web/user/password" next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } @@ -99,26 +99,26 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSR | Ok m -> let s = Views.I18N.localizer.Force () let! usr, pwHash = findUserByPassword m ctx.db - let! grp = ctx.db.TryGroupById m.smallGroupId + let! grp = ctx.db.TryGroupById m.SmallGroupId let nextUrl = match usr with | Some _ -> ctx.Session.user <- usr ctx.Session.smallGroup <- grp - match m.rememberMe with Some x when x -> setUserCookie ctx pwHash | _ -> () + if defaultArg m.RememberMe false then setUserCookie ctx pwHash addHtmlInfo ctx s["Log On Successful • Welcome to {0}", s["PrayerTracker"]] - match m.redirectUrl with + match m.RedirectUrl with | None -> "/web/small-group" | Some x when x = "" -> "/web/small-group" | Some x -> x | _ -> let grpName = match grp with Some g -> g.name | _ -> "N/A" { UserMessage.error with - text = htmlLocString s["Invalid credentials - log on unsuccessful"] - description = + Text = htmlLocString s["Invalid credentials - log on unsuccessful"] + Description = [ s["This is likely due to one of the following reasons"].Value ":
  • " - s["The e-mail address “{0}” is invalid.", WebUtility.HtmlEncode m.emailAddress].Value + s["The e-mail address “{0}” is invalid.", WebUtility.HtmlEncode m.Email].Value "
  • " s["The password entered does not match the password for the given e-mail address."].Value "
  • " @@ -132,7 +132,7 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSR |> addUserMessage ctx "/web/user/log-on" return! redirectTo false nextUrl next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } @@ -167,8 +167,8 @@ let logOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx addWarning ctx s["The page you requested requires authentication; please log on below."] | None -> () return! - { viewInfo ctx startTicks with helpLink = Some Help.logOn } - |> Views.User.logOn { UserLogOn.empty with redirectUrl = url } groups ctx + { viewInfo ctx startTicks with HelpLink = Some Help.logOn } + |> Views.User.logOn { UserLogOn.empty with RedirectUrl = url } groups ctx |> renderHtml next ctx } @@ -186,7 +186,7 @@ let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { /// GET /user/password let password : HttpHandler = requireAccess [ User ] >=> fun next ctx -> - { viewInfo ctx DateTime.Now.Ticks with helpLink = Some Help.changePassword } + { viewInfo ctx DateTime.Now.Ticks with HelpLink = Some Help.changePassword } |> Views.User.changePassword ctx |> renderHtml next ctx @@ -196,13 +196,13 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c match! ctx.TryBindFormAsync () with | Ok m -> let! user = - if m.isNew () then Task.FromResult (Some { User.empty with userId = Guid.NewGuid () }) - else ctx.db.TryUserById m.userId + if m.IsNew then Task.FromResult (Some { User.empty with userId = Guid.NewGuid () }) + else ctx.db.TryUserById m.UserId let saltedUser = match user with | Some u -> match u.salt with - | None when m.password <> "" -> + | None when m.Password <> "" -> // Generate salt so that a new password hash can be generated Some { u with salt = Some (Guid.NewGuid ()) } | _ -> @@ -211,15 +211,15 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c | _ -> user match saltedUser with | Some u -> - let updatedUser = m.populateUser u (pbkdf2Hash (Option.get u.salt)) - updatedUser |> (if m.isNew () then ctx.db.AddEntry else ctx.db.UpdateEntry) + let updatedUser = m.PopulateUser u (pbkdf2Hash (Option.get u.salt)) + updatedUser |> if m.IsNew then ctx.db.AddEntry else ctx.db.UpdateEntry let! _ = ctx.db.SaveChangesAsync () let s = Views.I18N.localizer.Force () - if m.isNew () then + if m.IsNew then let h = CommonFunctions.htmlString { UserMessage.info with - text = h s["Successfully {0} user", s["Added"].Value.ToLower ()] - description = + Text = h s["Successfully {0} user", s["Added"].Value.ToLower ()] + Description = h s["Please select at least one group for which this user ({0}) is authorized", updatedUser.fullName] |> Some @@ -230,7 +230,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c addInfo ctx s["Successfully {0} user", s["Updated"].Value.ToLower ()] return! redirectTo false "/web/users" next ctx | None -> return! fourOhFour next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } @@ -239,31 +239,31 @@ let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun match! ctx.TryBindFormAsync () with | Ok m -> let s = Views.I18N.localizer.Force () - match Seq.length m.smallGroups with + match Seq.length m.SmallGroups with | 0 -> addError ctx s["You must select at least one group to assign"] - return! redirectTo false $"/web/user/{flatGuid m.userId}/small-groups" next ctx + return! redirectTo false $"/web/user/{flatGuid m.UserId}/small-groups" next ctx | _ -> - match! ctx.db.TryUserByIdWithGroups m.userId with + match! ctx.db.TryUserByIdWithGroups m.UserId with | Some user -> - let grps = - m.smallGroups.Split ',' + let groups = + m.SmallGroups.Split ',' |> Array.map Guid.Parse |> List.ofArray user.smallGroups - |> Seq.filter (fun x -> not (grps |> List.exists (fun y -> y = x.smallGroupId))) + |> Seq.filter (fun x -> not (groups |> List.exists (fun y -> y = x.smallGroupId))) |> ctx.db.UserGroupXref.RemoveRange - grps + groups |> Seq.ofList |> Seq.filter (fun x -> not (user.smallGroups |> Seq.exists (fun y -> y.smallGroupId = x))) |> Seq.map (fun x -> { UserSmallGroup.empty with userId = user.userId; smallGroupId = x }) |> List.ofSeq |> List.iter ctx.db.AddEntry let! _ = ctx.db.SaveChangesAsync () - addInfo ctx s["Successfully updated group permissions for {0}", m.userName] + addInfo ctx s["Successfully updated group permissions for {0}", m.UserName] return! redirectTo false "/web/users" next ctx | _ -> return! fourOhFour next ctx - | Error e -> return! bindError e next ctx + | Result.Error e -> return! bindError e next ctx } @@ -272,11 +272,11 @@ let smallGroups userId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx let startTicks = DateTime.Now.Ticks match! ctx.db.TryUserByIdWithGroups userId with | Some user -> - let! grps = ctx.db.GroupList () + let! groups = ctx.db.GroupList () let curGroups = user.smallGroups |> Seq.map (fun g -> flatGuid g.smallGroupId) |> List.ofSeq return! viewInfo ctx startTicks - |> Views.User.assignGroups (AssignGroups.fromUser user) grps curGroups ctx + |> Views.User.assignGroups (AssignGroups.fromUser user) groups curGroups ctx |> renderHtml next ctx | None -> return! fourOhFour next ctx } diff --git a/src/PrayerTracker/wwwroot/js/app.js b/src/PrayerTracker/wwwroot/js/app.js index 4a9a8e7..e0831b7 100644 --- a/src/PrayerTracker/wwwroot/js/app.js +++ b/src/PrayerTracker/wwwroot/js/app.js @@ -210,8 +210,8 @@ const PT = { */ toggleType(name) { const isNamed = document.getElementById(`${name}Type_Name`) - const named = document.getElementById(`${name}Color_Select`) - const custom = document.getElementById(`${name}Color_Color`) + const named = document.getElementById(`${name}_Select`) + const custom = document.getElementById(`${name}_Color`) if (isNamed.checked) { custom.disabled = true named.disabled = false @@ -244,7 +244,7 @@ const PT = { PT.smallGroup.preferences.checkVisibility) }) PT.smallGroup.preferences.checkVisibility() - ;['headingLine', 'headingText'].map(name => { + ;['lineColor', 'headingColor'].map(name => { document.getElementById(`${name}Type_Name`).addEventListener('click', () => { PT.smallGroup.preferences.toggleType(name) })