Version 8 #43

Merged
danieljsummers merged 37 commits from version-8 into main 2022-08-19 19:08:31 +00:00
27 changed files with 1903 additions and 1734 deletions
Showing only changes of commit e1bdad15f7 - Show all commits

View File

@ -1,9 +1,11 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<AssemblyVersion>7.6.0.0</AssemblyVersion> <TargetFramework>net6.0</TargetFramework>
<FileVersion>7.6.0.0</FileVersion> <AssemblyVersion>8.0.0.0</AssemblyVersion>
<FileVersion>8.0.0.0</FileVersion>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<Version>7.6.0</Version> <Version>8.0.0</Version>
<DebugType>Embedded</DebugType>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -20,10 +20,11 @@ module private Helpers =
.ThenByDescending (fun req -> req.enteredDate) .ThenByDescending (fun req -> req.enteredDate)
/// Paginate a prayer request query /// Paginate a prayer request query
let paginate pageNbr pageSize (q : IQueryable<PrayerRequest>) = let paginate (pageNbr : int) pageSize (q : IQueryable<PrayerRequest>) =
q.Skip((pageNbr - 1) * pageSize).Take pageSize if pageNbr > 0 then q.Skip((pageNbr - 1) * pageSize).Take pageSize else q
open System
open System.Collections.Generic open System.Collections.Generic
open Microsoft.EntityFrameworkCore open Microsoft.EntityFrameworkCore
open Microsoft.FSharpLu open Microsoft.FSharpLu
@ -95,7 +96,7 @@ type AppDbContext with
this.PrayerRequests.Where(fun req -> req.smallGroupId = grp.smallGroupId) this.PrayerRequests.Where(fun req -> req.smallGroupId = grp.smallGroupId)
|> function |> function
| q when activeOnly -> | 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 -> q.Where(fun req ->
( req.updatedDate > asOf ( req.updatedDate > asOf
|| req.expiration = Manual || req.expiration = Manual

View File

@ -642,18 +642,25 @@ with
and [<CLIMutable; NoComparison; NoEquality>] SmallGroup = and [<CLIMutable; NoComparison; NoEquality>] SmallGroup =
{ /// The Id of this small group { /// The Id of this small group
smallGroupId : SmallGroupId smallGroupId : SmallGroupId
/// The church to which this group belongs /// The church to which this group belongs
churchId : ChurchId churchId : ChurchId
/// The name of the group /// The name of the group
name : string name : string
/// The church to which this small group belongs /// The church to which this small group belongs
church : Church church : Church
/// The preferences for the request list /// The preferences for the request list
preferences : ListPreferences preferences : ListPreferences
/// The members of the group /// The members of the group
members : ICollection<Member> members : ICollection<Member>
/// Prayer requests for this small group /// Prayer requests for this small group
prayerRequests : ICollection<PrayerRequest> prayerRequests : ICollection<PrayerRequest>
/// The users authorized to manage this group /// The users authorized to manage this group
users : ICollection<UserSmallGroup> users : ICollection<UserSmallGroup>
} }
@ -699,10 +706,13 @@ with
and [<CLIMutable; NoComparison; NoEquality>] TimeZone = and [<CLIMutable; NoComparison; NoEquality>] TimeZone =
{ /// The Id for this time zone (uses tzdata names) { /// The Id for this time zone (uses tzdata names)
timeZoneId : TimeZoneId timeZoneId : TimeZoneId
/// The description of this time zone /// The description of this time zone
description : string description : string
/// The order in which this timezone should be displayed /// The order in which this timezone should be displayed
sortOrder : int sortOrder : int
/// Whether this timezone is active /// Whether this timezone is active
isActive : bool isActive : bool
} }
@ -731,18 +741,25 @@ with
and [<CLIMutable; NoComparison; NoEquality>] User = and [<CLIMutable; NoComparison; NoEquality>] User =
{ /// The Id of this user { /// The Id of this user
userId : UserId userId : UserId
/// The first name of this user /// The first name of this user
firstName : string firstName : string
/// The last name of this user /// The last name of this user
lastName : string lastName : string
/// The e-mail address of the user /// The e-mail address of the user
emailAddress : string emailAddress : string
/// Whether this user is a PrayerTracker system administrator /// Whether this user is a PrayerTracker system administrator
isAdmin : bool isAdmin : bool
/// The user's hashed password /// The user's hashed password
passwordHash : string passwordHash : string
/// The salt for the user's hashed password /// The salt for the user's hashed password
salt : Guid option salt : Guid option
/// The small groups which this user is authorized /// The small groups which this user is authorized
smallGroups : ICollection<UserSmallGroup> smallGroups : ICollection<UserSmallGroup>
} }
@ -785,10 +802,13 @@ with
and [<CLIMutable; NoComparison; NoEquality>] UserSmallGroup = and [<CLIMutable; NoComparison; NoEquality>] UserSmallGroup =
{ /// The Id of the user who has access to the small group { /// The Id of the user who has access to the small group
userId : UserId userId : UserId
/// The Id of the small group to which the user has access /// The Id of the small group to which the user has access
smallGroupId : SmallGroupId smallGroupId : SmallGroupId
/// The user who has access to the small group /// The user who has access to the small group
user : User user : User
/// The small group to which the user has access /// The small group to which the user has access
smallGroup : SmallGroup smallGroup : SmallGroup
} }

View File

@ -15,8 +15,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FSharp.EFCore.OptionConverter" Version="1.0.0" /> <PackageReference Include="FSharp.EFCore.OptionConverter" Version="1.0.0" />
<PackageReference Include="Microsoft.FSharpLu" Version="0.11.7" /> <PackageReference Include="Microsoft.FSharpLu" Version="0.11.7" />
<PackageReference Include="NodaTime" Version="3.0.5" /> <PackageReference Include="NodaTime" Version="3.1.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.10" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
<PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -7,356 +7,369 @@ open System
[<Tests>] [<Tests>]
let asOfDateDisplayTests = let asOfDateDisplayTests =
testList "AsOfDateDisplay" [ testList "AsOfDateDisplay" [
test "NoDisplay code is correct" { test "NoDisplay code is correct" {
Expect.equal NoDisplay.code "N" "The code for NoDisplay should have been \"N\"" Expect.equal NoDisplay.code "N" "The code for NoDisplay should have been \"N\""
} }
test "ShortDate code is correct" { test "ShortDate code is correct" {
Expect.equal ShortDate.code "S" "The code for ShortDate should have been \"S\"" Expect.equal ShortDate.code "S" "The code for ShortDate should have been \"S\""
} }
test "LongDate code is correct" { test "LongDate code is correct" {
Expect.equal LongDate.code "L" "The code for LongDate should have been \"N\"" Expect.equal LongDate.code "L" "The code for LongDate should have been \"N\""
} }
test "fromCode N should return NoDisplay" { test "fromCode N should return NoDisplay" {
Expect.equal (AsOfDateDisplay.fromCode "N") NoDisplay "\"N\" should have been converted to NoDisplay" Expect.equal (AsOfDateDisplay.fromCode "N") NoDisplay "\"N\" should have been converted to NoDisplay"
} }
test "fromCode S should return ShortDate" { test "fromCode S should return ShortDate" {
Expect.equal (AsOfDateDisplay.fromCode "S") ShortDate "\"S\" should have been converted to ShortDate" Expect.equal (AsOfDateDisplay.fromCode "S") ShortDate "\"S\" should have been converted to ShortDate"
} }
test "fromCode L should return LongDate" { test "fromCode L should return LongDate" {
Expect.equal (AsOfDateDisplay.fromCode "L") LongDate "\"L\" should have been converted to LongDate" Expect.equal (AsOfDateDisplay.fromCode "L") LongDate "\"L\" should have been converted to LongDate"
} }
test "fromCode X should raise" { test "fromCode X should raise" {
Expect.throws (fun () -> AsOfDateDisplay.fromCode "X" |> ignore) Expect.throws (fun () -> AsOfDateDisplay.fromCode "X" |> ignore)
"An unknown code should have raised an exception" "An unknown code should have raised an exception"
} }
] ]
[<Tests>] [<Tests>]
let churchTests = let churchTests =
testList "Church" [ testList "Church" [
test "empty is as expected" { test "empty is as expected" {
let mt = Church.empty let mt = Church.empty
Expect.equal mt.churchId Guid.Empty "The church 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.name "" "The name should have been blank"
Expect.equal mt.city "" "The city 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.equal mt.st "" "The state should have been blank"
Expect.isFalse mt.hasInterface "The church should not show that it has an interface" 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.isNone mt.interfaceAddress "The interface address should not exist"
Expect.isNotNull mt.smallGroups "The small groups navigation property should not be null" 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" Expect.isEmpty mt.smallGroups "There should be no small groups for an empty church"
} }
] ]
[<Tests>] [<Tests>]
let emailFormatTests = let emailFormatTests =
testList "EmailFormat" [ testList "EmailFormat" [
test "HtmlFormat code is correct" { test "HtmlFormat code is correct" {
Expect.equal HtmlFormat.code "H" "The code for HtmlFormat should have been \"H\"" Expect.equal HtmlFormat.code "H" "The code for HtmlFormat should have been \"H\""
} }
test "PlainTextFormat code is correct" { test "PlainTextFormat code is correct" {
Expect.equal PlainTextFormat.code "P" "The code for PlainTextFormat should have been \"P\"" Expect.equal PlainTextFormat.code "P" "The code for PlainTextFormat should have been \"P\""
} }
test "fromCode H should return HtmlFormat" { test "fromCode H should return HtmlFormat" {
Expect.equal (EmailFormat.fromCode "H") HtmlFormat "\"H\" should have been converted to HtmlFormat" Expect.equal (EmailFormat.fromCode "H") HtmlFormat "\"H\" should have been converted to HtmlFormat"
} }
test "fromCode P should return ShortDate" { test "fromCode P should return ShortDate" {
Expect.equal (EmailFormat.fromCode "P") PlainTextFormat "\"P\" should have been converted to PlainTextFormat" 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" test "fromCode Z should raise" {
} Expect.throws (fun () -> EmailFormat.fromCode "Z" |> ignore)
"An unknown code should have raised an exception"
}
] ]
[<Tests>] [<Tests>]
let expirationTests = let expirationTests =
testList "Expiration" [ testList "Expiration" [
test "Automatic code is correct" { test "Automatic code is correct" {
Expect.equal Automatic.code "A" "The code for Automatic should have been \"A\"" Expect.equal Automatic.code "A" "The code for Automatic should have been \"A\""
} }
test "Manual code is correct" { test "Manual code is correct" {
Expect.equal Manual.code "M" "The code for Manual should have been \"M\"" Expect.equal Manual.code "M" "The code for Manual should have been \"M\""
} }
test "Forced code is correct" { test "Forced code is correct" {
Expect.equal Forced.code "F" "The code for Forced should have been \"F\"" Expect.equal Forced.code "F" "The code for Forced should have been \"F\""
} }
test "fromCode A should return Automatic" { test "fromCode A should return Automatic" {
Expect.equal (Expiration.fromCode "A") Automatic "\"A\" should have been converted to Automatic" Expect.equal (Expiration.fromCode "A") Automatic "\"A\" should have been converted to Automatic"
} }
test "fromCode M should return Manual" { test "fromCode M should return Manual" {
Expect.equal (Expiration.fromCode "M") Manual "\"M\" should have been converted to Manual" Expect.equal (Expiration.fromCode "M") Manual "\"M\" should have been converted to Manual"
} }
test "fromCode F should return Forced" { test "fromCode F should return Forced" {
Expect.equal (Expiration.fromCode "F") Forced "\"F\" should have been converted to Forced" Expect.equal (Expiration.fromCode "F") Forced "\"F\" should have been converted to Forced"
} }
test "fromCode V should raise" { test "fromCode V should raise" {
Expect.throws (fun () -> Expiration.fromCode "V" |> ignore) "An unknown code should have raised an exception" Expect.throws (fun () -> Expiration.fromCode "V" |> ignore)
} "An unknown code should have raised an exception"
}
] ]
[<Tests>] [<Tests>]
let listPreferencesTests = let listPreferencesTests =
testList "ListPreferences" [ testList "ListPreferences" [
test "empty is as expected" { test "empty is as expected" {
let mt = ListPreferences.empty let mt = ListPreferences.empty
Expect.equal mt.smallGroupId Guid.Empty "The small group 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.daysToExpire 14 "The default days to expire should have been 14" 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.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.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.emailFromName "PrayerTracker" "The default e-mail from name should have been PrayerTracker"
Expect.equal mt.emailFromAddress "prayer@djs-consulting.com" Expect.equal mt.emailFromAddress "prayer@djs-consulting.com"
"The default e-mail from address should have been 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.listFonts "Century Gothic,Tahoma,Luxi Sans,sans-serif"
Expect.equal mt.headingColor "maroon" "The default heading text color should have been maroon" "The default list fonts were incorrect"
Expect.equal mt.lineColor "navy" "The default heding line color should have been navy" Expect.equal mt.headingColor "maroon" "The default heading text color should have been maroon"
Expect.equal mt.headingFontSize 16 "The default heading font size should have been 16" Expect.equal mt.lineColor "navy" "The default heding line color should have been navy"
Expect.equal mt.textFontSize 12 "The default text font size should have been 12" Expect.equal mt.headingFontSize 16 "The default heading font size should have been 16"
Expect.equal mt.requestSort SortByDate "The default request sort should have been by date" Expect.equal mt.textFontSize 12 "The default text font size should have been 12"
Expect.equal mt.groupPassword "" "The default group password should have been blank" Expect.equal mt.requestSort SortByDate "The default request sort should have been by date"
Expect.equal mt.defaultEmailType HtmlFormat "The default e-mail type should have been HTML" Expect.equal mt.groupPassword "" "The default group password should have been blank"
Expect.isFalse mt.isPublic "The isPublic flag should not have been set" Expect.equal mt.defaultEmailType HtmlFormat "The default e-mail type should have been HTML"
Expect.equal mt.timeZoneId "America/Denver" "The default time zone should have been America/Denver" Expect.isFalse mt.isPublic "The isPublic flag should not have been set"
Expect.equal mt.timeZone.timeZoneId "" "The default preferences should have included an empty time zone" Expect.equal mt.timeZoneId "America/Denver" "The default time zone should have been America/Denver"
Expect.equal mt.pageSize 100 "The default page size should have been 100" Expect.equal mt.timeZone.timeZoneId "" "The default preferences should have included an empty time zone"
Expect.equal mt.asOfDateDisplay NoDisplay "The as-of date display should have been No Display" 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"
}
] ]
[<Tests>] [<Tests>]
let memberTests = let memberTests =
testList "Member" [ testList "Member" [
test "empty is as expected" { test "empty is as expected" {
let mt = Member.empty let mt = Member.empty
Expect.equal mt.memberId Guid.Empty "The member ID should have been an empty GUID" 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.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.memberName "" "The member name should have been blank"
Expect.equal mt.email "" "The member e-mail address 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.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" Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one"
} }
] ]
[<Tests>] [<Tests>]
let prayerRequestTests = let prayerRequestTests =
testList "PrayerRequest" [ testList "PrayerRequest" [
test "empty is as expected" { test "empty is as expected" {
let mt = PrayerRequest.empty let mt = PrayerRequest.empty
Expect.equal mt.prayerRequestId Guid.Empty "The request ID should have been an empty GUID" 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.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.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.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.enteredDate DateTime.MinValue "The entered date should have been the minimum"
Expect.equal mt.updatedDate DateTime.MinValue "The updated date should have been the minimum" Expect.equal mt.updatedDate DateTime.MinValue "The updated date should have been the minimum"
Expect.isNone mt.requestor "The requestor should not exist" Expect.isNone mt.requestor "The requestor should not exist"
Expect.equal mt.text "" "The request text should have been blank" 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.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.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.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" Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one"
} }
test "isExpired always returns false for expecting requests" { test "isExpired always returns false for expecting requests" {
let req = { PrayerRequest.empty with requestType = Expecting } let req = { PrayerRequest.empty with requestType = Expecting }
Expect.isFalse (req.isExpired DateTime.Now 0) "An expecting request should never be considered expired" Expect.isFalse (req.isExpired DateTime.Now 0) "An expecting request should never be considered expired"
} }
test "isExpired always returns false for manually-expired requests" { test "isExpired always returns false for manually-expired requests" {
let req = { PrayerRequest.empty with updatedDate = DateTime.Now.AddMonths -1; expiration = Manual } 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" 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" { test "isExpired always returns false for long term/recurring requests" {
let req = { PrayerRequest.empty with requestType = LongTermRequest } let req = { PrayerRequest.empty with requestType = LongTermRequest }
Expect.isFalse (req.isExpired DateTime.Now 0) "A recurring/long-term request should never be considered expired" 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 } test "isExpired always returns true for force-expired requests" {
Expect.isTrue (req.isExpired DateTime.Now 5) "A force-expired request should always be considered expired" 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 test "isExpired returns false for non-expired requests" {
let req = { PrayerRequest.empty with updatedDate = now.AddDays -5. } let now = DateTime.Now
Expect.isFalse (req.isExpired now 7) "A request updated 5 days ago should not be considered expired" 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 test "isExpired returns true for expired requests" {
let req = { PrayerRequest.empty with updatedDate = now.AddDays -8. } let now = DateTime.Now
Expect.isTrue (req.isExpired now 7) "A request updated 8 days ago should be considered expired" 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 test "isExpired returns true for same-day expired requests" {
let req = { PrayerRequest.empty with updatedDate = now.Date.AddDays(-7.).AddSeconds -1. } let now = DateTime.Now
Expect.isTrue (req.isExpired now 7) "A request entered a second before midnight should be considered expired" let req = { PrayerRequest.empty with updatedDate = now.Date.AddDays(-7.).AddSeconds -1. }
} Expect.isTrue (req.isExpired now 7)
test "updateRequired returns false for expired requests" { "A request entered a second before midnight should be considered expired"
let req = { PrayerRequest.empty with expiration = Forced } }
Expect.isFalse (req.updateRequired DateTime.Now 7 4) "An expired request should not require an update" test "updateRequired returns false for expired requests" {
} let req = { PrayerRequest.empty with expiration = Forced }
test "updateRequired returns false when an update is not required for an active request" { Expect.isFalse (req.updateRequired DateTime.Now 7 4) "An expired request should not require an update"
let now = DateTime.Now }
let req = test "updateRequired returns false when an update is not required for an active request" {
{ PrayerRequest.empty with let now = DateTime.Now
requestType = LongTermRequest let req =
updatedDate = now.AddDays -14. { PrayerRequest.empty with
} requestType = LongTermRequest
Expect.isFalse (req.updateRequired now 7 4) updatedDate = now.AddDays -14.
"An active request updated 14 days ago should not require an update until 28 days" }
} Expect.isFalse (req.updateRequired now 7 4)
test "updateRequired returns true when an update is required for an active request" { "An active request updated 14 days ago should not require an update until 28 days"
let now = DateTime.Now }
let req = test "updateRequired returns true when an update is required for an active request" {
{ PrayerRequest.empty with let now = DateTime.Now
requestType = LongTermRequest let req =
updatedDate = now.AddDays -34. { PrayerRequest.empty with
} requestType = LongTermRequest
Expect.isTrue (req.updateRequired now 7 4) updatedDate = now.AddDays -34.
"An active request updated 34 days ago should require an update (past 28 days)" }
} Expect.isTrue (req.updateRequired now 7 4)
"An active request updated 34 days ago should require an update (past 28 days)"
}
] ]
[<Tests>] [<Tests>]
let prayerRequestTypeTests = let prayerRequestTypeTests =
testList "PrayerRequestType" [ testList "PrayerRequestType" [
test "CurrentRequest code is correct" { test "CurrentRequest code is correct" {
Expect.equal CurrentRequest.code "C" "The code for CurrentRequest should have been \"C\"" Expect.equal CurrentRequest.code "C" "The code for CurrentRequest should have been \"C\""
} }
test "LongTermRequest code is correct" { test "LongTermRequest code is correct" {
Expect.equal LongTermRequest.code "L" "The code for LongTermRequest should have been \"L\"" Expect.equal LongTermRequest.code "L" "The code for LongTermRequest should have been \"L\""
} }
test "PraiseReport code is correct" { test "PraiseReport code is correct" {
Expect.equal PraiseReport.code "P" "The code for PraiseReport should have been \"P\"" Expect.equal PraiseReport.code "P" "The code for PraiseReport should have been \"P\""
} }
test "Expecting code is correct" { test "Expecting code is correct" {
Expect.equal Expecting.code "E" "The code for Expecting should have been \"E\"" Expect.equal Expecting.code "E" "The code for Expecting should have been \"E\""
} }
test "Announcement code is correct" { test "Announcement code is correct" {
Expect.equal Announcement.code "A" "The code for Announcement should have been \"A\"" Expect.equal Announcement.code "A" "The code for Announcement should have been \"A\""
} }
test "fromCode C should return CurrentRequest" { test "fromCode C should return CurrentRequest" {
Expect.equal (PrayerRequestType.fromCode "C") CurrentRequest Expect.equal (PrayerRequestType.fromCode "C") CurrentRequest
"\"C\" should have been converted to CurrentRequest" "\"C\" should have been converted to CurrentRequest"
} }
test "fromCode L should return LongTermRequest" { test "fromCode L should return LongTermRequest" {
Expect.equal (PrayerRequestType.fromCode "L") LongTermRequest Expect.equal (PrayerRequestType.fromCode "L") LongTermRequest
"\"L\" should have been converted to LongTermRequest" "\"L\" should have been converted to LongTermRequest"
} }
test "fromCode P should return PraiseReport" { test "fromCode P should return PraiseReport" {
Expect.equal (PrayerRequestType.fromCode "P") PraiseReport "\"P\" should have been converted to 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 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 A should return Announcement" {
} Expect.equal (PrayerRequestType.fromCode "A") Announcement
test "fromCode R should raise" { "\"A\" should have been converted to Announcement"
Expect.throws (fun () -> PrayerRequestType.fromCode "R" |> ignore) }
"An unknown code should have raised an exception" test "fromCode R should raise" {
} Expect.throws (fun () -> PrayerRequestType.fromCode "R" |> ignore)
"An unknown code should have raised an exception"
}
] ]
[<Tests>] [<Tests>]
let requestSortTests = let requestSortTests =
testList "RequestSort" [ testList "RequestSort" [
test "SortByDate code is correct" { test "SortByDate code is correct" {
Expect.equal SortByDate.code "D" "The code for SortByDate should have been \"D\"" Expect.equal SortByDate.code "D" "The code for SortByDate should have been \"D\""
} }
test "SortByRequestor code is correct" { test "SortByRequestor code is correct" {
Expect.equal SortByRequestor.code "R" "The code for SortByRequestor should have been \"R\"" Expect.equal SortByRequestor.code "R" "The code for SortByRequestor should have been \"R\""
} }
test "fromCode D should return SortByDate" { test "fromCode D should return SortByDate" {
Expect.equal (RequestSort.fromCode "D") SortByDate "\"D\" should have been converted to SortByDate" Expect.equal (RequestSort.fromCode "D") SortByDate "\"D\" should have been converted to SortByDate"
} }
test "fromCode R should return SortByRequestor" { test "fromCode R should return SortByRequestor" {
Expect.equal (RequestSort.fromCode "R") SortByRequestor "\"R\" should have been converted to 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" test "fromCode Q should raise" {
} Expect.throws (fun () -> RequestSort.fromCode "Q" |> ignore)
"An unknown code should have raised an exception"
}
] ]
[<Tests>] [<Tests>]
let smallGroupTests = let smallGroupTests =
testList "SmallGroup" [ testList "SmallGroup" [
let now = DateTime (2017, 5, 12, 12, 15, 0, DateTimeKind.Utc) let now = DateTime (2017, 5, 12, 12, 15, 0, DateTimeKind.Utc)
let withFakeClock f () = let withFakeClock f () =
FakeClock (Instant.FromDateTimeUtc now) |> f FakeClock (Instant.FromDateTimeUtc now) |> f
yield test "empty is as expected" { yield test "empty is as expected" {
let mt = SmallGroup.empty let mt = SmallGroup.empty
Expect.equal mt.smallGroupId Guid.Empty "The small group 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.churchId Guid.Empty "The church 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.name "" "The name should have been blank"
Expect.equal mt.church.churchId Guid.Empty "The church should have been an empty one" 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.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.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.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.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.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" Expect.isEmpty mt.users "There should be no users for an empty small group"
} }
yield! testFixture withFakeClock [ yield! testFixture withFakeClock [
"localTimeNow adjusts the time ahead of UTC", "localTimeNow adjusts the time ahead of UTC",
fun clock -> fun clock ->
let grp = { SmallGroup.empty with preferences = { ListPreferences.empty with timeZoneId = "Europe/Berlin" } } let grp =
Expect.isGreaterThan (grp.localTimeNow clock) now "UTC to Europe/Berlin should have added hours" { SmallGroup.empty with
"localTimeNow adjusts the time behind UTC", preferences = { ListPreferences.empty with timeZoneId = "Europe/Berlin" }
fun clock -> }
Expect.isLessThan (SmallGroup.empty.localTimeNow clock) now Expect.isGreaterThan (grp.localTimeNow clock) now "UTC to Europe/Berlin should have added hours"
"UTC to America/Denver should have subtracted hours" "localTimeNow adjusts the time behind UTC",
"localTimeNow returns UTC when the time zone is invalid", fun clock ->
fun clock -> Expect.isLessThan (SmallGroup.empty.localTimeNow clock) now
let grp = { SmallGroup.empty with preferences = { ListPreferences.empty with timeZoneId = "garbage" } } "UTC to America/Denver should have subtracted hours"
Expect.equal (grp.localTimeNow clock) now "UTC should have been returned for an invalid time zone" "localTimeNow returns UTC when the time zone is invalid",
] fun clock ->
yield test "localTimeNow fails when clock is not passed" { let grp = { SmallGroup.empty with preferences = { ListPreferences.empty with timeZoneId = "garbage" } }
Expect.throws (fun () -> (SmallGroup.empty.localTimeNow >> ignore) null) Expect.equal (grp.localTimeNow clock) now "UTC should have been returned for an invalid time zone"
"Should have raised an exception for null clock" ]
} yield test "localTimeNow fails when clock is not passed" {
yield test "localDateNow returns the date portion" { Expect.throws (fun () -> (SmallGroup.empty.localTimeNow >> ignore) null)
let now' = DateTime (2017, 5, 12, 1, 15, 0, DateTimeKind.Utc) "Should have raised an exception for null clock"
let clock = FakeClock (Instant.FromDateTimeUtc now') }
Expect.isLessThan (SmallGroup.empty.localDateNow clock) now.Date "The date should have been a day earlier" 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"
}
] ]
[<Tests>] [<Tests>]
let timeZoneTests = let timeZoneTests =
testList "TimeZone" [ testList "TimeZone" [
test "empty is as expected" { test "empty is as expected" {
let mt = TimeZone.empty let mt = TimeZone.empty
Expect.equal mt.timeZoneId "" "The time zone ID should have been blank" 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.description "" "The description should have been blank"
Expect.equal mt.sortOrder 0 "The sort order should have been zero" 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" Expect.isFalse mt.isActive "The is-active flag should not have been set"
} }
] ]
[<Tests>] [<Tests>]
let userTests = let userTests =
testList "User" [ testList "User" [
test "empty is as expected" { test "empty is as expected" {
let mt = User.empty let mt = User.empty
Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" 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.firstName "" "The first name should have been blank"
Expect.equal mt.lastName "" "The last 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.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.isFalse mt.isAdmin "The is admin flag should not have been set"
Expect.equal mt.passwordHash "" "The password hash should have been blank" Expect.equal mt.passwordHash "" "The password hash should have been blank"
Expect.isNone mt.salt "The password salt should not exist" 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.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" Expect.isEmpty mt.smallGroups "There should be no small groups for an empty user"
} }
test "fullName concatenates first and last names" { test "fullName concatenates first and last names" {
let user = { User.empty with firstName = "Unit"; lastName = "Test" } 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" Expect.equal user.fullName "Unit Test" "The full name should be the first and last, separated by a space"
} }
] ]
[<Tests>] [<Tests>]
let userSmallGroupTests = let userSmallGroupTests =
testList "UserSmallGroup" [ testList "UserSmallGroup" [
test "empty is as expected" { test "empty is as expected" {
let mt = UserSmallGroup.empty let mt = UserSmallGroup.empty
Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" 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.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.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" Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one"
} }
] ]

View File

@ -17,7 +17,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="9.0.4" /> <PackageReference Include="Expecto" Version="9.0.4" />
<PackageReference Include="Expecto.VisualStudio.TestAdapter" Version="10.0.2" /> <PackageReference Include="Expecto.VisualStudio.TestAdapter" Version="10.0.2" />
<PackageReference Include="NodaTime.Testing" Version="3.0.5" /> <PackageReference Include="NodaTime.Testing" Version="3.1.0" />
<PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,208 +1,214 @@
module PrayerTracker.UI.CommonFunctionsTests module PrayerTracker.UI.CommonFunctionsTests
open System.IO
open Expecto open Expecto
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Microsoft.AspNetCore.Mvc.Localization open Microsoft.AspNetCore.Mvc.Localization
open Microsoft.Extensions.Localization open Microsoft.Extensions.Localization
open PrayerTracker.Tests.TestLocalization open PrayerTracker.Tests.TestLocalization
open PrayerTracker.Views open PrayerTracker.Views
open System.IO
[<Tests>] [<Tests>]
let iconSizedTests = let iconSizedTests =
testList "iconSized" [ testList "iconSized" [
test "succeeds" { test "succeeds" {
let ico = iconSized 18 "tom-&-jerry" |> renderHtmlNode let ico = iconSized 18 "tom-&-jerry" |> renderHtmlNode
Expect.equal ico "<i class=\"material-icons md-18\">tom-&-jerry</i>" "icon HTML not correct" Expect.equal ico """<i class="material-icons md-18">tom-&-jerry</i>""" "icon HTML not correct"
} }
] ]
[<Tests>] [<Tests>]
let iconTests = let iconTests =
testList "icon" [ testList "icon" [
test "succeeds" { test "succeeds" {
let ico = icon "bob-&-tom" |> renderHtmlNode let ico = icon "bob-&-tom" |> renderHtmlNode
Expect.equal ico "<i class=\"material-icons\">bob-&-tom</i>" "icon HTML not correct" Expect.equal ico """<i class="material-icons">bob-&-tom</i>""" "icon HTML not correct"
} }
] ]
[<Tests>] [<Tests>]
let locStrTests = let locStrTests =
testList "locStr" [ testList "locStr" [
test "succeeds" { test "succeeds" {
let enc = locStr (LocalizedString ("test", "test&")) |> renderHtmlNode let enc = locStr (LocalizedString ("test", "test&")) |> renderHtmlNode
Expect.equal enc "test&amp;" "string not encoded correctly" Expect.equal enc "test&amp;" "string not encoded correctly"
} }
] ]
[<Tests>] [<Tests>]
let namedColorListTests = let namedColorListTests =
testList "namedColorList" [ testList "namedColorList" [
test "succeeds with default values" { test "succeeds with default values" {
let expected = let expected =
[ "<select name=\"the-name\">" [ """<select name="the-name">"""
"<option value=\"aqua\" style=\"background-color:aqua;color:black;\">aqua</option>" """<option value="aqua" style="background-color:aqua;color:black;">aqua</option>"""
"<option value=\"black\" style=\"background-color:black;color:white;\">black</option>" """<option value="black" style="background-color:black;color:white;">black</option>"""
"<option value=\"blue\" style=\"background-color:blue;color:white;\">blue</option>" """<option value="blue" style="background-color:blue;color:white;">blue</option>"""
"<option value=\"fuchsia\" style=\"background-color:fuchsia;color:black;\">fuchsia</option>" """<option value="fuchsia" style="background-color:fuchsia;color:black;">fuchsia</option>"""
"<option value=\"gray\" style=\"background-color:gray;color:white;\">gray</option>" """<option value="gray" style="background-color:gray;color:white;">gray</option>"""
"<option value=\"green\" style=\"background-color:green;color:white;\">green</option>" """<option value="green" style="background-color:green;color:white;">green</option>"""
"<option value=\"lime\" style=\"background-color:lime;color:black;\">lime</option>" """<option value="lime" style="background-color:lime;color:black;">lime</option>"""
"<option value=\"maroon\" style=\"background-color:maroon;color:white;\">maroon</option>" """<option value="maroon" style="background-color:maroon;color:white;">maroon</option>"""
"<option value=\"navy\" style=\"background-color:navy;color:white;\">navy</option>" """<option value="navy" style="background-color:navy;color:white;">navy</option>"""
"<option value=\"olive\" style=\"background-color:olive;color:white;\">olive</option>" """<option value="olive" style="background-color:olive;color:white;">olive</option>"""
"<option value=\"purple\" style=\"background-color:purple;color:white;\">purple</option>" """<option value="purple" style="background-color:purple;color:white;">purple</option>"""
"<option value=\"red\" style=\"background-color:red;color:black;\">red</option>" """<option value="red" style="background-color:red;color:black;">red</option>"""
"<option value=\"silver\" style=\"background-color:silver;color:black;\">silver</option>" """<option value="silver" style="background-color:silver;color:black;">silver</option>"""
"<option value=\"teal\" style=\"background-color:teal;color:white;\">teal</option>" """<option value="teal" style="background-color:teal;color:white;">teal</option>"""
"<option value=\"white\" style=\"background-color:white;color:black;\">white</option>" """<option value="white" style="background-color:white;color:black;">white</option>"""
"<option value=\"yellow\" style=\"background-color:yellow;color:black;\">yellow</option>" """<option value="yellow" style="background-color:yellow;color:black;">yellow</option>"""
"</select>" "</select>"
] ]
|> String.concat "" |> String.concat ""
let selectList = namedColorList "the-name" "" [] _s |> renderHtmlNode let selectList = namedColorList "the-name" "" [] _s |> renderHtmlNode
Expect.equal expected selectList "The default select list was not generated correctly" Expect.equal expected selectList "The default select list was not generated correctly"
} }
test "succeeds with a selected value" { test "succeeds with a selected value" {
let selectList = namedColorList "the-name" "white" [] _s |> renderHtmlNode let selectList = namedColorList "the-name" "white" [] _s |> renderHtmlNode
Expect.stringContains selectList " selected>white</option>" "Selected option not generated correctly" Expect.stringContains selectList " selected>white</option>" "Selected option not generated correctly"
} }
test "succeeds with extra attributes" { test "succeeds with extra attributes" {
let selectList = namedColorList "the-name" "" [ _id "myId" ] _s |> renderHtmlNode let selectList = namedColorList "the-name" "" [ _id "myId" ] _s |> renderHtmlNode
Expect.stringStarts selectList "<select name=\"the-name\" id=\"myId\">" "Attributes not included correctly" Expect.stringStarts selectList """<select name="the-name" id="myId">""" "Attributes not included correctly"
} }
] ]
[<Tests>] [<Tests>]
let radioTests = let radioTests =
testList "radio" [ testList "radio" [
test "succeeds when not selected" { test "succeeds when not selected" {
let rad = radio "a-name" "anId" "test" "unit" |> renderHtmlNode let rad = radio "a-name" "anId" "test" "unit" |> renderHtmlNode
Expect.equal rad "<input type=\"radio\" name=\"a-name\" id=\"anId\" value=\"test\">" Expect.equal rad """<input type="radio" name="a-name" id="anId" value="test">"""
"Unselected radio button not generated correctly" "Unselected radio button not generated correctly"
} }
test "succeeds when selected" { test "succeeds when selected" {
let rad = radio "a-name" "anId" "unit" "unit" |> renderHtmlNode let rad = radio "a-name" "anId" "unit" "unit" |> renderHtmlNode
Expect.equal rad "<input type=\"radio\" name=\"a-name\" id=\"anId\" value=\"unit\" checked>" Expect.equal rad """<input type="radio" name="a-name" id="anId" value="unit" checked>"""
"Selected radio button not generated correctly" "Selected radio button not generated correctly"
} }
] ]
[<Tests>] [<Tests>]
let rawLocTextTests = let rawLocTextTests =
testList "rawLocText" [ testList "rawLocText" [
test "succeeds" { test "succeeds" {
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw (LocalizedHtmlString ("test", "test&")) |> renderHtmlNode let raw = rawLocText sw (LocalizedHtmlString ("test", "test&")) |> renderHtmlNode
Expect.equal raw "test&" "string not written correctly" Expect.equal raw "test&" "string not written correctly"
} }
] ]
[<Tests>] [<Tests>]
let selectDefaultTests = let selectDefaultTests =
testList "selectDefault" [ testList "selectDefault" [
test "succeeds" { test "succeeds" {
Expect.equal (selectDefault "a&b") "— a&b —" "Default selection not generated correctly" Expect.equal (selectDefault "a&b") "— a&b —" "Default selection not generated correctly"
} }
] ]
[<Tests>] [<Tests>]
let selectListTests = let selectListTests =
testList "selectList" [ testList "selectList" [
test "succeeds with minimum options" { test "succeeds with minimum options" {
let theList = selectList "a-list" "" [] [] |> renderHtmlNode let theList = selectList "a-list" "" [] [] |> renderHtmlNode
Expect.equal theList "<select name=\"a-list\" id=\"a-list\"></select>" "Empty select list not generated correctly" Expect.equal theList """<select name="a-list" id="a-list"></select>"""
} "Empty select list not generated correctly"
test "succeeds with all options" { }
let theList = test "succeeds with all options" {
[ "tom", "Tom&" let theList =
"bob", "Bob" [ "tom", "Tom&"
"jan", "Jan" "bob", "Bob"
] "jan", "Jan"
|> selectList "the-list" "bob" [ _style "ugly" ] ]
|> renderHtmlNode |> selectList "the-list" "bob" [ _style "ugly" ]
let expected = |> renderHtmlNode
[ "<select name=\"the-list\" id=\"the-list\" style=\"ugly\">" let expected =
"<option value=\"tom\">Tom&amp;</option>" [ """<select name="the-list" id="the-list" style="ugly">"""
"<option value=\"bob\" selected>Bob</option>" """<option value="tom">Tom&amp;</option>"""
"<option value=\"jan\">Jan</option>" """<option value="bob" selected>Bob</option>"""
"</select>" """<option value="jan">Jan</option>"""
] """</select>"""
|> String.concat "" ]
Expect.equal theList expected "Filled select list not generated correctly" |> String.concat ""
} Expect.equal theList expected "Filled select list not generated correctly"
}
] ]
[<Tests>] [<Tests>]
let spaceTests = let spaceTests =
testList "space" [ testList "space" [
test "succeeds" { test "succeeds" {
Expect.equal (renderHtmlNode space) " " "space literal not correct" Expect.equal (renderHtmlNode space) " " "space literal not correct"
} }
] ]
[<Tests>] [<Tests>]
let submitTests = let submitTests =
testList "submit" [ testList "submit" [
test "succeeds" { test "succeeds" {
let btn = submit [ _class "slick" ] "file-ico" _s.["a&b"] |> renderHtmlNode let btn = submit [ _class "slick" ] "file-ico" _s["a&b"] |> renderHtmlNode
Expect.equal Expect.equal
btn btn
"<button type=\"submit\" class=\"slick\"><i class=\"material-icons\">file-ico</i> &nbsp;a&amp;b</button>" """<button type="submit" class="slick"><i class="material-icons">file-ico</i> &nbsp;a&amp;b</button>"""
"Submit button not generated correctly" "Submit button not generated correctly"
} }
] ]
[<Tests>] [<Tests>]
let tableSummaryTests = let tableSummaryTests =
testList "tableSummary" [ testList "tableSummary" [
test "succeeds for no entries" { test "succeeds for no entries" {
let sum = tableSummary 0 _s |> renderHtmlNode let sum = tableSummary 0 _s |> renderHtmlNode
Expect.equal sum "<div class=\"pt-center-text\"><small>No Entries to Display</small></div>" Expect.equal sum """<div class="pt-center-text"><small>No Entries to Display</small></div>"""
"Summary for no items is incorrect" "Summary for no items is incorrect"
} }
test "succeeds for one entry" { test "succeeds for one entry" {
let sum = tableSummary 1 _s |> renderHtmlNode let sum = tableSummary 1 _s |> renderHtmlNode
Expect.equal sum "<div class=\"pt-center-text\"><small>Displaying 1 Entry</small></div>" Expect.equal sum """<div class="pt-center-text"><small>Displaying 1 Entry</small></div>"""
"Summary for one item is incorrect" "Summary for one item is incorrect"
} }
test "succeeds for many entries" { test "succeeds for many entries" {
let sum = tableSummary 5 _s |> renderHtmlNode let sum = tableSummary 5 _s |> renderHtmlNode
Expect.equal sum "<div class=\"pt-center-text\"><small>Displaying 5 Entries</small></div>" Expect.equal sum """<div class="pt-center-text"><small>Displaying 5 Entries</small></div>"""
"Summary for many items is incorrect" "Summary for many items is incorrect"
} }
] ]
module TimeZones = module TimeZones =
open PrayerTracker.Views.CommonFunctions.TimeZones open PrayerTracker.Views.CommonFunctions.TimeZones
[<Tests>] [<Tests>]
let nameTests = let nameTests =
testList "TimeZones.name" [ testList "TimeZones.name" [
test "succeeds for US Eastern time" { test "succeeds for US Eastern time" {
Expect.equal (name "America/New_York" _s |> string) "Eastern" "US Eastern time zone not returned correctly" 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 Central time" {
} Expect.equal (name "America/Chicago" _s |> string) "Central"
test "succeeds for US Mountain time" { "US Central time zone not returned correctly"
Expect.equal (name "America/Denver" _s |> string) "Mountain" "US Mountain time zone not returned correctly" }
} test "succeeds for US Mountain time" {
test "succeeds for US Mountain (AZ) time" { Expect.equal (name "America/Denver" _s |> string) "Mountain"
Expect.equal (name "America/Phoenix" _s |> string) "Mountain (Arizona)" "US Mountain time zone not returned correctly"
"US Mountain (AZ) time zone not returned correctly" }
} test "succeeds for US Mountain (AZ) time" {
test "succeeds for US Pacific time" { Expect.equal (name "America/Phoenix" _s |> string) "Mountain (Arizona)"
Expect.equal (name "America/Los_Angeles" _s |> string) "Pacific" "US Pacific time zone not returned correctly" "US Mountain (AZ) time zone not returned correctly"
} }
test "succeeds for Central European time" { test "succeeds for US Pacific time" {
Expect.equal (name "Europe/Berlin" _s |> string) "Central European" Expect.equal (name "America/Los_Angeles" _s |> string) "Pacific"
"Central European time zone not returned correctly" "US Pacific time zone not returned correctly"
} }
test "fails for unexpected time zone" { test "succeeds for Central European time" {
Expect.equal (name "Wakanda" _s |> string) "Wakanda" "Unexpected time zone should have returned the original ID" 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"
}
]

View File

@ -136,7 +136,7 @@ module StringTests =
[<Tests>] [<Tests>]
let stripTagsTests = let stripTagsTests =
let testString = "<p class=\"testing\">Here is some text<br> <br />and some more</p>" let testString = """<p class="testing">Here is some text<br> <br />and some more</p>"""
testList "stripTags" [ testList "stripTags" [
test "does nothing if all tags are allowed" { test "does nothing if all tags are allowed" {
Expect.equal (stripTags [ "p"; "br" ] testString) testString Expect.equal (stripTags [ "p"; "br" ] testString) testString

File diff suppressed because it is too large Load Diff

View File

@ -6,45 +6,50 @@ open PrayerTracker.ViewModels
/// View for the church edit page /// View for the church edit page
let edit (m : EditChurch) ctx vi = 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 () let s = I18N.localizer.Force ()
[ form [ _action "/web/church/save"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/web/church/save"; _method "post"; _class "pt-center-columns" ] [
style [ _scoped ] [ style [ _scoped ] [
rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }" rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }"
] ]
csrfToken ctx 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "name" ] [ locStr s["Church Name"] ] 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" ] [ div [ _class "pt-field" ] [
label [ _for "City"] [ locStr s["City"] ] 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" ] [ div [ _class "pt-field" ] [
label [ _for "ST" ] [ locStr s["State"] ] label [ _for "state" ] [ locStr s["State or Province"] ]
input [ _type "text"; _name "st"; _id "st"; _required; _minlength "2"; _maxlength "2"; _value m.st ] 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-field-row" ] [
div [ _class "pt-checkbox-field" ] [ div [ _class "pt-checkbox-field" ] [
input [ _type "checkbox" input [ _type "checkbox"
_name "hasInterface" _name (nameof m.HasInterface)
_id "hasInterface" _id "hasInterface"
_value "True" _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"] ] 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-row pt-fadeable"; _id "divInterfaceAddress" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "interfaceAddress" ] [ locStr s["VPR Interface URL"] ] label [ _for "interfaceAddress" ] [ locStr s["VPR Interface URL"] ]
input input [ _type "url"
[ _type "url"; _name "interfaceAddress"; _id "interfaceAddress"; _name (nameof m.InterfaceAddress)
_value (match m.interfaceAddress with Some ia -> ia | None -> "") _id "interfaceAddress";
] _value (defaultArg m.InterfaceAddress "") ]
] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save Church"] ] div [ _class "pt-field-row" ] [ submit [] "save" s["Save Church"] ]

View File

@ -37,7 +37,7 @@ let error code vi =
_alt $"""%A{s["PrayerTracker"]} %A{s["from Bit Badger Solutions"]}""" _alt $"""%A{s["PrayerTracker"]} %A{s["from Bit Badger Solutions"]}"""
_title $"""%A{s["PrayerTracker"]} %A{s["from Bit Badger Solutions"]}""" _title $"""%A{s["PrayerTracker"]} %A{s["from Bit Badger Solutions"]}"""
_style "vertical-align:text-bottom;" ] _style "vertical-align:text-bottom;" ]
str vi.version str vi.Version
] ]
] ]
|> div [] |> div []

View File

@ -20,7 +20,7 @@ module Navigation =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let menuSpacer = rawText "&nbsp; " let menuSpacer = rawText "&nbsp; "
let leftLinks = [ let leftLinks = [
match m.user with match m.User with
| Some u -> | Some u ->
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn"; _role "button"; _aria "label" s["Requests"].Value; _title s["Requests"].Value ] a [ _class "dropbtn"; _role "button"; _aria "label" s["Requests"].Value; _title s["Requests"].Value ]
@ -56,7 +56,7 @@ module Navigation =
] ]
] ]
| None -> | None ->
match m.group with match m.Group with
| Some _ -> | Some _ ->
li [] [ li [] [
a [ _href "/web/prayer-requests/view" a [ _href "/web/prayer-requests/view"
@ -91,9 +91,9 @@ module Navigation =
] ]
] ]
let rightLinks = let rightLinks =
match m.group with match m.Group with
| Some _ -> [ | Some _ -> [
match m.user with match m.User with
| Some _ -> | Some _ ->
li [] [ li [] [
a [ _href "/web/user/password" a [ _href "/web/user/password"
@ -137,9 +137,9 @@ module Navigation =
rawText " &nbsp; &bull; &nbsp; " rawText " &nbsp; &bull; &nbsp; "
a [ _href "/web/language/es" ] [ locStr s["Cambie a Español"] ] a [ _href "/web/language/es" ] [ locStr s["Cambie a Español"] ]
] ]
match m.group with match m.Group with
| Some g ->[ | Some g ->[
match m.user with match m.User with
| Some u -> | Some u ->
span [ _class "u" ] [ locStr s["Currently Logged On"] ] span [ _class "u" ] [ locStr s["Currently Logged On"] ]
rawText "&nbsp; &nbsp;" rawText "&nbsp; &nbsp;"
@ -151,7 +151,7 @@ module Navigation =
rawText "&nbsp; " rawText "&nbsp; "
icon "group" icon "group"
space space
match m.user with match m.User with
| Some _ -> a [ _href "/web/small-group" ] [ strong [] [ str g.name ] ] | Some _ -> a [ _href "/web/small-group" ] [ strong [] [ str g.name ] ]
| None -> strong [] [ str g.name ] | None -> strong [] [ str g.name ]
rawText " &nbsp;" rawText " &nbsp;"
@ -190,9 +190,9 @@ let private htmlHead m pageTitle =
meta [ _charset "UTF-8" ] meta [ _charset "UTF-8" ]
title [] [ locStr pageTitle; titleSep; locStr s["PrayerTracker"] ] title [] [ locStr pageTitle; titleSep; locStr s["PrayerTracker"] ]
yield! commonHead yield! commonHead
for cssFile in m.style do for cssFile in m.Style do
link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ] 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" ] [] script [ _src $"/js/{jsFile}.js" ] []
] ]
@ -207,25 +207,25 @@ let private helpLink link =
/// Render the page title, and optionally a help link /// Render the page title, and optionally a help link
let private renderPageTitle m pageTitle = let private renderPageTitle m pageTitle =
h2 [ _id "pt-page-title" ] [ 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 locStr pageTitle
] ]
/// Render the messages that may need to be displayed to the user /// Render the messages that may need to be displayed to the user
let private messages m = let private messages m =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
m.messages m.Messages
|> List.map (fun msg -> |> List.map (fun msg ->
table [ _class $"pt-msg {msg.level.ToLower ()}" ] [ table [ _class $"pt-msg {MessageLevel.toCssClass msg.Level}" ] [
tr [] [ tr [] [
td [] [ td [] [
match msg.level with match msg.Level with
| "Info" -> () | Info -> ()
| lvl -> | lvl ->
strong [] [ locStr s[lvl] ] strong [] [ locStr s[MessageLevel.toString lvl] ]
rawText " &#xbb; " rawText " &#xbb; "
rawText msg.text.Value rawText msg.Text.Value
match msg.description with match msg.Description with
| Some desc -> | Some desc ->
br [] br []
div [ _class "description" ] [ rawText desc.Value ] div [ _class "description" ] [ rawText desc.Value ]
@ -238,7 +238,7 @@ let private messages m =
let private htmlFooter m = let private htmlFooter m =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let imgText = sprintf "%O %O" s["PrayerTracker"] s["from Bit Badger Solutions"] 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 [] [ footer [] [
div [ _id "pt-legal" ] [ div [ _id "pt-legal" ] [
a [ _href "/web/legal/privacy-policy" ] [ locStr s["Privacy Policy"] ] a [ _href "/web/legal/privacy-policy" ] [ locStr s["Privacy Policy"] ]
@ -255,7 +255,7 @@ let private htmlFooter m =
div [ _id "pt-footer" ] [ div [ _id "pt-footer" ] [
a [ _href "/web/"; _style "line-height:28px;" ] a [ _href "/web/"; _style "line-height:28px;" ]
[ img [ _src $"""/img/%O{s["footer_en"]}.png"""; _alt imgText; _title imgText ] ] [ img [ _src $"""/img/%O{s["footer_en"]}.png"""; _alt imgText; _title imgText ] ]
str m.version str m.Version
space space
i [ _title s["This page loaded in {0:N3} seconds", resultTime].Value; _class "material-icons md-18" ] i [ _title s["This page loaded in {0:N3} seconds", resultTime].Value; _class "material-icons md-18" ]
[ str "schedule" ] [ str "schedule" ]

View File

@ -13,35 +13,35 @@ open PrayerTracker.ViewModels
/// View for the prayer request edit page /// View for the prayer request edit page
let edit (m : EditRequest) today ctx vi = let edit (m : EditRequest) today ctx vi =
let s = I18N.localizer.Force () 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" ] [ [ form [ _action "/web/prayer-request/save"; _method "post"; _class "pt-center-columns" ] [
csrfToken ctx 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "requestType" ] [ locStr s["Request Type"] ] label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ]
ReferenceList.requestTypeList s ReferenceList.requestTypeList s
|> Seq.ofList |> Seq.ofList
|> Seq.map (fun (typ, desc) -> typ.code, desc.Value) |> 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" ] [ div [ _class "pt-field" ] [
label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ] label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ]
input [ _type "text" input [ _type "text"
_name "requestor" _name (nameof m.Requestor)
_id "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" ] [ div [ _class "pt-field" ] [
label [ _for "enteredDate" ] [ locStr s["Date"] ] 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 else
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
div [ _class "pt-checkbox-field" ] [ div [ _class "pt-checkbox-field" ] [
br [] 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"] ] label [ _for "skipDateUpdate" ] [ locStr s["Check to not update the date"] ]
br [] br []
small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ] 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [] [ locStr s["Expiration"] ] label [] [ locStr s["Expiration"] ]
ReferenceList.expirationList s ((m.isNew >> not) ()) ReferenceList.expirationList s (not m.IsNew)
|> List.map (fun exp -> |> List.map (fun exp ->
let radioId = $"expiration_{fst exp}" let radioId = $"expiration_{fst exp}"
span [ _class "text-nowrap" ] [ 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) ] label [ _for radioId ] [ locStr (snd exp) ]
rawText " &nbsp; &nbsp; " rawText " &nbsp; &nbsp; "
]) ])
@ -65,7 +65,7 @@ let edit (m : EditRequest) today ctx vi =
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field pt-editor" ] [ div [ _class "pt-field pt-editor" ] [
label [ _for "text" ] [ locStr s["Request"] ] 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"] ] 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 /// View for the request e-mail results page
let email m vi = let email m vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = $"""{s["Prayer Requests"].Value} {m.listGroup.name}""" let pageTitle = $"""{s["Prayer Requests"].Value} {m.SmallGroup.name}"""
let prefs = m.listGroup.preferences let prefs = m.SmallGroup.preferences
let addresses = String.Join (", ", m.recipients |> List.map (fun mbr -> $"{mbr.memberName} <{mbr.email}>")) 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;" ] [ [ 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"] locStr s["The request list was sent to the following people, via individual e-mails"]
rawText ":" rawText ":"
@ -88,11 +88,11 @@ let email m vi =
small [] [ str addresses ] small [] [ str addresses ]
] ]
span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ] 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 []
br [] br []
span [ _class "pt-email-heading" ] [ locStr s["Plain-Text Format"]; rawText ":" ] 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.Content.standard
|> Layout.standard vi pageTitle |> Layout.standard vi pageTitle
@ -101,7 +101,7 @@ let email m vi =
/// View for a small group's public prayer request list /// View for a small group's public prayer request list
let list (m : RequestList) vi = let list (m : RequestList) vi =
[ br [] [ br []
I18N.localizer.Force () |> (m.asHtml >> rawText) I18N.localizer.Force () |> (m.AsHtml >> rawText)
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi "View Request List" |> Layout.standard vi "View Request List"
@ -154,24 +154,24 @@ let lists (groups : SmallGroup list) vi =
/// View for the prayer request maintenance page /// View for the prayer request maintenance page
let maintain m (ctx : HttpContext) vi = let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let l = I18N.forView "Requests/Maintain" let l = I18N.forView "Requests/Maintain"
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
let now = m.smallGroup.localDateNow (ctx.GetService<IClock> ()) let now = m.SmallGroup.localDateNow (ctx.GetService<IClock> ())
let typs = ReferenceList.requestTypeList s |> Map.ofList let types = ReferenceList.requestTypeList s |> Map.ofList
let updReq (req : PrayerRequest) = 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" "pt-request-update"
else "" else ""
|> _class |> _class
let reqExp (req : PrayerRequest) = 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 /// Iterate the sequence once, before we render, so we can get the count of it at the top of the table
let requests = let requests =
m.requests m.Requests
|> Seq.map (fun req -> |> List.map (fun req ->
let reqId = flatGuid req.prayerRequestId let reqId = flatGuid req.prayerRequestId
let reqText = htmlToPlainText req.text let reqText = htmlToPlainText req.text
let delAction = $"/web/prayer-request/{reqId}/delete" let delAction = $"/web/prayer-request/{reqId}/delete"
@ -187,7 +187,7 @@ let maintain m (ctx : HttpContext) vi =
td [] [ td [] [
a [ _href $"/web/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] a [ _href $"/web/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ]
[ icon "edit" ] [ 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" a [ _href $"/web/prayer-request/{reqId}/restore"
_title l["Restore This Inactive Request"].Value ] _title l["Restore This Inactive Request"].Value ]
[ icon "visibility" ] [ icon "visibility" ]
@ -202,7 +202,7 @@ let maintain m (ctx : HttpContext) vi =
td [ updReq req ] [ td [ updReq req ] [
str (req.updatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture)) 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 [ reqExp req ] [ str (match req.requestor with Some r -> r | None -> " ") ]
td [] [ td [] [
match reqText.Length with match reqText.Length with
@ -218,7 +218,7 @@ let maintain m (ctx : HttpContext) vi =
rawText " &nbsp; &nbsp; &nbsp; " rawText " &nbsp; &nbsp; &nbsp; "
a [ _href "/web/prayer-requests/view"; _title s["View Prayer Request List"].Value ] a [ _href "/web/prayer-requests/view"; _title s["View Prayer Request List"].Value ]
[ icon "list"; rawText " &nbsp;"; locStr s["View Prayer Request List"] ] [ icon "list"; rawText " &nbsp;"; locStr s["View Prayer Request List"] ]
match m.searchTerm with match m.SearchTerm with
| Some _ -> | Some _ ->
rawText " &nbsp; &nbsp; &nbsp; " rawText " &nbsp; &nbsp; &nbsp; "
a [ _href "/web/prayer-requests"; _title l["Clear Search Criteria"].Value ] a [ _href "/web/prayer-requests"; _title l["Clear Search Criteria"].Value ]
@ -229,7 +229,7 @@ let maintain m (ctx : HttpContext) vi =
input [ _type "text" input [ _type "text"
_name "search" _name "search"
_placeholder l["Search requests..."].Value _placeholder l["Search requests..."].Value
_value (defaultArg m.searchTerm "") _value (defaultArg m.SearchTerm "")
] ]
space space
submit [] "search" s["Search"] submit [] "search" s["Search"]
@ -253,54 +253,52 @@ let maintain m (ctx : HttpContext) vi =
] ]
div [ _class "pt-center-text" ] [ div [ _class "pt-center-text" ] [
br [] br []
match m.onlyActive with match m.OnlyActive with
| Some true -> | Some true ->
raw l["Inactive requests are currently not shown"] raw l["Inactive requests are currently not shown"]
br [] br []
a [ _href "/web/prayer-requests/inactive" ] [ raw l["Show Inactive Requests"] ] a [ _href "/web/prayer-requests/inactive" ] [ raw l["Show Inactive Requests"] ]
| _ -> | _ ->
match Option.isSome m.onlyActive with if defaultArg m.OnlyActive false then
| true ->
raw l["Inactive requests are currently shown"] raw l["Inactive requests are currently shown"]
br [] br []
a [ _href "/web/prayer-requests" ] [ raw l["Do Not Show Inactive Requests"] ] a [ _href "/web/prayer-requests" ] [ raw l["Do Not Show Inactive Requests"] ]
br [] br []
br [] br []
| false -> () let search = [ match m.SearchTerm with Some s -> "search", s | None -> () ]
let srch = [ match m.searchTerm with Some s -> "search", s | None -> () ] let pg = defaultArg m.PageNbr 1
let pg = defaultArg m.pageNbr 1 let url =
let url = match m.OnlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/web/prayer-requests%s"
match m.onlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/web/prayer-requests%s"
match pg with match pg with
| 1 -> () | 1 -> ()
| _ -> | _ ->
// button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; locStr text ] // button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; 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) ] a [ _href (makeUrl url withPage) ]
[ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ] [ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ]
rawText " &nbsp; &nbsp; " rawText " &nbsp; &nbsp; "
match requests.Length = m.smallGroup.preferences.pageSize with match requests.Length = m.SmallGroup.preferences.pageSize with
| true -> | 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" ] [ raw l["Next Page"]; space; icon "keyboard_arrow_right" ]
| false -> () | false -> ()
] ]
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
] ]
|> Layout.Content.wide |> 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 /// View for the printable prayer request list
let print m version = let print m version =
let s = I18N.localizer.Force () 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}""" let imgAlt = $"""{s["PrayerTracker"].Value} {s["from Bit Badger Solutions"].Value}"""
article [] [ article [] [
rawText (m.asHtml s) rawText (m.AsHtml s)
br [] br []
hr [] 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""" img [ _src $"""/img/{s["footer_en"].Value}.png"""
_style "vertical-align:text-bottom;" _style "vertical-align:text-bottom;"
_alt imgAlt _alt imgAlt
@ -315,21 +313,21 @@ let print m version =
/// View for the prayer request list /// View for the prayer request list
let view m vi = let view m vi =
let s = I18N.localizer.Force () 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 " &nbsp; &nbsp; &nbsp; " let spacer = rawText " &nbsp; &nbsp; &nbsp; "
let dtString = m.date.ToString "yyyy-MM-dd" let dtString = m.Date.ToString "yyyy-MM-dd"
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _class "pt-icon-link" a [ _class "pt-icon-link"
_href $"/web/prayer-requests/print/{dtString}" _href $"/web/prayer-requests/print/{dtString}"
_title s["View Printable"].Value _title s["View Printable"].Value
] [ icon "print"; rawText " &nbsp;"; locStr s["View Printable"] ] ] [ icon "print"; rawText " &nbsp;"; locStr s["View Printable"] ]
if m.canEmail then if m.CanEmail then
spacer spacer
if m.date.DayOfWeek <> DayOfWeek.Sunday then if m.Date.DayOfWeek <> DayOfWeek.Sunday then
let rec findSunday (date : DateTime) = let rec findSunday (date : DateTime) =
if date.DayOfWeek = DayOfWeek.Sunday then date else findSunday (date.AddDays 1.) 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" a [ _class "pt-icon-link"
_href $"""/web/prayer-requests/view/{sunday.ToString "yyyy-MM-dd"}""" _href $"""/web/prayer-requests/view/{sunday.ToString "yyyy-MM-dd"}"""
_title s["List for Next Sunday"].Value ] [ _title s["List for Next Sunday"].Value ] [
@ -349,7 +347,7 @@ let view m vi =
] ]
] ]
br [] br []
rawText (m.asHtml s) rawText (m.AsHtml s)
] ]
|> Layout.Content.standard |> Layout.Content.standard
|> Layout.standard vi pageTitle |> Layout.standard vi pageTitle

View File

@ -18,14 +18,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="5.0.0" /> <PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageReference Include="MailKit" Version="2.15.0" /> <PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -822,4 +822,7 @@
<data name="as of" xml:space="preserve"> <data name="as of" xml:space="preserve">
<value>como de</value> <value>como de</value>
</data> </data>
<data name="State or Province" xml:space="preserve">
<value>Estado o Provincia</value>
</data>
</root> </root>

View File

@ -10,13 +10,14 @@ open PrayerTracker.ViewModels
/// View for the announcement page /// View for the announcement page
let announcement isAdmin ctx vi = let announcement isAdmin ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let m = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None }
let reqTypes = ReferenceList.requestTypeList s let reqTypes = ReferenceList.requestTypeList s
[ form [ _action "/web/small-group/announcement/send"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/web/small-group/announcement/send"; _method "post"; _class "pt-center-columns" ] [
csrfToken ctx csrfToken ctx
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field pt-editor" ] [ div [ _class "pt-field pt-editor" ] [
label [ _for "text" ] [ locStr s["Announcement Text"] ] label [ _for "text" ] [ locStr s["Announcement Text"] ]
textarea [ _name "text"; _id "text"; _autofocus ] [] textarea [ _name (nameof m.Text); _id "text"; _autofocus ] []
] ]
] ]
if isAdmin then if isAdmin then
@ -24,27 +25,27 @@ let announcement isAdmin ctx vi =
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [] [ locStr s["Send Announcement to"]; rawText ":" ] label [] [ locStr s["Send Announcement to"]; rawText ":" ]
div [ _class "pt-center-text" ] [ 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 " &nbsp; &nbsp; " ] label [ _for "sendY" ] [ locStr s["This Group"]; rawText " &nbsp; &nbsp; " ]
radio "sendToClass" "sendN" "N" "Y" radio (nameof m.SendToClass) "sendN" "N" "Y"
label [ _for "sendN" ] [ locStr s["All {0} Users", s["PrayerTracker"]] ] 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-field-row pt-fadeable pt-shown"; _id "divAddToList" ] [
div [ _class "pt-checkbox-field" ] [ 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"] ] label [ _for "addToRequestList" ] [ locStr s["Add to Request List"] ]
] ]
] ]
div [ _class "pt-field-row pt-fadeable"; _id "divCategory" ] [ div [ _class "pt-field-row pt-fadeable"; _id "divCategory" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "requestType" ] [ locStr s["Request Type"] ] label [ _for (nameof m.RequestType) ] [ locStr s["Request Type"] ]
reqTypes reqTypes
|> Seq.ofList |> Seq.ofList
|> Seq.map (fun (typ, desc) -> typ.code, desc.Value) |> 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"] ] 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 announcementSent (m : Announcement) vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
[ span [ _class "pt-email-heading" ] [ locStr s["HTML Format"]; rawText ":" ] [ 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 []
br [] br []
span [ _class "pt-email-heading" ] [ locStr s["Plain-Text Format"]; rawText ":" ] 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.Content.standard
|> Layout.standard vi "Announcement Sent" |> Layout.standard vi "Announcement Sent"
@ -72,24 +73,24 @@ let announcementSent (m : Announcement) vi =
/// View for the small group add/edit page /// View for the small group add/edit page
let edit (m : EditSmallGroup) (churches : Church list) ctx vi = let edit (m : EditSmallGroup) (churches : Church list) ctx vi =
let s = I18N.localizer.Force () 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" ] [ form [ _action "/web/small-group/save"; _method "post"; _class "pt-center-columns" ] [
csrfToken ctx 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "name" ] [ locStr s["Group Name"] ] 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "churchId" ] [ locStr s["Church"] ] label [ _for (nameof m.ChurchId) ] [ locStr s["Church"] ]
seq { seq {
"", selectDefault s["Select Church"].Value "", selectDefault s["Select Church"].Value
yield! churches |> List.map (fun c -> flatGuid c.churchId, c.name) 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"] ] 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 /// 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 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" ] [ 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 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "memberName" ] [ locStr s["Member Name"] ] label [ _for "name" ] [ locStr s["Member Name"] ]
input [ _type "text"; _name "memberName"; _id "memberName"; _required; _autofocus; _value m.memberName ] input [ _type "text"; _name (nameof m.Name); _id "name"; _required; _autofocus; _value m.Name ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "emailAddress" ] [ locStr s["E-mail Address"] ] label [ _for "email" ] [ locStr s["E-mail Address"] ]
input [ _type "email"; _name "emailAddress"; _id "emailAddress"; _required; _value m.emailAddress ] input [ _type "email"; _name (nameof m.Email); _id "email"; _required; _value m.Email ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "emailType" ] [ locStr s["E-mail Format"] ] label [ _for (nameof m.Format) ] [ locStr s["E-mail Format"] ]
typs types
|> Seq.map (fun typ -> fst typ, (snd typ).Value) |> 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"] ] 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 /// 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 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" ] [ [ form [ _action "/web/small-group/log-on/submit"; _method "post"; _class "pt-center-columns" ] [
csrfToken ctx csrfToken ctx
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "smallGroupId" ] [ locStr s["Group"] ] label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ]
seq { seq {
match grps.Length with match groups.Length with
| 0 -> "", s["There are no classes with passwords defined"].Value | 0 -> "", s["There are no classes with passwords defined"].Value
| _ -> | _ ->
"", selectDefault s["Select Group"].Value "", selectDefault s["Select Group"].Value
yield! yield!
grps groups
|> List.map (fun grp -> flatGuid grp.smallGroupId, $"{grp.church.name} | {grp.name}") |> 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" ] [ div [ _class "pt-field" ] [
label [ _for "password" ] [ locStr s["Password"] ] 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 ()) ] _placeholder (s["Case-Sensitive"].Value.ToLower ()) ]
] ]
] ]
div [ _class "pt-checkbox-field" ] [ 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"] ] label [ _for "rememberMe" ] [ locStr s["Remember Me"] ]
br [] br []
small [] [ em [] [ str (s["Requires Cookies"].Value.ToLower ()) ] ] 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 /// 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 s = I18N.localizer.Force ()
let grpTbl = let grpTbl =
match grps with match groups with
| [] -> space | [] -> space
| _ -> | _ ->
table [ _class "pt-table pt-action-table" ] [ table [ _class "pt-table pt-action-table" ] [
@ -187,7 +192,7 @@ let maintain (grps : SmallGroup list) ctx vi =
th [] [ locStr s["Time Zone"] ] th [] [ locStr s["Time Zone"] ]
] ]
] ]
grps groups
|> List.map (fun g -> |> List.map (fun g ->
let grpId = flatGuid g.smallGroupId let grpId = flatGuid g.smallGroupId
let delAction = $"/web/small-group/{grpId}/delete" let delAction = $"/web/small-group/{grpId}/delete"
@ -218,7 +223,7 @@ let maintain (grps : SmallGroup list) ctx vi =
br [] br []
br [] br []
] ]
tableSummary grps.Length s tableSummary groups.Length s
grpTbl grpTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] 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 /// View for the member maintenance page
let members (mbrs : Member list) (emailTyps : Map<string, LocalizedString>) ctx vi = let members (members : Member list) (emailTyps : Map<string, LocalizedString>) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let mbrTbl = let mbrTbl =
match mbrs with match members with
| [] -> space | [] -> space
| _ -> | _ ->
table [ _class "pt-table pt-action-table" ] [ table [ _class "pt-table pt-action-table" ] [
@ -242,7 +247,7 @@ let members (mbrs : Member list) (emailTyps : Map<string, LocalizedString>) ctx
th [] [ locStr s["Format"] ] th [] [ locStr s["Format"] ]
] ]
] ]
mbrs members
|> List.map (fun mbr -> |> List.map (fun mbr ->
let mbrId = flatGuid mbr.memberId let mbrId = flatGuid mbr.memberId
let delAction = $"/web/small-group/member/{mbrId}/delete" let delAction = $"/web/small-group/member/{mbrId}/delete"
@ -271,7 +276,7 @@ let members (mbrs : Member list) (emailTyps : Map<string, LocalizedString>) ctx
br [] br []
br [] br []
] ]
tableSummary mbrs.Length s tableSummary members.Length s
mbrTbl mbrTbl
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
] ]
@ -283,7 +288,7 @@ let members (mbrs : Member list) (emailTyps : Map<string, LocalizedString>) ctx
let overview m vi = let overview m vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let linkSpacer = rawText "&nbsp; " let linkSpacer = rawText "&nbsp; "
let typs = ReferenceList.requestTypeList s |> dict let types = ReferenceList.requestTypeList s |> dict
article [ _class "pt-overview" ] [ article [ _class "pt-overview" ] [
section [] [ section [] [
header [ _role "heading" ] [ header [ _role "heading" ] [
@ -306,16 +311,16 @@ let overview m vi =
] ]
div [] [ div [] [
p [ _class "pt-center-text" ] [ 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 [] hr []
for cat in m.activeReqsByCat do for cat in m.ActiveReqsByType do
str (cat.Value.ToString "N0") str (cat.Value.ToString "N0")
space space
locStr typs[cat.Key] locStr types[cat.Key]
br [] br []
br [] br []
str (m.allReqs.ToString "N0") str (m.AllReqs.ToString "N0")
space space
locStr s["Total Requests"] locStr s["Total Requests"]
hr [] hr []
@ -332,7 +337,7 @@ let overview m vi =
locStr s["Group Members"] locStr s["Group Members"]
] ]
div [ _class "pt-center-text" ] [ 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 [] hr []
a [ _href "/web/small-group/members" ] [ icon "email"; linkSpacer; locStr s["Maintain Group Members"] ] 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 () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
[ form [ _action "/web/small-group/preferences/save"; _method "post"; _class "pt-center-columns" ] [ [ 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 csrfToken ctx
fieldset [] [ fieldset [] [
legend [] [ strong [] [ icon "date_range"; rawText " &nbsp;"; locStr s["Dates"] ] ] legend [] [ strong [] [ icon "date_range"; rawText " &nbsp;"; locStr s["Dates"] ] ]
@ -358,24 +364,37 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "expireDays" ] [ locStr s["Requests Expire After"] ] label [ _for "expireDays" ] [ locStr s["Requests Expire After"] ]
span [] [ span [] [
input [ _type "number"; _name "expireDays"; _id "expireDays"; _min "1"; _max "30"; _required input [ _type "number"
_autofocus; _value (string m.expireDays) ] _name (nameof m.ExpireDays)
_id "expireDays"
_min "1"; _max "30"
_required
_autofocus
_value (string m.ExpireDays) ]
space; str (s["Days"].Value.ToLower ()) space; str (s["Days"].Value.ToLower ())
] ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "daysToKeepNew" ] [ locStr s["Requests “New” For"] ] label [ _for "daysToKeepNew" ] [ locStr s["Requests “New” For"] ]
span [] [ span [] [
input [ _type "number"; _name "daysToKeepNew"; _id "daysToKeepNew"; _min "1"; _max "30" input [ _type "number"
_required; _value (string m.daysToKeepNew) ] _name (nameof m.DaysToKeepNew)
_id "daysToKeepNew"
_min "1"; _max "30"
_required
_value (string m.DaysToKeepNew) ]
space; str (s["Days"].Value.ToLower ()) space; str (s["Days"].Value.ToLower ())
] ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "longTermUpdateWeeks" ] [ locStr s["Long-Term Requests Alerted for Update"] ] label [ _for "longTermUpdateWeeks" ] [ locStr s["Long-Term Requests Alerted for Update"] ]
span [] [ span [] [
input [ _type "number"; _name "longTermUpdateWeeks"; _id "longTermUpdateWeeks"; _min "1" input [ _type "number"
_max "30"; _required; _value (string m.longTermUpdateWeeks) ] _name (nameof m.LongTermUpdateWeeks)
_id "longTermUpdateWeeks"
_min "1"; _max "30"
_required
_value (string m.LongTermUpdateWeeks) ]
space; str (s["Weeks"].Value.ToLower ()) space; str (s["Weeks"].Value.ToLower ())
] ]
] ]
@ -383,10 +402,10 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
] ]
fieldset [] [ fieldset [] [
legend [] [ strong [] [ icon "sort"; rawText " &nbsp;"; locStr s["Request Sorting"] ] ] legend [] [ strong [] [ icon "sort"; rawText " &nbsp;"; 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"] ] label [ _for "requestSort_D" ] [ locStr s["Sort by Last Updated Date"] ]
rawText " &nbsp; " rawText " &nbsp; "
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"] ] label [ _for "requestSort_R" ] [ locStr s["Sort by Requestor Name"] ]
] ]
fieldset [] [ fieldset [] [
@ -394,17 +413,24 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "emailFromName" ] [ locStr s["From Name"] ] 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" ] [ div [ _class "pt-field" ] [
label [ _for "emailFromAddress" ] [ locStr s["From Address"] ] label [ _for "emailFromAddress" ] [ locStr s["From Address"] ]
input [ _type "email"; _name "emailFromAddress"; _id "emailFromAddress"; _required input [ _type "email"
_value m.emailFromAddress ] _name (nameof m.EmailFromAddress)
_id "emailFromAddress"
_required
_value m.EmailFromAddress ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "defaultEmailType" ] [ locStr s["E-mail Format"] ] label [ _for (nameof m.DefaultEmailType) ] [ locStr s["E-mail Format"] ]
seq { seq {
"", selectDefault s["Select"].Value "", selectDefault s["Select"].Value
yield! yield!
@ -412,7 +438,7 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
|> Seq.skip 1 |> Seq.skip 1
|> Seq.map (fun typ -> fst typ, (snd typ).Value) |> 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" ] [ div [ _class "pt-field" ] [
label [ _class "pt-center-text" ] [ locStr s["Color of Heading Lines"] ] label [ _class "pt-center-text" ] [ locStr s["Color of Heading Lines"] ]
span [] [ span [] [
radio "headingLineType" "headingLineType_Name" "Name" m.headingLineType radio (nameof m.LineColorType) "lineColorType_Name" "Name" m.LineColorType
label [ _for "headingLineType_Name" ] [ locStr s["Named Color"] ] label [ _for "lineColorType_Name" ] [ locStr s["Named Color"] ]
namedColorList "headingLineColor" m.headingLineColor namedColorList (nameof m.LineColor) m.LineColor
[ _id "headingLineColor_Select" [ _id "lineColor_Select"
match m.headingLineColor.StartsWith "#" with true -> _disabled | false -> () ] s if m.LineColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ()) rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio "headingLineType" "headingLineType_RGB" "RGB" m.headingLineType radio (nameof m.LineColorType) "lineColorType_RGB" "RGB" m.LineColorType
label [ _for "headingLineType_RGB" ] [ locStr s["Custom Color"] ] label [ _for "lineColorType_RGB" ] [ locStr s["Custom Color"] ]
input [ _type "color" input [ _type "color"
_name "headingLineColor" _name (nameof m.LineColor)
_id "headingLineColor_Color" _id "lineColor_Color"
_value m.headingLineColor _value m.LineColor // TODO: convert to hex or skip if named
match m.headingLineColor.StartsWith "#" with true -> () | false -> _disabled ] if not (m.LineColor.StartsWith "#") then _disabled ]
] ]
] ]
] ]
@ -442,19 +468,19 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _class "pt-center-text" ] [ locStr s["Color of Heading Text"] ] label [ _class "pt-center-text" ] [ locStr s["Color of Heading Text"] ]
span [] [ span [] [
radio "headingTextType" "headingTextType_Name" "Name" m.headingTextType radio (nameof m.HeadingColorType) "headingColorType_Name" "Name" m.HeadingColorType
label [ _for "headingTextType_Name" ] [ locStr s["Named Color"] ] label [ _for "headingColorType_Name" ] [ locStr s["Named Color"] ]
namedColorList "headingTextColor" m.headingTextColor namedColorList (nameof m.HeadingColor) m.HeadingColor
[ _id "headingTextColor_Select" [ _id "headingColor_Select"
match m.headingTextColor.StartsWith "#" with true -> _disabled | false -> () ] s if m.HeadingColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ()) rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio "headingTextType" "headingTextType_RGB" "RGB" m.headingTextType radio (nameof m.HeadingColorType) "headingColorType_RGB" "RGB" m.HeadingColorType
label [ _for "headingTextType_RGB" ] [ locStr s["Custom Color"] ] label [ _for "headingColorType_RGB" ] [ locStr s["Custom Color"] ]
input [ _type "color" input [ _type "color"
_name "headingTextColor" _name (nameof m.HeadingColor)
_id "headingTextColor_Color" _id "headingColor_Color"
_value m.headingTextColor _value m.HeadingColor // TODO: convert to hex or skip if named
match m.headingTextColor.StartsWith "#" with true -> () | false -> _disabled ] if not (m.HeadingColor.StartsWith "#") then _disabled ]
] ]
] ]
] ]
@ -462,19 +488,27 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
fieldset [] [ fieldset [] [
legend [] [ strong [] [ icon "font_download"; rawText " &nbsp;"; locStr s["Fonts"] ] ] legend [] [ strong [] [ icon "font_download"; rawText " &nbsp;"; locStr s["Fonts"] ] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "listFonts" ] [ locStr s["Fonts** for List"] ] label [ _for "fonts" ] [ locStr s["Fonts** for List"] ]
input [ _type "text"; _name "listFonts"; _id "listFonts"; _required; _value m.listFonts ] input [ _type "text"; _name (nameof m.Fonts); _id "fonts"; _required; _value m.Fonts ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "headingFontSize" ] [ locStr s["Heading Text Size"] ] label [ _for "headingFontSize" ] [ locStr s["Heading Text Size"] ]
input [ _type "number"; _name "headingFontSize"; _id "headingFontSize"; _min "8"; _max "24" input [ _type "number"
_required; _value (string m.headingFontSize) ] _name (nameof m.HeadingFontSize)
_id "headingFontSize"
_min "8"; _max "24"
_required
_value (string m.HeadingFontSize) ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "listFontSize" ] [ locStr s["List Text Size"] ] label [ _for "listFontSize" ] [ locStr s["List Text Size"] ]
input [ _type "number"; _name "listFontSize"; _id "listFontSize"; _min "8"; _max "24"; _required input [ _type "number"
_value (string m.listFontSize) ] _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 " &nbsp;"; locStr s["Other Settings"] ] ] legend [] [ strong [] [ icon "settings"; rawText " &nbsp;"; locStr s["Other Settings"] ] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "timeZone" ] [ locStr s["Time Zone"] ] label [ _for (nameof m.TimeZone) ] [ locStr s["Time Zone"] ]
seq { seq {
"", selectDefault s["Select"].Value "", selectDefault s["Select"].Value
yield! tzs |> List.map (fun tz -> tz.timeZoneId, (TimeZones.name tz.timeZoneId s).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" ] [ div [ _class "pt-field" ] [
label [] [ locStr s["Request List Visibility"] ] label [] [ locStr s["Request List Visibility"] ]
span [] [ 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"] ] label [ _for "viz_Public" ] [ locStr s["Public"] ]
rawText " &nbsp;" rawText " &nbsp;"
radio "listVisibility" "viz_Private" (string RequestVisibility.``private``) radio (nameof m.Visibility) "viz_Private" (string RequestVisibility.``private``)
(string m.listVisibility) (string m.Visibility)
label [ _for "viz_Private" ] [ locStr s["Private"] ] label [ _for "viz_Private" ] [ locStr s["Private"] ]
rawText " &nbsp;" rawText " &nbsp;"
radio "listVisibility" "viz_Password" (string RequestVisibility.passwordProtected) radio (nameof m.Visibility) "viz_Password" (string RequestVisibility.passwordProtected)
(string m.listVisibility) (string m.Visibility)
label [ _for "viz_Password" ] [ locStr s["Password Protected"] ] 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 [ _id "divClassPassword"; _class $"pt-field-row pt-fadeable{classSuffix}" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "groupPassword" ] [ locStr s["Group Password (Used to Read Online)"] ] label [ _for "groupPassword" ] [ locStr s["Group Password (Used to Read Online)"] ]
input [ _type "text"; _name "groupPassword"; _id "groupPassword"; input [ _type "text"
_value (match m.groupPassword with Some x -> x | None -> "") ] _name (nameof m.GroupPassword)
_id "groupPassword"
_value (defaultArg m.GroupPassword "") ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "pageSize" ] [ locStr s["Page Size"] ] label [ _for "pageSize" ] [ locStr s["Page Size"] ]
input [ _type "number"; _name "pageSize"; _id "pageSize"; _min "10"; _max "255"; _required input [ _type "number"
_value (string m.pageSize) ] _name (nameof m.PageSize)
_id "pageSize"
_min "10"; _max "255"
_required
_value (string m.PageSize) ]
] ]
div [ _class "pt-field" ] [ 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 ReferenceList.asOfDateList s
|> List.map (fun (code, desc) -> code, desc.Value) |> List.map (fun (code, desc) -> code, desc.Value)
|> selectList "asOfDate" m.asOfDate [ _required ] |> selectList (nameof m.AsOfDate) m.AsOfDate [ _required ]
] ]
] ]
] ]

View File

@ -7,11 +7,11 @@ open PrayerTracker.ViewModels
/// View for the group assignment page /// View for the group assignment page
let assignGroups m groups curGroups ctx vi = let assignGroups m groups curGroups ctx vi =
let s = I18N.localizer.Force () 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" ] [ form [ _action "/web/user/small-groups/save"; _method "post"; _class "pt-center-columns" ] [
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name "userId"; _value (flatGuid m.userId) ] input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ]
input [ _type "hidden"; _name "userName"; _value m.userName ] input [ _type "hidden"; _name (nameof m.UserName); _value m.UserName ]
table [ _class "pt-table" ] [ table [ _class "pt-table" ] [
thead [] [ thead [] [
tr [] [ tr [] [
@ -25,10 +25,10 @@ let assignGroups m groups curGroups ctx vi =
tr [] [ tr [] [
td [] [ td [] [
input [ _type "checkbox" input [ _type "checkbox"
_name "smallGroups" _name (nameof m.SmallGroups)
_id inputId _id inputId
_value grpId _value grpId
match curGroups |> List.contains grpId with true -> _checked | false -> () ] if List.contains grpId curGroups then _checked ]
] ]
td [] [ label [ _for inputId ] [ str grpName ] ] td [] [ label [ _for inputId ] [ str grpName ] ]
]) ])
@ -44,6 +44,7 @@ let assignGroups m groups curGroups ctx vi =
/// View for the password change page /// View for the password change page
let changePassword ctx vi = let changePassword ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let m = { OldPassword = ""; NewPassword = ""; NewPasswordConfirm = "" }
[ p [ _class "pt-center-text" ] [ [ 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."] 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "oldPassword" ] [ locStr s["Current Password"] ] 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "newPassword" ] [ locStr s["New Password Twice"] ] 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" ] [ div [ _class "pt-field" ] [
label [] [ rawText "&nbsp;" ] label [] [ rawText "&nbsp;" ]
input [ _type "password"; _name "newPasswordConfirm"; _id "newPasswordConfirm"; _required ] input [ _type "password"; _name (nameof m.NewPasswordConfirm); _id "newPasswordConfirm"; _required ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
@ -81,49 +82,57 @@ let changePassword ctx vi =
/// View for the edit user page /// View for the edit user page
let edit (m : EditUser) ctx vi = let edit (m : EditUser) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.isNew () then "Add a New User" else "Edit User" 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 pwPlaceholder = s[if m.IsNew then "" else "No change"].Value
[ form [ _action "/web/user/edit/save"; _method "post"; _class "pt-center-columns" [ 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"]}')""" ] [ _onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')""" ] [
style [ _scoped ] style [ _scoped ]
[ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #emailAddress { width: 20rem; } " ] [ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #email { width: 20rem; } " ]
csrfToken ctx 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "firstName" ] [ locStr s["First Name"] ] 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" ] [ div [ _class "pt-field" ] [
label [ _for "lastName" ] [ locStr s["Last Name"] ] 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" ] [ div [ _class "pt-field" ] [
label [ _for "emailAddress" ] [ locStr s["E-mail Address"] ] label [ _for "email" ] [ locStr s["E-mail Address"] ]
input [ _type "email"; _name "emailAddress"; _id "emailAddress"; _value m.emailAddress; _required ] input [ _type "email"; _name (nameof m.Email); _id "email"; _value m.Email; _required ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "password" ] [ locStr s["Password"] ] 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" ] [ div [ _class "pt-field" ] [
label [ _for "passwordConfirm" ] [ locStr s["Password Again"] ] 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" ] [ div [ _class "pt-checkbox-field" ] [
input [ _type "checkbox" input [ _type "checkbox"
_name "isAdmin" _name (nameof m.IsAdmin)
_id "isAdmin" _id "isAdmin"
_value "True" _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"] ] label [ _for "isAdmin" ] [ locStr s["This user is a PrayerTracker administrator"] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "save" s["Save User"] ] 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.Content.standard
|> Layout.standard vi pageTitle |> Layout.standard vi pageTitle
@ -133,33 +142,35 @@ let edit (m : EditUser) ctx vi =
let logOn (m : UserLogOn) groups ctx vi = let logOn (m : UserLogOn) groups ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
form [ _action "/web/user/log-on"; _method "post"; _class "pt-center-columns" ] [ 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 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-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "emailAddress"] [ locStr s["E-mail Address"] ] label [ _for "email"] [ locStr s["E-mail Address"] ]
input [ _type "email"; _name "emailAddress"; _id "emailAddress"; _value m.emailAddress; _required input [ _type "email"; _name (nameof m.Email); _id "email"; _value m.Email; _required; _autofocus ]
_autofocus ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "password" ] [ locStr s["Password"] ] 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 ())) ] _placeholder (sprintf "(%s)" (s["Case-Sensitive"].Value.ToLower ())) ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "smallGroupId" ] [ locStr s["Group"] ] label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ]
seq { seq {
"", selectDefault s["Select Group"].Value "", selectDefault s["Select Group"].Value
yield! groups yield! groups
} }
|> selectList "smallGroupId" "" [ _required ] |> selectList (nameof m.SmallGroupId) "" [ _required ]
] ]
] ]
div [ _class "pt-checkbox-field" ] [ 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"] ] label [ _for "rememberMe" ] [ locStr s["Remember Me"] ]
br [] br []
small [] [ em [] [ rawText "("; str (s["Requires Cookies"].Value.ToLower ()); rawText ")" ] ] small [] [ em [] [ rawText "("; str (s["Requires Cookies"].Value.ToLower ()); rawText ")" ] ]

View File

@ -32,12 +32,7 @@ module String =
let replaceFirst (needle : string) replacement (haystack : string) = let replaceFirst (needle : string) replacement (haystack : string) =
match haystack.IndexOf needle with match haystack.IndexOf needle with
| -1 -> haystack | -1 -> haystack
| idx -> | idx -> String.concat "" [ haystack[0..idx - 1]; replacement; haystack[idx + needle.Length..] ]
[ haystack[0..idx - 1]
replacement
haystack[idx + needle.Length..]
]
|> String.concat ""
open System.Text.RegularExpressions open System.Text.RegularExpressions
@ -49,14 +44,15 @@ let stripTags allowedTags input =
let mutable output = input let mutable output = input
for tag in stripHtmlExp.Matches input do for tag in stripHtmlExp.Matches input do
let htmlTag = tag.Value.ToLower () let htmlTag = tag.Value.ToLower ()
let isAllowed = let shouldReplace =
allowedTags allowedTags
|> List.fold (fun acc t -> |> List.fold (fun acc t ->
acc acc
|| htmlTag.IndexOf $"<{t}>" = 0 || htmlTag.IndexOf $"<{t}>" = 0
|| htmlTag.IndexOf $"<{t} " = 0 || htmlTag.IndexOf $"<{t} " = 0
|| htmlTag.IndexOf $"</{t}" = 0) false || htmlTag.IndexOf $"</{t}" = 0) false
if isAllowed then output <- String.replaceFirst tag.Value "" output |> not
if shouldReplace then output <- String.replaceFirst tag.Value "" output
output output
@ -88,9 +84,9 @@ let wordWrap charPerLine (input : string) =
remaining <- remaining[spaceIdx + 1..] remaining <- remaining[spaceIdx + 1..]
// Leftovers - yum! // Leftovers - yum!
match remaining.Length with 0 -> () | _ -> yield remaining match remaining.Length with 0 -> () | _ -> yield remaining
yield ""
} }
|> Seq.fold (fun (acc : StringBuilder) -> acc.AppendLine) (StringBuilder ()) |> String.concat "\n"
|> string
/// Modify the text returned by CKEditor into the format we need for request and announcement text /// Modify the text returned by CKEditor into the format we need for request and announcement text
let ckEditorToText (text : string) = let ckEditorToText (text : string) =

View File

@ -43,19 +43,40 @@ module ReferenceList =
Announcement, s["Announcements"] 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 /// This is used to create a message that is displayed to the user
[<NoComparison; NoEquality>] [<NoComparison; NoEquality>]
type UserMessage = type UserMessage =
{ /// The type { /// The type
level : string Level : MessageLevel
/// The actual message /// The actual message
text : HtmlString Text : HtmlString
/// The description (further information) /// The description (further information)
description : HtmlString option Description : HtmlString option
} }
/// Support for the UserMessage type /// Support for the UserMessage type
@ -63,23 +84,23 @@ module UserMessage =
/// Error message template /// Error message template
let error = let error =
{ level = "ERROR" { Level = Error
text = HtmlString.Empty Text = HtmlString.Empty
description = None Description = None
} }
/// Warning message template /// Warning message template
let warning = let warning =
{ level = "WARNING" { Level = Warning
text = HtmlString.Empty Text = HtmlString.Empty
description = None Description = None
} }
/// Info message template /// Info message template
let info = let info =
{ level = "Info" { Level = Info
text = HtmlString.Empty Text = HtmlString.Empty
description = None Description = None
} }
@ -89,28 +110,28 @@ open System
[<NoComparison; NoEquality>] [<NoComparison; NoEquality>]
type AppViewInfo = type AppViewInfo =
{ /// CSS files for the page { /// CSS files for the page
style : string list Style : string list
/// JavaScript files for the page /// JavaScript files for the page
script : string list Script : string list
/// The link for help on this page /// The link for help on this page
helpLink : string option HelpLink : string option
/// Messages to be displayed to the user /// Messages to be displayed to the user
messages : UserMessage list Messages : UserMessage list
/// The current version of PrayerTracker /// The current version of PrayerTracker
version : string Version : string
/// The ticks when the request started /// The ticks when the request started
requestStart : int64 RequestStart : int64
/// The currently logged on user, if there is one /// 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 /// The currently logged on small group, if there is one
group : SmallGroup option Group : SmallGroup option
} }
/// Support for the AppViewInfo type /// Support for the AppViewInfo type
@ -118,14 +139,14 @@ module AppViewInfo =
/// A fresh version that can be populated to process the current request /// A fresh version that can be populated to process the current request
let fresh = let fresh =
{ style = [] { Style = []
script = [] Script = []
helpLink = None HelpLink = None
messages = [] Messages = []
version = "" Version = ""
requestStart = DateTime.Now.Ticks RequestStart = DateTime.Now.Ticks
user = None User = None
group = None Group = None
} }
@ -133,34 +154,35 @@ module AppViewInfo =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type Announcement = type Announcement =
{ /// Whether the announcement should be sent to the class or to PrayerTracker users { /// Whether the announcement should be sent to the class or to PrayerTracker users
sendToClass : string SendToClass : string
/// The text of the announcement /// The text of the announcement
text : string Text : string
/// Whether this announcement should be added to the "Announcements" of the prayer list /// 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 /// The ID of the request type to which this announcement should be added
requestType : string option RequestType : string option
} }
with with
/// The text of the announcement, in plain text /// 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 /// Form for assigning small groups to a user
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type AssignGroups = type AssignGroups =
{ /// The Id of the user being assigned { /// The Id of the user being assigned
userId : UserId UserId : UserId
/// The full name of the user being assigned /// The full name of the user being assigned
userName : string UserName : string
/// The Ids of the small groups to which the user is authorized /// The Ids of the small groups to which the user is authorized
smallGroups : string SmallGroups : string
} }
/// Support for the AssignGroups type /// Support for the AssignGroups type
@ -168,9 +190,9 @@ module AssignGroups =
/// Create an instance of this form from an existing user /// Create an instance of this form from an existing user
let fromUser (u : User) = let fromUser (u : User) =
{ userId = u.userId { UserId = u.userId
userName = u.fullName UserName = u.fullName
smallGroups = "" SmallGroups = ""
} }
@ -178,11 +200,13 @@ module AssignGroups =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type ChangePassword = type ChangePassword =
{ /// The user's current password { /// The user's current password
oldPassword : string OldPassword : string
/// The user's new password /// The user's new password
newPassword : string NewPassword : string
/// The user's new password, confirmed /// The user's new password, confirmed
newPasswordConfirm : string NewPasswordConfirm : string
} }
@ -190,36 +214,37 @@ type ChangePassword =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type EditChurch = type EditChurch =
{ /// The Id of the church { /// The Id of the church
churchId : ChurchId ChurchId : ChurchId
/// The name of the church /// The name of the church
name : string Name : string
/// The city for the church /// The city for the church
city : string City : string
/// The state for the church /// The state or province for the church
st : string State : string
/// Whether the church has an active VPR interface /// Whether the church has an active Virtual Prayer Room interface
hasInterface : bool option HasInterface : bool option
/// The address for the interface /// The address for the interface
interfaceAddress : string option InterfaceAddress : string option
} }
with with
/// Is this a new church? /// 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 /// Populate a church from this form
member this.populateChurch (church : Church) = member this.PopulateChurch (church : Church) =
{ church with { church with
name = this.name name = this.Name
city = this.city city = this.City
st = this.st st = this.State
hasInterface = match this.hasInterface with Some x -> x | None -> false hasInterface = match this.HasInterface with Some x -> x | None -> false
interfaceAddress = match this.hasInterface with Some x when x -> this.interfaceAddress | _ -> None interfaceAddress = match this.HasInterface with Some x when x -> this.InterfaceAddress | _ -> None
} }
/// Support for the EditChurch type /// Support for the EditChurch type
@ -227,22 +252,22 @@ module EditChurch =
/// Create an instance from an existing church /// Create an instance from an existing church
let fromChurch (ch : Church) = let fromChurch (ch : Church) =
{ churchId = ch.churchId { ChurchId = ch.churchId
name = ch.name Name = ch.name
city = ch.city City = ch.city
st = ch.st State = ch.st
hasInterface = match ch.hasInterface with true -> Some true | false -> None HasInterface = match ch.hasInterface with true -> Some true | false -> None
interfaceAddress = ch.interfaceAddress InterfaceAddress = ch.interfaceAddress
} }
/// An instance to use for adding churches /// An instance to use for adding churches
let empty = let empty =
{ churchId = Guid.Empty { ChurchId = Guid.Empty
name = "" Name = ""
city = "" City = ""
st = "" State = ""
hasInterface = None HasInterface = None
interfaceAddress = None InterfaceAddress = None
} }
@ -250,39 +275,40 @@ module EditChurch =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type EditMember = type EditMember =
{ /// The Id for this small group member (not user-entered) { /// The Id for this small group member (not user-entered)
memberId : MemberId MemberId : MemberId
/// The name of the member /// The name of the member
memberName : string Name : string
/// The e-mail address /// The e-mail address
emailAddress : string Email : string
/// The e-mail format /// The e-mail format
emailType : string Format : string
} }
with with
/// Is this a new member? /// 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 /// Support for the EditMember type
module EditMember = module EditMember =
/// Create an instance from an existing member /// Create an instance from an existing member
let fromMember (m : Member) = let fromMember (m : Member) =
{ memberId = m.memberId { MemberId = m.memberId
memberName = m.memberName Name = m.memberName
emailAddress = m.email Email = m.email
emailType = match m.format with Some f -> f | None -> "" Format = match m.format with Some f -> f | None -> ""
} }
/// An empty instance /// An empty instance
let empty = let empty =
{ memberId = Guid.Empty { MemberId = Guid.Empty
memberName = "" Name = ""
emailAddress = "" Email = ""
emailType = "" Format = ""
} }
@ -290,90 +316,90 @@ module EditMember =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type EditPreferences = type EditPreferences =
{ /// The number of days after which requests are automatically expired { /// The number of days after which requests are automatically expired
expireDays : int ExpireDays : int
/// The number of days requests are considered "new" /// 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 /// 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 /// Whether to sort by updated date or requestor/subject
requestSort : string RequestSort : string
/// The name from which e-mail will be sent /// The name from which e-mail will be sent
emailFromName : string EmailFromName : string
/// The e-mail address from which e-mail will be sent /// The e-mail address from which e-mail will be sent
emailFromAddress : string EmailFromAddress : string
/// The default e-mail type for this group /// The default e-mail type for this group
defaultEmailType : string DefaultEmailType : string
/// Whether the heading line color uses named colors or R/G/B /// Whether the heading line color uses named colors or R/G/B
headingLineType : string LineColorType : string
/// The named color for the heading lines /// The named color for the heading lines
headingLineColor : string LineColor : string
/// Whether the heading text color uses named colors or R/G/B /// Whether the heading text color uses named colors or R/G/B
headingTextType : string HeadingColorType : string
/// The named color for the heading text /// The named color for the heading text
headingTextColor : string HeadingColor : string
/// The fonts to use for the list /// The fonts to use for the list
listFonts : string Fonts : string
/// The font size for the heading text /// The font size for the heading text
headingFontSize : int HeadingFontSize : int
/// The font size for the list text /// The font size for the list text
listFontSize : int ListFontSize : int
/// The time zone for the class /// The time zone for the class
timeZone : string TimeZone : string
/// The list visibility /// The list visibility
listVisibility : int Visibility : int
/// The small group password /// The small group password
groupPassword : string option GroupPassword : string option
/// The page size for search / inactive requests /// The page size for search / inactive requests
pageSize : int PageSize : int
/// How the as-of date should be displayed /// How the as-of date should be displayed
asOfDate : string AsOfDate : string
} }
with with
/// Set the properties of a small group based on the form's properties /// 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 = let isPublic, grpPw =
match this.listVisibility with match this.Visibility with
| RequestVisibility.``public`` -> true, "" | RequestVisibility.``public`` -> true, ""
| RequestVisibility.passwordProtected -> false, (defaultArg this.groupPassword "") | RequestVisibility.passwordProtected -> false, (defaultArg this.GroupPassword "")
| RequestVisibility.``private`` | RequestVisibility.``private``
| _ -> false, "" | _ -> false, ""
{ prefs with { prefs with
daysToExpire = this.expireDays daysToExpire = this.ExpireDays
daysToKeepNew = this.daysToKeepNew daysToKeepNew = this.DaysToKeepNew
longTermUpdateWeeks = this.longTermUpdateWeeks longTermUpdateWeeks = this.LongTermUpdateWeeks
requestSort = RequestSort.fromCode this.requestSort requestSort = RequestSort.fromCode this.RequestSort
emailFromName = this.emailFromName emailFromName = this.EmailFromName
emailFromAddress = this.emailFromAddress emailFromAddress = this.EmailFromAddress
defaultEmailType = EmailFormat.fromCode this.defaultEmailType defaultEmailType = EmailFormat.fromCode this.DefaultEmailType
lineColor = this.headingLineColor lineColor = this.LineColor
headingColor = this.headingTextColor headingColor = this.HeadingColor
listFonts = this.listFonts listFonts = this.Fonts
headingFontSize = this.headingFontSize headingFontSize = this.HeadingFontSize
textFontSize = this.listFontSize textFontSize = this.ListFontSize
timeZoneId = this.timeZone timeZoneId = this.TimeZone
isPublic = isPublic isPublic = isPublic
groupPassword = grpPw groupPassword = grpPw
pageSize = this.pageSize pageSize = this.PageSize
asOfDateDisplay = AsOfDateDisplay.fromCode this.asOfDate asOfDateDisplay = AsOfDateDisplay.fromCode this.AsOfDate
} }
/// Support for the EditPreferences type /// Support for the EditPreferences type
@ -381,25 +407,25 @@ module EditPreferences =
/// Populate an edit form from existing preferences /// Populate an edit form from existing preferences
let fromPreferences (prefs : ListPreferences) = let fromPreferences (prefs : ListPreferences) =
let setType (x : string) = match x.StartsWith "#" with true -> "RGB" | false -> "Name" let setType (x : string) = match x.StartsWith "#" with true -> "RGB" | false -> "Name"
{ expireDays = prefs.daysToExpire { ExpireDays = prefs.daysToExpire
daysToKeepNew = prefs.daysToKeepNew DaysToKeepNew = prefs.daysToKeepNew
longTermUpdateWeeks = prefs.longTermUpdateWeeks LongTermUpdateWeeks = prefs.longTermUpdateWeeks
requestSort = prefs.requestSort.code RequestSort = prefs.requestSort.code
emailFromName = prefs.emailFromName EmailFromName = prefs.emailFromName
emailFromAddress = prefs.emailFromAddress EmailFromAddress = prefs.emailFromAddress
defaultEmailType = prefs.defaultEmailType.code DefaultEmailType = prefs.defaultEmailType.code
headingLineType = setType prefs.lineColor LineColorType = setType prefs.lineColor
headingLineColor = prefs.lineColor LineColor = prefs.lineColor
headingTextType = setType prefs.headingColor HeadingColorType = setType prefs.headingColor
headingTextColor = prefs.headingColor HeadingColor = prefs.headingColor
listFonts = prefs.listFonts Fonts = prefs.listFonts
headingFontSize = prefs.headingFontSize HeadingFontSize = prefs.headingFontSize
listFontSize = prefs.textFontSize ListFontSize = prefs.textFontSize
timeZone = prefs.timeZoneId TimeZone = prefs.timeZoneId
groupPassword = Some prefs.groupPassword GroupPassword = Some prefs.groupPassword
pageSize = prefs.pageSize PageSize = prefs.pageSize
asOfDate = prefs.asOfDateDisplay.code AsOfDate = prefs.asOfDateDisplay.code
listVisibility = Visibility =
match true with match true with
| _ when prefs.isPublic -> RequestVisibility.``public`` | _ when prefs.isPublic -> RequestVisibility.``public``
| _ when prefs.groupPassword = "" -> RequestVisibility.``private`` | _ when prefs.groupPassword = "" -> RequestVisibility.``private``
@ -411,53 +437,54 @@ module EditPreferences =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type EditRequest = type EditRequest =
{ /// The Id of the request { /// The Id of the request
requestId : PrayerRequestId RequestId : PrayerRequestId
/// The type of the request /// The type of the request
requestType : string RequestType : string
/// The date of the request /// The date of the request
enteredDate : DateTime option EnteredDate : DateTime option
/// Whether to update the date or not /// Whether to update the date or not
skipDateUpdate : bool option SkipDateUpdate : bool option
/// The requestor or subject /// The requestor or subject
requestor : string option Requestor : string option
/// How this request is expired /// How this request is expired
expiration : string Expiration : string
/// The text of the request /// The text of the request
text : string Text : string
} }
with with
/// Is this a new request? /// 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 /// Support for the EditRequest type
module EditRequest = module EditRequest =
/// An empty instance to use for new requests /// An empty instance to use for new requests
let empty = let empty =
{ requestId = Guid.Empty { RequestId = Guid.Empty
requestType = CurrentRequest.code RequestType = CurrentRequest.code
enteredDate = None EnteredDate = None
skipDateUpdate = None SkipDateUpdate = None
requestor = None Requestor = None
expiration = Automatic.code Expiration = Automatic.code
text = "" Text = ""
} }
/// Create an instance from an existing request /// Create an instance from an existing request
let fromRequest req = let fromRequest req =
{ empty with { empty with
requestId = req.prayerRequestId RequestId = req.prayerRequestId
requestType = req.requestType.code RequestType = req.requestType.code
requestor = req.requestor Requestor = req.requestor
expiration = req.expiration.code Expiration = req.expiration.code
text = req.text Text = req.text
} }
@ -465,41 +492,42 @@ module EditRequest =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type EditSmallGroup = type EditSmallGroup =
{ /// The Id of the small group { /// The Id of the small group
smallGroupId : SmallGroupId SmallGroupId : SmallGroupId
/// The name of the small group /// The name of the small group
name : string Name : string
/// The Id of the church to which this small group belongs /// The Id of the church to which this small group belongs
churchId : ChurchId ChurchId : ChurchId
} }
with with
/// Is this a new small group? /// 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 /// Populate a small group from this form
member this.populateGroup (grp : SmallGroup) = member this.populateGroup (grp : SmallGroup) =
{ grp with { grp with
name = this.name name = this.Name
churchId = this.churchId churchId = this.ChurchId
} }
/// Support for the EditSmallGroup type /// Support for the EditSmallGroup type
module EditSmallGroup = module EditSmallGroup =
/// Create an instance from an existing small group /// Create an instance from an existing small group
let fromGroup (g : SmallGroup) = let fromGroup (g : SmallGroup) =
{ smallGroupId = g.smallGroupId { SmallGroupId = g.smallGroupId
name = g.name Name = g.name
churchId = g.churchId ChurchId = g.churchId
} }
/// An empty instance (used when adding a new group) /// An empty instance (used when adding a new group)
let empty = let empty =
{ smallGroupId = Guid.Empty { SmallGroupId = Guid.Empty
name = "" Name = ""
churchId = Guid.Empty ChurchId = Guid.Empty
} }
@ -507,65 +535,66 @@ module EditSmallGroup =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type EditUser = type EditUser =
{ /// The Id of the user { /// The Id of the user
userId : UserId UserId : UserId
/// The first name of the user /// The first name of the user
firstName : string FirstName : string
/// The last name of the user /// The last name of the user
lastName : string LastName : string
/// The e-mail address for the user /// The e-mail address for the user
emailAddress : string Email : string
/// The password for the user /// The password for the user
password : string Password : string
/// The password hash for the user a second time /// The password hash for the user a second time
passwordConfirm : string PasswordConfirm : string
/// Is this user a PrayerTracker administrator? /// Is this user a PrayerTracker administrator?
isAdmin : bool option IsAdmin : bool option
} }
with with
/// Is this a new user? /// 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 /// Populate a user from the form
member this.populateUser (user : User) hasher = member this.PopulateUser (user : User) hasher =
{ user with { user with
firstName = this.firstName firstName = this.FirstName
lastName = this.lastName lastName = this.LastName
emailAddress = this.emailAddress emailAddress = this.Email
isAdmin = match this.isAdmin with Some x -> x | None -> false isAdmin = defaultArg this.IsAdmin false
} }
|> function |> function
| u when isNull this.password || this.password = "" -> u | u when isNull this.Password || this.Password = "" -> u
| u -> { u with passwordHash = hasher this.password } | u -> { u with passwordHash = hasher this.Password }
/// Support for the EditUser type /// Support for the EditUser type
module EditUser = module EditUser =
/// An empty instance /// An empty instance
let empty = let empty =
{ userId = Guid.Empty { UserId = Guid.Empty
firstName = "" FirstName = ""
lastName = "" LastName = ""
emailAddress = "" Email = ""
password = "" Password = ""
passwordConfirm = "" PasswordConfirm = ""
isAdmin = None IsAdmin = None
} }
/// Create an instance from an existing user /// Create an instance from an existing user
let fromUser (user : User) = let fromUser (user : User) =
{ empty with { empty with
userId = user.userId UserId = user.userId
firstName = user.firstName FirstName = user.firstName
lastName = user.lastName LastName = user.lastName
emailAddress = user.emailAddress Email = user.emailAddress
isAdmin = match user.isAdmin with true -> Some true | false -> None IsAdmin = if user.isAdmin then Some true else None
} }
@ -573,13 +602,13 @@ module EditUser =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type GroupLogOn = type GroupLogOn =
{ /// The ID of the small group to which the user is logging on { /// The ID of the small group to which the user is logging on
smallGroupId : SmallGroupId SmallGroupId : SmallGroupId
/// The password entered /// The password entered
password : string Password : string
/// Whether to remember the login /// Whether to remember the login
rememberMe : bool option RememberMe : bool option
} }
/// Support for the GroupLogOn type /// Support for the GroupLogOn type
@ -587,9 +616,9 @@ module GroupLogOn =
/// An empty instance /// An empty instance
let empty = let empty =
{ smallGroupId = Guid.Empty { SmallGroupId = Guid.Empty
password = "" Password = ""
rememberMe = None RememberMe = None
} }
@ -597,19 +626,19 @@ module GroupLogOn =
[<NoComparison; NoEquality>] [<NoComparison; NoEquality>]
type MaintainRequests = type MaintainRequests =
{ /// The requests to be displayed { /// The requests to be displayed
requests : PrayerRequest seq Requests : PrayerRequest list
/// The small group to which the requests belong /// The small group to which the requests belong
smallGroup : SmallGroup SmallGroup : SmallGroup
/// Whether only active requests are included /// Whether only active requests are included
onlyActive : bool option OnlyActive : bool option
/// The search term for the requests /// The search term for the requests
searchTerm : string option SearchTerm : string option
/// The page number of the results /// The page number of the results
pageNbr : int option PageNbr : int option
} }
/// Support for the MaintainRequests type /// Support for the MaintainRequests type
@ -617,11 +646,11 @@ module MaintainRequests =
/// An empty instance /// An empty instance
let empty = let empty =
{ requests = Seq.empty { Requests = []
smallGroup = SmallGroup.empty SmallGroup = SmallGroup.empty
onlyActive = None OnlyActive = None
searchTerm = None SearchTerm = None
pageNbr = None PageNbr = None
} }
@ -629,16 +658,16 @@ module MaintainRequests =
[<NoComparison; NoEquality>] [<NoComparison; NoEquality>]
type Overview = type Overview =
{ /// The total number of active requests { /// The total number of active requests
totalActiveReqs : int TotalActiveReqs : int
/// The numbers of active requests by category /// The numbers of active requests by request type
activeReqsByCat : Map<PrayerRequestType, int> ActiveReqsByType : Map<PrayerRequestType, int>
/// A count of all requests /// A count of all requests
allReqs : int AllReqs : int
/// A count of all members /// A count of all members
totalMbrs : int TotalMembers : int
} }
@ -646,19 +675,19 @@ type Overview =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type UserLogOn = type UserLogOn =
{ /// The e-mail address of the user { /// The e-mail address of the user
emailAddress : string Email : string
/// The password entered /// The password entered
password : string Password : string
/// The ID of the small group to which the user is logging on /// The ID of the small group to which the user is logging on
smallGroupId : SmallGroupId SmallGroupId : SmallGroupId
/// Whether to remember the login /// Whether to remember the login
rememberMe : bool option RememberMe : bool option
/// The URL to which the user should be redirected once login is successful /// The URL to which the user should be redirected once login is successful
redirectUrl : string option RedirectUrl : string option
} }
/// Support for the UserLogOn type /// Support for the UserLogOn type
@ -666,11 +695,11 @@ module UserLogOn =
/// An empty instance /// An empty instance
let empty = let empty =
{ emailAddress = "" { Email = ""
password = "" Password = ""
smallGroupId = Guid.Empty SmallGroupId = Guid.Empty
rememberMe = None RememberMe = None
redirectUrl = None RedirectUrl = None
} }
@ -679,64 +708,64 @@ open Giraffe.ViewEngine
/// This represents a list of requests /// This represents a list of requests
type RequestList = type RequestList =
{ /// The prayer request list { /// The prayer request list
requests : PrayerRequest list Requests : PrayerRequest list
/// The date for which this list is being generated /// The date for which this list is being generated
date : DateTime Date : DateTime
/// The small group to which this list belongs /// The small group to which this list belongs
listGroup : SmallGroup SmallGroup : SmallGroup
/// Whether to show the class header /// Whether to show the class header
showHeader : bool ShowHeader : bool
/// The list of recipients (populated if requests are e-mailed) /// The list of recipients (populated if requests are e-mailed)
recipients : Member list Recipients : Member list
/// Whether the user can e-mail this list /// Whether the user can e-mail this list
canEmail : bool CanEmail : bool
} }
with with
/// Group requests by their type, along with the type and its localized string /// 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 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)) |> 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? /// Is this request new?
member this.isNew (req : PrayerRequest) = member this.IsNew (req : PrayerRequest) =
(this.date - req.updatedDate).Days <= this.listGroup.preferences.daysToKeepNew (this.Date - req.updatedDate).Days <= this.SmallGroup.preferences.daysToKeepNew
/// Generate this list as HTML /// Generate this list as HTML
member this.asHtml (s : IStringLocalizer) = member this.AsHtml (s : IStringLocalizer) =
let prefs = this.listGroup.preferences let prefs = this.SmallGroup.preferences
let asOfSize = Math.Round (float prefs.textFontSize * 0.8, 2) 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}" ] [ div [ _style $"text-align:center;font-family:{prefs.listFonts}" ] [
span [ _style $"font-size:%i{prefs.headingFontSize}pt;" ] [ span [ _style $"font-size:%i{prefs.headingFontSize}pt;" ] [
strong [] [ str s["Prayer Requests"].Value ] strong [] [ str s["Prayer Requests"].Value ]
] ]
br [] br []
span [ _style $"font-size:%i{prefs.textFontSize}pt;" ] [ span [ _style $"font-size:%i{prefs.textFontSize}pt;" ] [
strong [] [ str this.listGroup.name ] strong [] [ str this.SmallGroup.name ]
br [] br []
str (this.date.ToString s["MMMM d, yyyy"].Value) str (this.Date.ToString s["MMMM d, yyyy"].Value)
] ]
] ]
br [] 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;" ] [ div [ _style "padding-left:10px;padding-bottom:.5em;" ] [
table [ _style $"font-family:{prefs.listFonts};page-break-inside:avoid;" ] [ table [ _style $"font-family:{prefs.listFonts};page-break-inside:avoid;" ] [
tr [] [ tr [] [
@ -748,7 +777,7 @@ with
] ]
reqs reqs
|> List.map (fun req -> |> 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;" ] [ li [ _style $"list-style-type:{bullet};font-family:{prefs.listFonts};font-size:%i{prefs.textFontSize}pt;padding-bottom:.25em;" ] [
match req.requestor with match req.requestor with
| Some r when r <> "" -> | Some r when r <> "" ->
@ -776,25 +805,25 @@ with
|> RenderView.AsString.htmlNodes |> RenderView.AsString.htmlNodes
/// Generate this list as plain text /// Generate this list as plain text
member this.asText (s : IStringLocalizer) = member this.AsText (s : IStringLocalizer) =
seq { seq {
this.listGroup.name this.SmallGroup.name
s["Prayer Requests"].Value 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) "-" let dashes = String.replicate (name.Value.Length + 4) "-"
dashes dashes
$" {name.Value.ToUpper ()}" $" {name.Value.ToUpper ()}"
dashes dashes
for req in reqs do 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 -> "" let requestor = match req.requestor with Some r -> $"{r} - " | None -> ""
match this.listGroup.preferences.asOfDateDisplay with match this.SmallGroup.preferences.asOfDateDisplay with
| NoDisplay -> "" | NoDisplay -> ""
| _ -> | _ ->
let dt = let dt =
match this.listGroup.preferences.asOfDateDisplay with match this.SmallGroup.preferences.asOfDateDisplay with
| ShortDate -> req.updatedDate.ToShortDateString () | ShortDate -> req.updatedDate.ToShortDateString ()
| LongDate -> req.updatedDate.ToLongDateString () | LongDate -> req.updatedDate.ToLongDateString ()
| _ -> "" | _ -> ""

View File

@ -70,17 +70,17 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
match! ctx.TryBindFormAsync<EditChurch> () with match! ctx.TryBindFormAsync<EditChurch> () with
| Ok m -> | Ok m ->
let! church = let! church =
if m.isNew () then Task.FromResult (Some { Church.empty with churchId = Guid.NewGuid () }) if m.IsNew then Task.FromResult (Some { Church.empty with churchId = Guid.NewGuid () })
else ctx.db.TryChurchById m.churchId else ctx.db.TryChurchById m.ChurchId
match church with match church with
| Some ch -> | Some ch ->
m.populateChurch ch m.PopulateChurch ch
|> (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! _ = ctx.db.SaveChangesAsync ()
let s = Views.I18N.localizer.Force () 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} church “{1}”", act, m.name] addInfo ctx s["Successfully {0} church “{1}”", act, m.Name]
return! redirectTo false "/web/churches" next ctx return! redirectTo false "/web/churches" next ctx
| None -> return! fourOhFour next ctx | None -> return! fourOhFour next ctx
| Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }

View File

@ -85,11 +85,11 @@ let viewInfo (ctx : HttpContext) startTicks =
HttpOnly = true)) HttpOnly = true))
| None -> () | None -> ()
{ AppViewInfo.fresh with { AppViewInfo.fresh with
version = appVersion Version = appVersion
messages = msg Messages = msg
requestStart = startTicks RequestStart = startTicks
user = ctx.Session.user User = ctx.Session.user
group = ctx.Session.smallGroup Group = ctx.Session.smallGroup
} }
/// The view is the last parameter, so it can be composed /// 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 /// Add an error message to the session
let addError ctx msg = 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 /// Add an informational message to the session
let addInfo ctx msg = 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 /// Add an informational HTML message to the session
let addHtmlInfo ctx msg = 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 /// Add a warning message to the session
let addWarning ctx msg = 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 /// A level of required access

View File

@ -21,7 +21,7 @@ let getConnection () = task {
/// Create a mail message object, filled with everything but the body content /// Create a mail message object, filled with everything but the body content
let createMessage (grp : SmallGroup) subj = let createMessage (grp : SmallGroup) subj =
let msg = MimeMessage () let msg = new MimeMessage ()
msg.From.Add (MailboxAddress (grp.preferences.emailFromName, fromAddress)) msg.From.Add (MailboxAddress (grp.preferences.emailFromName, fromAddress))
msg.Subject <- subj msg.Subject <- subj
msg.ReplyTo.Add (MailboxAddress (grp.preferences.emailFromName, grp.preferences.emailFromAddress)) msg.ReplyTo.Add (MailboxAddress (grp.preferences.emailFromName, grp.preferences.emailFromAddress))
@ -40,7 +40,7 @@ let createHtmlMessage grp subj body (s : IStringLocalizer) =
] ]
|> String.concat "" |> String.concat ""
let msg = createMessage grp subj let msg = createMessage grp subj
msg.Body <- TextPart (TextFormat.Html, Text = bodyText) msg.Body <- new TextPart (TextFormat.Html, Text = bodyText)
msg msg
/// Create a plain-text-format e-mail message /// Create a plain-text-format e-mail message
@ -54,13 +54,13 @@ let createTextMessage grp subj body (s : IStringLocalizer) =
] ]
|> String.concat "" |> String.concat ""
let msg = createMessage grp subj let msg = createMessage grp subj
msg.Body <- TextPart (TextFormat.Plain, Text = bodyText) msg.Body <- new TextPart (TextFormat.Plain, Text = bodyText)
msg msg
/// Send e-mails to a class /// Send e-mails to a class
let sendEmails (client : SmtpClient) (recipients : Member list) grp subj html text s = task { let sendEmails (client : SmtpClient) (recipients : Member list) grp subj html text s = task {
let htmlMsg = createHtmlMessage grp subj html s use htmlMsg = createHtmlMessage grp subj html s
let plainTextMsg = createTextMessage grp subj text s use plainTextMsg = createTextMessage grp subj text s
for mbr in recipients do for mbr in recipients do
let emailType = let emailType =
@ -71,10 +71,10 @@ let sendEmails (client : SmtpClient) (recipients : Member list) grp subj html te
match emailType with match emailType with
| HtmlFormat -> | HtmlFormat ->
htmlMsg.To.Add emailTo htmlMsg.To.Add emailTo
do! client.SendAsync htmlMsg let! _ = client.SendAsync htmlMsg
htmlMsg.To.Clear () htmlMsg.To.Clear ()
| PlainTextFormat -> | PlainTextFormat ->
plainTextMsg.To.Add emailTo plainTextMsg.To.Add emailTo
do! client.SendAsync plainTextMsg let! _ = client.SendAsync plainTextMsg
plainTextMsg.To.Clear () plainTextMsg.To.Clear ()
} }

View File

@ -16,8 +16,8 @@ let private findRequest (ctx : HttpContext) reqId = task {
| Some _ -> | Some _ ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
addError ctx s["The prayer request you tried to access is not assigned to your group"] addError ctx s["The prayer request you tried to access is not assigned to your group"]
return Error (redirectTo false "/web/unauthorized") return Result.Error (redirectTo false "/web/unauthorized")
| None -> return Error fourOhFour | None -> return Result.Error fourOhFour
} }
/// Generate a list of requests for the given date /// 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 listDate = match date with Some d -> d | None -> grp.localDateNow clock
let! reqs = ctx.db.AllRequestsForSmallGroup grp clock (Some listDate) true 0 let! reqs = ctx.db.AllRequestsForSmallGroup grp clock (Some listDate) true 0
return return
{ requests = reqs |> List.ofSeq { Requests = reqs
date = listDate Date = listDate
listGroup = grp SmallGroup = grp
showHeader = true ShowHeader = true
canEmail = ctx.Session.user |> Option.isSome CanEmail = Option.isSome ctx.Session.user
recipients = [] Recipients = []
} }
} }
@ -50,7 +50,7 @@ let edit (reqId : PrayerRequestId) : HttpHandler = requireAccess [ User ] >=> fu
let now = grp.localDateNow (ctx.GetService<IClock> ()) let now = grp.localDateNow (ctx.GetService<IClock> ())
if reqId = Guid.Empty then if reqId = Guid.Empty then
return! 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 |> Views.PrayerRequest.edit EditRequest.empty (now.ToString "yyyy-MM-dd") ctx
|> renderHtml next ctx |> renderHtml next ctx
else else
@ -59,18 +59,18 @@ let edit (reqId : PrayerRequestId) : HttpHandler = requireAccess [ User ] >=> fu
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
if req.isExpired now grp.preferences.daysToExpire then if req.isExpired now grp.preferences.daysToExpire then
{ UserMessage.warning with { UserMessage.warning with
text = htmlLocString s["This request is expired."] Text = htmlLocString s["This request is expired."]
description = 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["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"]] s["Expire Immediately"], s["Check to not update the date"]]
|> (htmlLocString >> Some) |> (htmlLocString >> Some)
} }
|> addUserMessage ctx |> addUserMessage ctx
return! 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 |> Views.PrayerRequest.edit (EditRequest.fromRequest req) "" ctx
|> renderHtml next 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 let! recipients = ctx.db.AllMembersForSmallGroup grp.smallGroupId
use! client = Email.getConnection () use! client = Email.getConnection ()
do! Email.sendEmails client recipients do! Email.sendEmails client recipients
grp s["Prayer Requests for {0} - {1:MMMM d, yyyy}", grp.name, list.date].Value grp s["Prayer Requests for {0} - {1:MMMM d, yyyy}", grp.name, list.Date].Value
(list.asHtml s) (list.asText s) s (list.AsHtml s) (list.AsText s) s
return! return!
viewInfo ctx startTicks viewInfo ctx startTicks
|> Views.PrayerRequest.email { list with recipients = recipients } |> Views.PrayerRequest.email { list with Recipients = recipients }
|> renderHtml next ctx |> renderHtml next ctx
} }
@ -102,7 +102,7 @@ let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun
let! _ = ctx.db.SaveChangesAsync () let! _ = ctx.db.SaveChangesAsync ()
addInfo ctx s["The prayer request was deleted successfully"] addInfo ctx s["The prayer request was deleted successfully"]
return! redirectTo false "/web/prayer-requests" next ctx 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 () let! _ = ctx.db.SaveChangesAsync ()
addInfo ctx s["Successfully {0} prayer request", s["Expired"].Value.ToLower ()] addInfo ctx s["Successfully {0} prayer request", s["Expired"].Value.ToLower ()]
return! redirectTo false "/web/prayer-requests" next ctx 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! return!
viewInfo ctx startTicks viewInfo ctx startTicks
|> Views.PrayerRequest.list |> Views.PrayerRequest.list
{ requests = reqs { Requests = reqs
date = grp.localDateNow clock Date = grp.localDateNow clock
listGroup = grp SmallGroup = grp
showHeader = true ShowHeader = true
canEmail = ctx.Session.user |> Option.isSome CanEmail = Option.isSome ctx.Session.user
recipients = [] Recipients = []
} }
|> renderHtml next ctx |> renderHtml next ctx
| Some _ -> | Some _ ->
@ -165,29 +165,29 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
let pageNbr = let pageNbr =
match ctx.GetQueryStringValue "page" with match ctx.GetQueryStringValue "page" with
| Ok pg -> match Int32.TryParse pg with true, p -> p | false, _ -> 1 | Ok pg -> match Int32.TryParse pg with true, p -> p | false, _ -> 1
| Error _ -> 1 | Result.Error _ -> 1
let! m = backgroundTask { let! m = backgroundTask {
match ctx.GetQueryStringValue "search" with match ctx.GetQueryStringValue "search" with
| Ok search -> | Ok search ->
let! reqs = ctx.db.SearchRequestsForSmallGroup grp search pageNbr let! reqs = ctx.db.SearchRequestsForSmallGroup grp search pageNbr
return return
{ MaintainRequests.empty with { MaintainRequests.empty with
requests = reqs Requests = reqs
searchTerm = Some search SearchTerm = Some search
pageNbr = Some pageNbr PageNbr = Some pageNbr
} }
| Error _ -> | Result.Error _ ->
let! reqs = ctx.db.AllRequestsForSmallGroup grp (ctx.GetService<IClock> ()) None onlyActive pageNbr let! reqs = ctx.db.AllRequestsForSmallGroup grp (ctx.GetService<IClock> ()) None onlyActive pageNbr
return return
{ MaintainRequests.empty with { MaintainRequests.empty with
requests = reqs Requests = reqs
onlyActive = Some onlyActive OnlyActive = Some onlyActive
pageNbr = match onlyActive with true -> None | false -> Some pageNbr PageNbr = if onlyActive then None else Some pageNbr
} }
} }
return! return!
{ viewInfo ctx startTicks with helpLink = Some Help.maintainRequests } { viewInfo ctx startTicks with HelpLink = Some Help.maintainRequests }
|> Views.PrayerRequest.maintain { m with smallGroup = grp } ctx |> Views.PrayerRequest.maintain { m with SmallGroup = grp } ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
@ -210,7 +210,7 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas
let! _ = ctx.db.SaveChangesAsync () let! _ = ctx.db.SaveChangesAsync ()
addInfo ctx s["Successfully {0} prayer request", s["Restored"].Value.ToLower ()] addInfo ctx s["Successfully {0} prayer request", s["Restored"].Value.ToLower ()]
return! redirectTo false "/web/prayer-requests" next ctx 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<EditRequest> () with match! ctx.TryBindFormAsync<EditRequest> () with
| Ok m -> | Ok m ->
let! req = let! req =
if m.isNew () then Task.FromResult (Some { PrayerRequest.empty with prayerRequestId = Guid.NewGuid () }) if m.IsNew then Task.FromResult (Some { PrayerRequest.empty with prayerRequestId = Guid.NewGuid () })
else ctx.db.TryRequestById m.requestId else ctx.db.TryRequestById m.RequestId
match req with match req with
| Some pr -> | Some pr ->
let upd8 = let upd8 =
{ pr with { pr with
requestType = PrayerRequestType.fromCode m.requestType requestType = PrayerRequestType.fromCode m.RequestType
requestor = match m.requestor with Some x when x.Trim () = "" -> None | x -> x requestor = match m.Requestor with Some x when x.Trim () = "" -> None | x -> x
text = ckEditorToText m.text text = ckEditorToText m.Text
expiration = Expiration.fromCode m.expiration expiration = Expiration.fromCode m.Expiration
} }
let grp = currentGroup ctx let grp = currentGroup ctx
let now = grp.localDateNow (ctx.GetService<IClock> ()) let now = grp.localDateNow (ctx.GetService<IClock> ())
match m.isNew () with match m.IsNew with
| true -> | true ->
let dt = match m.enteredDate with Some x -> x | None -> now let dt = defaultArg m.EnteredDate now
{ upd8 with { upd8 with
smallGroupId = grp.smallGroupId smallGroupId = grp.smallGroupId
userId = (currentUser ctx).userId userId = (currentUser ctx).userId
enteredDate = dt enteredDate = dt
updatedDate = 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 } | 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! _ = ctx.db.SaveChangesAsync ()
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
let act = if m.isNew () then "Added" else "Updated" let act = if m.IsNew then "Added" else "Updated"
addInfo ctx s["Successfully {0} prayer request", s.[act].Value.ToLower ()] addInfo ctx s["Successfully {0} prayer request", s[act].Value.ToLower ()]
return! redirectTo false "/web/prayer-requests" next ctx return! redirectTo false "/web/prayer-requests" next ctx
| None -> return! fourOhFour 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) let! list = generateRequestList ctx (parseListDate date)
return! return!
viewInfo ctx startTicks viewInfo ctx startTicks
|> Views.PrayerRequest.view { list with showHeader = false } |> Views.PrayerRequest.view { list with ShowHeader = false }
|> renderHtml next ctx |> renderHtml next ctx
} }

View File

@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <OutputType>Exe</OutputType>
<PublishSingleFile>True</PublishSingleFile>
<SelfContained>False</SelfContained>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -23,9 +25,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="5.0.0" /> <PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.1" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.10" /> <PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -21,7 +21,7 @@ let private setGroupCookie (ctx : HttpContext) pwHash =
/// GET /small-group/announcement /// GET /small-group/announcement
let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx -> 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 |> Views.SmallGroup.announcement (currentUser ctx).isAdmin ctx
|> renderHtml next ctx |> renderHtml next ctx
@ -31,13 +31,13 @@ let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=>
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
match! ctx.db.TryGroupById groupId with match! ctx.db.TryGroupById groupId with
| Some grp -> | Some grp ->
let! reqs = ctx.db.CountRequestsBySmallGroup groupId let! reqs = ctx.db.CountRequestsBySmallGroup groupId
let! usrs = ctx.db.CountUsersBySmallGroup groupId let! users = ctx.db.CountUsersBySmallGroup groupId
ctx.db.RemoveEntry grp ctx.db.RemoveEntry grp
let! _ = ctx.db.SaveChangesAsync () let! _ = ctx.db.SaveChangesAsync ()
addInfo ctx addInfo ctx
s["The group {0} and its {1} prayer request(s) were deleted successfully; revoked access from {2} user(s)", 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 return! redirectTo false "/web/small-groups" next ctx
| None -> return! fourOhFour next ctx | None -> return! fourOhFour next ctx
} }
@ -82,18 +82,18 @@ let editMember (memberId : MemberId) : HttpHandler = requireAccess [ User ] >=>
let startTicks = DateTime.Now.Ticks let startTicks = DateTime.Now.Ticks
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
let grp = currentGroup ctx 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 if memberId = Guid.Empty then
return! return!
viewInfo ctx startTicks viewInfo ctx startTicks
|> Views.SmallGroup.editMember EditMember.empty typs ctx |> Views.SmallGroup.editMember EditMember.empty types ctx
|> renderHtml next ctx |> renderHtml next ctx
else else
match! ctx.db.TryMemberById memberId with match! ctx.db.TryMemberById memberId with
| Some mbr when mbr.smallGroupId = grp.smallGroupId -> | Some mbr when mbr.smallGroupId = grp.smallGroupId ->
return! return!
viewInfo ctx startTicks viewInfo ctx startTicks
|> Views.SmallGroup.editMember (EditMember.fromMember mbr) typs ctx |> Views.SmallGroup.editMember (EditMember.fromMember mbr) types ctx
|> renderHtml next ctx |> renderHtml next ctx
| Some _ | Some _
| None -> return! fourOhFour next ctx | None -> return! fourOhFour next ctx
@ -102,12 +102,12 @@ let editMember (memberId : MemberId) : HttpHandler = requireAccess [ User ] >=>
/// GET /small-group/log-on/[group-id?] /// GET /small-group/log-on/[group-id?]
let logOn (groupId : SmallGroupId option) : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { let logOn (groupId : SmallGroupId option) : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task {
let startTicks = DateTime.Now.Ticks let startTicks = DateTime.Now.Ticks
let! grps = ctx.db.ProtectedGroups () let! groups = ctx.db.ProtectedGroups ()
let grpId = match groupId with Some gid -> flatGuid gid | None -> "" let grpId = match groupId with Some gid -> flatGuid gid | None -> ""
return! return!
{ viewInfo ctx startTicks with helpLink = Some Help.logOn } { viewInfo ctx startTicks with HelpLink = Some Help.logOn }
|> Views.SmallGroup.logOn grps grpId ctx |> Views.SmallGroup.logOn groups grpId ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
@ -117,28 +117,26 @@ let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validat
match! ctx.TryBindFormAsync<GroupLogOn> () with match! ctx.TryBindFormAsync<GroupLogOn> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () 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 -> | Some grp ->
ctx.Session.smallGroup <- Some grp ctx.Session.smallGroup <- Some grp
match m.rememberMe with if defaultArg m.RememberMe false then (setGroupCookie ctx << sha1Hash) m.Password
| Some x when x -> (setGroupCookie ctx << sha1Hash) m.password
| _ -> ()
addInfo ctx s["Log On Successful Welcome to {0}", s["PrayerTracker"]] addInfo ctx s["Log On Successful Welcome to {0}", s["PrayerTracker"]]
return! redirectTo false "/web/prayer-requests/view" next ctx return! redirectTo false "/web/prayer-requests/view" next ctx
| None -> | None ->
addError ctx s["Password incorrect - login unsuccessful"] addError ctx s["Password incorrect - login unsuccessful"]
return! redirectTo false $"/web/small-group/log-on/{flatGuid m.smallGroupId}" next ctx return! redirectTo false $"/web/small-group/log-on/{flatGuid m.SmallGroupId}" next ctx
| Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// GET /small-groups /// GET /small-groups
let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
let startTicks = DateTime.Now.Ticks let startTicks = DateTime.Now.Ticks
let! grps = ctx.db.AllGroups () let! groups = ctx.db.AllGroups ()
return! return!
viewInfo ctx startTicks viewInfo ctx startTicks
|> Views.SmallGroup.maintain grps ctx |> Views.SmallGroup.maintain groups ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
@ -148,11 +146,11 @@ let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let startTicks = DateTime.Now.Ticks let startTicks = DateTime.Now.Ticks
let grp = currentGroup ctx let grp = currentGroup ctx
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
let! mbrs = ctx.db.AllMembersForSmallGroup grp.smallGroupId let! members = ctx.db.AllMembersForSmallGroup grp.smallGroupId
let typs = ReferenceList.emailTypeList grp.preferences.defaultEmailType s |> Map.ofSeq let types = ReferenceList.emailTypeList grp.preferences.defaultEmailType s |> Map.ofSeq
return! return!
{ viewInfo ctx startTicks with helpLink = Some Help.maintainGroupMembers } { viewInfo ctx startTicks with HelpLink = Some Help.maintainGroupMembers }
|> Views.SmallGroup.members mbrs typs ctx |> Views.SmallGroup.members members types ctx
|> renderHtml next 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! reqCount = ctx.db.CountRequestsBySmallGroup (currentGroup ctx).smallGroupId
let! mbrCount = ctx.db.CountMembersForSmallGroup (currentGroup ctx).smallGroupId let! mbrCount = ctx.db.CountMembersForSmallGroup (currentGroup ctx).smallGroupId
let m = let m =
{ totalActiveReqs = List.length reqs { TotalActiveReqs = List.length reqs
allReqs = reqCount AllReqs = reqCount
totalMbrs = mbrCount TotalMembers = mbrCount
activeReqsByCat = ActiveReqsByType =
(reqs (reqs
|> Seq.ofList |> Seq.ofList
|> Seq.map (fun req -> req.requestType) |> 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 startTicks = DateTime.Now.Ticks
let! tzs = ctx.db.AllTimeZones () let! tzs = ctx.db.AllTimeZones ()
return! 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 |> Views.SmallGroup.preferences (EditPreferences.fromPreferences (currentGroup ctx).preferences) tzs ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
@ -200,22 +198,22 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
let! group = let! group =
if m.isNew () then Task.FromResult (Some { SmallGroup.empty with smallGroupId = Guid.NewGuid () }) if m.IsNew then Task.FromResult (Some { SmallGroup.empty with smallGroupId = Guid.NewGuid () })
else ctx.db.TryGroupById m.smallGroupId else ctx.db.TryGroupById m.SmallGroupId
match group with match group with
| Some grp -> | Some grp ->
m.populateGroup grp m.populateGroup grp
|> function |> function
| grp when m.isNew () -> | grp when m.IsNew ->
ctx.db.AddEntry grp ctx.db.AddEntry grp
ctx.db.AddEntry { grp.preferences with smallGroupId = grp.smallGroupId } ctx.db.AddEntry { grp.preferences with smallGroupId = grp.smallGroupId }
| grp -> ctx.db.UpdateEntry grp | grp -> ctx.db.UpdateEntry grp
let! _ = ctx.db.SaveChangesAsync () let! _ = ctx.db.SaveChangesAsync ()
let act = s[if m.isNew () then "Added" else "Updated"].Value.ToLower () let act = s[if m.IsNew then "Added" else "Updated"].Value.ToLower ()
addHtmlInfo ctx s["Successfully {0} group “{1}”", act, m.name] addHtmlInfo ctx s["Successfully {0} group “{1}”", act, m.Name]
return! redirectTo false "/web/small-groups" next ctx return! redirectTo false "/web/small-groups" next ctx
| None -> return! fourOhFour 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 -> | Ok m ->
let grp = currentGroup ctx let grp = currentGroup ctx
let! mMbr = let! mMbr =
if m.isNew () then if m.IsNew then
Task.FromResult (Some { Member.empty with memberId = Guid.NewGuid (); smallGroupId = grp.smallGroupId }) 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 match mMbr with
| Some mbr when mbr.smallGroupId = grp.smallGroupId -> | Some mbr when mbr.smallGroupId = grp.smallGroupId ->
{ mbr with { mbr with
memberName = m.memberName memberName = m.Name
email = m.emailAddress email = m.Email
format = match m.emailType with "" | null -> None | _ -> Some m.emailType 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! _ = ctx.db.SaveChangesAsync ()
let s = Views.I18N.localizer.Force () 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] addInfo ctx s["Successfully {0} group member", act]
return! redirectTo false "/web/small-group/members" next ctx return! redirectTo false "/web/small-group/members" next ctx
| Some _ | Some _
| None -> return! fourOhFour next ctx | 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. // database values, not the then out-of-sync session ones.
match! ctx.db.TryGroupById (currentGroup ctx).smallGroupId with match! ctx.db.TryGroupById (currentGroup ctx).smallGroupId with
| Some grp -> | Some grp ->
let prefs = m.populatePreferences grp.preferences let prefs = m.PopulatePreferences grp.preferences
ctx.db.UpdateEntry prefs ctx.db.UpdateEntry prefs
let! _ = ctx.db.SaveChangesAsync () let! _ = ctx.db.SaveChangesAsync ()
// Refresh session instance // Refresh session instance
@ -265,7 +263,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=>
addInfo ctx s["Group preferences updated successfully"] addInfo ctx s["Group preferences updated successfully"]
return! redirectTo false "/web/small-group/preferences" next ctx return! redirectTo false "/web/small-group/preferences" next ctx
| None -> return! fourOhFour 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<IClock> ()) let now = grp.localTimeNow (ctx.GetService<IClock> ())
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
// Reformat the text to use the class's font stylings // Reformat the text to use the class's font stylings
let requestText = ckEditorToText m.text let requestText = ckEditorToText m.Text
let htmlText = let htmlText =
p [ _style $"font-family:{grp.preferences.listFonts};font-size:%d{grp.preferences.textFontSize}pt;" ] p [ _style $"font-family:{grp.preferences.listFonts};font-size:%d{grp.preferences.textFontSize}pt;" ]
[ rawText requestText ] [ rawText requestText ]
@ -287,7 +285,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=>
let plainText = (htmlToPlainText >> wordWrap 74) htmlText let plainText = (htmlToPlainText >> wordWrap 74) htmlText
// Send the e-mails // Send the e-mails
let! recipients = let! recipients =
match m.sendToClass with match m.SendToClass with
| "N" when usr.isAdmin -> ctx.db.AllUsersAsMembers () | "N" when usr.isAdmin -> ctx.db.AllUsersAsMembers ()
| _ -> ctx.db.AllMembersForSmallGroup grp.smallGroupId | _ -> ctx.db.AllMembersForSmallGroup grp.smallGroupId
use! client = Email.getConnection () use! client = Email.getConnection ()
@ -296,7 +294,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=>
(now.ToString "h:mm tt").ToLower ()].Value (now.ToString "h:mm tt").ToLower ()].Value
htmlText plainText s htmlText plainText s
// Add to the request list if desired // Add to the request list if desired
match m.sendToClass, m.addToRequestList with match m.SendToClass, m.AddToRequestList with
| "N", _ | "N", _
| _, None -> () | _, None -> ()
| _, Some x when not x -> () | _, Some x when not x -> ()
@ -305,7 +303,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=>
prayerRequestId = Guid.NewGuid () prayerRequestId = Guid.NewGuid ()
smallGroupId = grp.smallGroupId smallGroupId = grp.smallGroupId
userId = usr.userId userId = usr.userId
requestType = (Option.get >> PrayerRequestType.fromCode) m.requestType requestType = (Option.get >> PrayerRequestType.fromCode) m.RequestType
text = requestText text = requestText
enteredDate = now enteredDate = now
updatedDate = now updatedDate = now
@ -315,14 +313,14 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=>
() ()
// Tell 'em what they've won, Johnny! // Tell 'em what they've won, Johnny!
let toWhom = let toWhom =
match m.sendToClass with match m.SendToClass with
| "N" -> s["{0} users", s["PrayerTracker"]].Value | "N" -> s["{0} users", s["PrayerTracker"]].Value
| _ -> s["Group Members"].Value.ToLower () | _ -> 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]] addInfo ctx s["Successfully sent announcement to all {0} {1}", toWhom, s[andAdded]]
return! return!
viewInfo ctx startTicks viewInfo ctx startTicks
|> Views.SmallGroup.announcementSent { m with text = htmlText } |> Views.SmallGroup.announcementSent { m with Text = htmlText }
|> renderHtml next ctx |> renderHtml next ctx
| Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }

View File

@ -23,18 +23,18 @@ let private setUserCookie (ctx : HttpContext) pwHash =
/// Retrieve a user from the database by password /// 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 // 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 { 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 -> | Some u when Option.isSome u.salt ->
// Already upgraded; match = success // 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 if u.passwordHash = pwHash then
return Some { u with passwordHash = ""; salt = None; smallGroups = List<UserSmallGroup>() }, pwHash return Some { u with passwordHash = ""; salt = None; smallGroups = List<UserSmallGroup>() }, pwHash
else return None, "" 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! // Not upgraded, but password is good; upgrade 'em!
// Upgrade 'em! // Upgrade 'em!
let salt = Guid.NewGuid () 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 } let upgraded = { u with salt = Some salt; passwordHash = pwHash }
db.UpdateEntry upgraded db.UpdateEntry upgraded
let! _ = db.SaveChangesAsync () let! _ = db.SaveChangesAsync ()
@ -54,16 +54,16 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> f
match dbUsr with match dbUsr with
| Some usr -> | Some usr ->
// Check the old password against a possibly non-salted hash // 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 |> ctx.db.TryUserLogOnByCookie curUsr.userId (currentGroup ctx).smallGroupId
| _ -> Task.FromResult None | _ -> Task.FromResult None
match user with match user with
| Some _ when m.newPassword = m.newPasswordConfirm -> | Some _ when m.NewPassword = m.NewPasswordConfirm ->
match dbUsr with match dbUsr with
| Some usr -> | Some usr ->
// Generate new salt whenever the password is changed // Generate new salt whenever the password is changed
let salt = Guid.NewGuid () 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 () let! _ = ctx.db.SaveChangesAsync ()
// If the user is remembered, update the cookie with the new hash // 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 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 -> | None ->
addError ctx s["The old password was incorrect - your password was NOT changed"] addError ctx s["The old password was incorrect - your password was NOT changed"]
return! redirectTo false "/web/user/password" next ctx 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 -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
let! usr, pwHash = findUserByPassword m ctx.db let! usr, pwHash = findUserByPassword m ctx.db
let! grp = ctx.db.TryGroupById m.smallGroupId let! grp = ctx.db.TryGroupById m.SmallGroupId
let nextUrl = let nextUrl =
match usr with match usr with
| Some _ -> | Some _ ->
ctx.Session.user <- usr ctx.Session.user <- usr
ctx.Session.smallGroup <- grp 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"]] addHtmlInfo ctx s["Log On Successful Welcome to {0}", s["PrayerTracker"]]
match m.redirectUrl with match m.RedirectUrl with
| None -> "/web/small-group" | None -> "/web/small-group"
| Some x when x = "" -> "/web/small-group" | Some x when x = "" -> "/web/small-group"
| Some x -> x | Some x -> x
| _ -> | _ ->
let grpName = match grp with Some g -> g.name | _ -> "N/A" let grpName = match grp with Some g -> g.name | _ -> "N/A"
{ UserMessage.error with { UserMessage.error with
text = htmlLocString s["Invalid credentials - log on unsuccessful"] Text = htmlLocString s["Invalid credentials - log on unsuccessful"]
description = Description =
[ s["This is likely due to one of the following reasons"].Value [ s["This is likely due to one of the following reasons"].Value
":<ul><li>" ":<ul><li>"
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
"</li><li>" "</li><li>"
s["The password entered does not match the password for the given e-mail address."].Value s["The password entered does not match the password for the given e-mail address."].Value
"</li><li>" "</li><li>"
@ -132,7 +132,7 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSR
|> addUserMessage ctx |> addUserMessage ctx
"/web/user/log-on" "/web/user/log-on"
return! redirectTo false nextUrl next ctx 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."] addWarning ctx s["The page you requested requires authentication; please log on below."]
| None -> () | None -> ()
return! return!
{ viewInfo ctx startTicks with helpLink = Some Help.logOn } { viewInfo ctx startTicks with HelpLink = Some Help.logOn }
|> Views.User.logOn { UserLogOn.empty with redirectUrl = url } groups ctx |> Views.User.logOn { UserLogOn.empty with RedirectUrl = url } groups ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
@ -186,7 +186,7 @@ let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
/// GET /user/password /// GET /user/password
let password : HttpHandler = requireAccess [ User ] >=> fun next ctx -> 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 |> Views.User.changePassword ctx
|> renderHtml next ctx |> renderHtml next ctx
@ -196,13 +196,13 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
match! ctx.TryBindFormAsync<EditUser> () with match! ctx.TryBindFormAsync<EditUser> () with
| Ok m -> | Ok m ->
let! user = let! user =
if m.isNew () then Task.FromResult (Some { User.empty with userId = Guid.NewGuid () }) if m.IsNew then Task.FromResult (Some { User.empty with userId = Guid.NewGuid () })
else ctx.db.TryUserById m.userId else ctx.db.TryUserById m.UserId
let saltedUser = let saltedUser =
match user with match user with
| Some u -> | Some u ->
match u.salt with match u.salt with
| None when m.password <> "" -> | None when m.Password <> "" ->
// Generate salt so that a new password hash can be generated // Generate salt so that a new password hash can be generated
Some { u with salt = Some (Guid.NewGuid ()) } Some { u with salt = Some (Guid.NewGuid ()) }
| _ -> | _ ->
@ -211,15 +211,15 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
| _ -> user | _ -> user
match saltedUser with match saltedUser with
| Some u -> | Some u ->
let updatedUser = m.populateUser u (pbkdf2Hash (Option.get u.salt)) let updatedUser = m.PopulateUser u (pbkdf2Hash (Option.get u.salt))
updatedUser |> (if m.isNew () then ctx.db.AddEntry else ctx.db.UpdateEntry) updatedUser |> if m.IsNew then ctx.db.AddEntry else ctx.db.UpdateEntry
let! _ = ctx.db.SaveChangesAsync () let! _ = ctx.db.SaveChangesAsync ()
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
if m.isNew () then if m.IsNew then
let h = CommonFunctions.htmlString let h = CommonFunctions.htmlString
{ UserMessage.info with { UserMessage.info with
text = h s["Successfully {0} user", s["Added"].Value.ToLower ()] Text = h s["Successfully {0} user", s["Added"].Value.ToLower ()]
description = Description =
h s["Please select at least one group for which this user ({0}) is authorized", h s["Please select at least one group for which this user ({0}) is authorized",
updatedUser.fullName] updatedUser.fullName]
|> Some |> Some
@ -230,7 +230,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
addInfo ctx s["Successfully {0} user", s["Updated"].Value.ToLower ()] addInfo ctx s["Successfully {0} user", s["Updated"].Value.ToLower ()]
return! redirectTo false "/web/users" next ctx return! redirectTo false "/web/users" next ctx
| None -> return! fourOhFour 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<AssignGroups> () with match! ctx.TryBindFormAsync<AssignGroups> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
match Seq.length m.smallGroups with match Seq.length m.SmallGroups with
| 0 -> | 0 ->
addError ctx s["You must select at least one group to assign"] 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 -> | Some user ->
let grps = let groups =
m.smallGroups.Split ',' m.SmallGroups.Split ','
|> Array.map Guid.Parse |> Array.map Guid.Parse
|> List.ofArray |> List.ofArray
user.smallGroups 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 |> ctx.db.UserGroupXref.RemoveRange
grps groups
|> Seq.ofList |> Seq.ofList
|> Seq.filter (fun x -> not (user.smallGroups |> Seq.exists (fun y -> y.smallGroupId = x))) |> 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 }) |> Seq.map (fun x -> { UserSmallGroup.empty with userId = user.userId; smallGroupId = x })
|> List.ofSeq |> List.ofSeq
|> List.iter ctx.db.AddEntry |> List.iter ctx.db.AddEntry
let! _ = ctx.db.SaveChangesAsync () 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! redirectTo false "/web/users" next ctx
| _ -> return! fourOhFour 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 let startTicks = DateTime.Now.Ticks
match! ctx.db.TryUserByIdWithGroups userId with match! ctx.db.TryUserByIdWithGroups userId with
| Some user -> | 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 let curGroups = user.smallGroups |> Seq.map (fun g -> flatGuid g.smallGroupId) |> List.ofSeq
return! return!
viewInfo ctx startTicks viewInfo ctx startTicks
|> Views.User.assignGroups (AssignGroups.fromUser user) grps curGroups ctx |> Views.User.assignGroups (AssignGroups.fromUser user) groups curGroups ctx
|> renderHtml next ctx |> renderHtml next ctx
| None -> return! fourOhFour next ctx | None -> return! fourOhFour next ctx
} }

View File

@ -210,8 +210,8 @@ const PT = {
*/ */
toggleType(name) { toggleType(name) {
const isNamed = document.getElementById(`${name}Type_Name`) const isNamed = document.getElementById(`${name}Type_Name`)
const named = document.getElementById(`${name}Color_Select`) const named = document.getElementById(`${name}_Select`)
const custom = document.getElementById(`${name}Color_Color`) const custom = document.getElementById(`${name}_Color`)
if (isNamed.checked) { if (isNamed.checked) {
custom.disabled = true custom.disabled = true
named.disabled = false named.disabled = false
@ -244,7 +244,7 @@ const PT = {
PT.smallGroup.preferences.checkVisibility) PT.smallGroup.preferences.checkVisibility)
}) })
PT.smallGroup.preferences.checkVisibility() PT.smallGroup.preferences.checkVisibility()
;['headingLine', 'headingText'].map(name => { ;['lineColor', 'headingColor'].map(name => {
document.getElementById(`${name}Type_Name`).addEventListener('click', () => { document.getElementById(`${name}Type_Name`).addEventListener('click', () => {
PT.smallGroup.preferences.toggleType(name) PT.smallGroup.preferences.toggleType(name)
}) })