From 42e3a581318debfe9e78c526b08fdbce8e83c8cf Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 30 Jan 2025 20:36:00 -0500 Subject: [PATCH] Move module funcs to properties --- src/PrayerTracker.Data/Access.fs | 122 ++++----- src/PrayerTracker.Data/Entities.fs | 172 ++++++------ src/PrayerTracker.Tests/Data/EntitiesTests.fs | 114 ++++---- src/PrayerTracker.Tests/UI/ViewModelsTests.fs | 64 ++--- src/PrayerTracker.UI/CommonFunctions.fs | 14 +- src/PrayerTracker.UI/PrayerRequest.fs | 8 +- src/PrayerTracker.UI/SmallGroup.fs | 6 +- src/PrayerTracker.UI/ViewModels.fs | 244 +++++++++--------- src/PrayerTracker/Church.fs | 4 +- src/PrayerTracker/PrayerRequest.fs | 14 +- src/PrayerTracker/SmallGroup.fs | 12 +- src/PrayerTracker/User.fs | 20 +- 12 files changed, 384 insertions(+), 410 deletions(-) diff --git a/src/PrayerTracker.Data/Access.fs b/src/PrayerTracker.Data/Access.fs index d08e68d..4d77c41 100644 --- a/src/PrayerTracker.Data/Access.fs +++ b/src/PrayerTracker.Data/Access.fs @@ -8,7 +8,7 @@ open PrayerTracker.Entities /// Helper functions for the PostgreSQL data implementation [] module private Helpers = - + /// Map a row to a Church instance let mapToChurch (row : RowReader) = { Id = ChurchId (row.uuid "id") @@ -18,7 +18,7 @@ module private Helpers = HasVpsInterface = row.bool "has_vps_interface" InterfaceAddress = row.stringOrNone "interface_address" } - + /// Map a row to a ListPreferences instance let mapToListPreferences (row : RowReader) = { SmallGroupId = SmallGroupId (row.uuid "small_group_id") @@ -40,7 +40,7 @@ module private Helpers = DefaultEmailType = EmailFormat.Parse (row.string "default_email_type") AsOfDateDisplay = AsOfDateDisplay.Parse (row.string "as_of_date_display") } - + /// Map a row to a Member instance let mapToMember (row : RowReader) = { Id = MemberId (row.uuid "id") @@ -49,7 +49,7 @@ module private Helpers = Email = row.string "email" Format = row.stringOrNone "email_format" |> Option.map EmailFormat.Parse } - + /// Map a row to a Prayer Request instance let mapToPrayerRequest (row : RowReader) = { Id = PrayerRequestId (row.uuid "id") @@ -63,15 +63,15 @@ module private Helpers = RequestType = PrayerRequestType.Parse (row.string "request_type") Expiration = Expiration.Parse (row.string "expiration") } - + /// Map a row to a Small Group instance let mapToSmallGroup (row : RowReader) = { Id = SmallGroupId (row.uuid "id") ChurchId = ChurchId (row.uuid "church_id") Name = row.string "group_name" - Preferences = ListPreferences.empty + Preferences = ListPreferences.Empty } - + /// Map a row to a Small Group information set let mapToSmallGroupInfo (row : RowReader) = { Id = Giraffe.ShortGuid.fromGuid (row.uuid "id") @@ -80,17 +80,17 @@ module private Helpers = TimeZoneId = TimeZoneId (row.string "time_zone_id") IsPublic = row.bool "is_public" } - + /// Map a row to a Small Group list item let mapToSmallGroupItem (row : RowReader) = Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}""" - - /// Map a row to a Small Group instance with populated list preferences + + /// Map a row to a Small Group instance with populated list preferences let mapToSmallGroupWithPreferences (row : RowReader) = { mapToSmallGroup row with Preferences = mapToListPreferences row } - + /// Map a row to a User instance let mapToUser (row : RowReader) = { Id = UserId (row.uuid "id") @@ -107,11 +107,11 @@ open BitBadger.Documents.Postgres /// Functions to manipulate churches module Churches = - + /// Get a list of all churches let all () = Custom.list "SELECT * FROM pt.church ORDER BY church_name" [] mapToChurch - + /// Delete a church by its ID let deleteById (churchId : ChurchId) = backgroundTask { let idParam = [ [ "@churchId", Sql.uuid churchId.Value ] ] @@ -127,7 +127,7 @@ module Churches = "DELETE FROM pt.church WHERE id = @churchId", idParam ] () } - + /// Save a church's information let save (church : Church) = Custom.nonQuery @@ -147,7 +147,7 @@ module Churches = "@state", Sql.string church.State "@hasVpsInterface", Sql.bool church.HasVpsInterface "@interfaceAddress", Sql.stringOrNone church.InterfaceAddress ] - + /// Find a church by its ID let tryById (churchId : ChurchId) = Custom.single "SELECT * FROM pt.church WHERE id = @id" [ "@id", Sql.uuid churchId.Value ] mapToChurch @@ -155,21 +155,21 @@ module Churches = /// Functions to manipulate small group members module Members = - + /// Count members for the given small group let countByGroup (groupId : SmallGroupId) = Custom.scalar "SELECT COUNT(id) AS mbr_count FROM pt.member WHERE small_group_id = @groupId" [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "mbr_count") - + /// Delete a small group member by its ID let deleteById (memberId : MemberId) = Custom.nonQuery "DELETE FROM pt.member WHERE id = @id" [ "@id", Sql.uuid memberId.Value ] - + /// Retrieve all members for a given small group let forGroup (groupId : SmallGroupId) = Custom.list "SELECT * FROM pt.member WHERE small_group_id = @groupId ORDER BY member_name" [ "@groupId", Sql.uuid groupId.Value ] mapToMember - + /// Save a small group member let save (mbr : Member) = Custom.nonQuery @@ -186,7 +186,7 @@ module Members = "@name", Sql.string mbr.Name "@email", Sql.string mbr.Email "@format", Sql.stringOrNone (mbr.Format |> Option.map string) ] - + /// Retrieve a small group member by its ID let tryById (memberId : MemberId) = Custom.single "SELECT * FROM pt.member WHERE id = @id" [ "@id", Sql.uuid memberId.Value ] mapToMember @@ -196,16 +196,16 @@ module Members = type PrayerRequestOptions = { /// The small group for which requests should be retrieved SmallGroup : SmallGroup - + /// The clock instance to use for date/time manipulation Clock : IClock - + /// The date for which the list is being retrieved ListDate : LocalDate option - + /// Whether only active requests should be retrieved ActiveOnly : bool - + /// The page number, for paged lists PageNumber : int } @@ -213,17 +213,17 @@ type PrayerRequestOptions = /// Functions to manipulate prayer requests module PrayerRequests = - + /// Central place to append sort criteria for prayer request queries let private orderBy sort = match sort with | SortByDate -> "updated_date DESC, entered_date DESC, requestor" | SortByRequestor -> "requestor, updated_date DESC, entered_date DESC" - + /// Paginate a prayer request query let private paginate (pageNbr : int) pageSize = if pageNbr > 0 then $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" else "" - + /// Count the number of prayer requests for a church let countByChurch (churchId : ChurchId) = Custom.scalar @@ -231,24 +231,24 @@ module PrayerRequests = FROM pt.prayer_request WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)" [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "req_count") - + /// Count the number of prayer requests for a small group let countByGroup (groupId : SmallGroupId) = Custom.scalar "SELECT COUNT(id) AS req_count FROM pt.prayer_request WHERE small_group_id = @groupId" [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "req_count") - + /// Delete a prayer request by its ID let deleteById (reqId : PrayerRequestId) = Custom.nonQuery "DELETE FROM pt.prayer_request WHERE id = @id" [ "@id", Sql.uuid reqId.Value ] - + /// Get all (or active) requests for a small group as of now or the specified date let forGroup (opts : PrayerRequestOptions) = - let theDate = defaultArg opts.ListDate (SmallGroup.localDateNow opts.Clock opts.SmallGroup) + let theDate = defaultArg opts.ListDate (opts.SmallGroup.LocalDateNow opts.Clock) let where, parameters = if opts.ActiveOnly then let asOf = NpgsqlParameter ( "@asOf", - (theDate.AtStartOfDayInZone(SmallGroup.timeZone opts.SmallGroup) + (theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone) - Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire) .ToInstant ()) " AND ( updated_date > @asOf @@ -269,7 +269,7 @@ module PrayerRequests = ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort} {paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}" (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) mapToPrayerRequest - + /// Save a prayer request let save (req : PrayerRequest) = Custom.nonQuery @@ -296,10 +296,10 @@ module PrayerRequests = "@text", Sql.string req.Text "@notifyChaplain", Sql.bool req.NotifyChaplain "@expiration", Sql.string (string req.Expiration) ] - + /// Search prayer requests for the given term let searchForGroup group searchTerm pageNbr = - Custom.list + Custom.list $"SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND request_text ILIKE @search UNION SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search @@ -311,7 +311,7 @@ module PrayerRequests = let tryById (reqId : PrayerRequestId) = Custom.single "SELECT * FROM pt.prayer_request WHERE id = @id" [ "@id", Sql.uuid reqId.Value ] mapToPrayerRequest - + /// Update the expiration for the given prayer request let updateExpiration (req : PrayerRequest) withTime = let sql, parameters = @@ -326,12 +326,12 @@ module PrayerRequests = /// Functions to retrieve small group information module SmallGroups = - + /// Count the number of small groups for a church let countByChurch (churchId : ChurchId) = Custom.scalar "SELECT COUNT(id) AS group_count FROM pt.small_group WHERE church_id = @churchId" [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "group_count") - + /// Delete a small group by its ID let deleteById (groupId : SmallGroupId) = backgroundTask { let idParam = [ [ "@groupId", Sql.uuid groupId.Value ] ] @@ -345,7 +345,7 @@ module SmallGroups = "DELETE FROM pt.small_group WHERE id = @groupId", idParam ] () } - + /// Get information for all small groups let infoForAll () = Custom.list @@ -355,7 +355,7 @@ module SmallGroups = INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id ORDER BY sg.group_name" [] mapToSmallGroupInfo - + /// Get a list of small group IDs along with a description that includes the church name let listAll () = Custom.list @@ -364,7 +364,7 @@ module SmallGroups = INNER JOIN pt.church c ON c.id = g.church_id ORDER BY c.church_name, g.group_name" [] mapToSmallGroupItem - + /// Get a list of small group IDs and descriptions for groups with a group password let listProtected () = Custom.list @@ -375,7 +375,7 @@ module SmallGroups = WHERE COALESCE(lp.group_password, '') <> '' ORDER BY c.church_name, g.group_name" [] mapToSmallGroupItem - + /// Get a list of small group IDs and descriptions for groups that are public or have a group password let listPublicAndProtected () = Custom.list @@ -387,7 +387,7 @@ module SmallGroups = OR COALESCE(lp.group_password, '') <> '' ORDER BY c.church_name, g.group_name" [] mapToSmallGroupInfo - + /// Log on for a small group (includes list preferences) let logOn (groupId : SmallGroupId) password = Custom.single @@ -397,7 +397,7 @@ module SmallGroups = WHERE sg.id = @id AND lp.group_password = @password" [ "@id", Sql.uuid groupId.Value; "@password", Sql.string password ] mapToSmallGroupWithPreferences - + /// Save a small group let save (group : SmallGroup) isNew = backgroundTask { let! _ = @@ -420,7 +420,7 @@ module SmallGroups = ] () } - + /// Save a small group's list preferences let savePreferences (pref : ListPreferences) = Custom.nonQuery @@ -458,14 +458,14 @@ module SmallGroups = "@groupPassword", Sql.string pref.GroupPassword "@defaultEmailType", Sql.string (string pref.DefaultEmailType) "@isPublic", Sql.bool pref.IsPublic - "@timeZoneId", Sql.string (TimeZoneId.toString pref.TimeZoneId) + "@timeZoneId", Sql.string (string pref.TimeZoneId) "@pageSize", Sql.int pref.PageSize "@asOfDateDisplay", Sql.string (string pref.AsOfDateDisplay) ] - + /// Get a small group by its ID let tryById (groupId : SmallGroupId) = Custom.single "SELECT * FROM pt.small_group WHERE id = @id" [ "@id", Sql.uuid groupId.Value ] mapToSmallGroup - + /// Get a small group by its ID with its list preferences populated let tryByIdWithPreferences (groupId : SmallGroupId) = Custom.single @@ -478,11 +478,11 @@ module SmallGroups = /// Functions to manipulate users module Users = - + /// Retrieve all PrayerTracker users let all () = Custom.list "SELECT * FROM pt.pt_user ORDER BY last_name, first_name" [] mapToUser - + /// Count the number of users for a church let countByChurch (churchId : ChurchId) = Custom.scalar @@ -495,21 +495,21 @@ module Users = WHERE usg.user_id = u.id AND sg.church_id = @churchId)" [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "user_count") - + /// Count the number of users for a small group let countByGroup (groupId : SmallGroupId) = Custom.scalar "SELECT COUNT(user_id) AS user_count FROM pt.user_small_group WHERE small_group_id = @groupId" [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "user_count") - + /// Delete a user by its database ID let deleteById (userId : UserId) = Custom.nonQuery "DELETE FROM pt.pt_user WHERE id = @id" [ "@id", Sql.uuid userId.Value ] - + /// Get the IDs of the small groups for which the given user is authorized let groupIdsByUserId (userId : UserId) = Custom.list "SELECT small_group_id FROM pt.user_small_group WHERE user_id = @id" [ "@id", Sql.uuid userId.Value ] (fun row -> SmallGroupId (row.uuid "small_group_id")) - + /// Get a list of users authorized to administer the given small group let listByGroupId (groupId : SmallGroupId) = Custom.list @@ -519,9 +519,9 @@ module Users = WHERE usg.small_group_id = @groupId ORDER BY u.last_name, u.first_name" [ "@groupId", Sql.uuid groupId.Value ] mapToUser - + /// Save a user's information - let save (user : User) = + let save (user : User) = Custom.nonQuery "INSERT INTO pt.pt_user ( id, first_name, last_name, email, is_admin, password_hash @@ -539,7 +539,7 @@ module Users = "@email", Sql.string user.Email "@isAdmin", Sql.bool user.IsAdmin "@passwordHash", Sql.string user.PasswordHash ] - + /// Find a user by its e-mail address and authorized small group let tryByEmailAndGroup email (groupId : SmallGroupId) = Custom.single @@ -548,21 +548,21 @@ module Users = INNER JOIN pt.user_small_group usg ON usg.user_id = u.id AND usg.small_group_id = @groupId WHERE u.email = @email" [ "@email", Sql.string email; "@groupId", Sql.uuid groupId.Value ] mapToUser - + /// Find a user by their database ID let tryById (userId : UserId) = Custom.single "SELECT * FROM pt.pt_user WHERE id = @id" [ "@id", Sql.uuid userId.Value ] mapToUser - + /// Update a user's last seen date/time let updateLastSeen (userId : UserId) (now : Instant) = Custom.nonQuery "UPDATE pt.pt_user SET last_seen = @now WHERE id = @id" [ "@id", Sql.uuid userId.Value; "@now", Sql.parameter (NpgsqlParameter ("@now", now)) ] - + /// Update a user's password hash let updatePassword (user : User) = Custom.nonQuery "UPDATE pt.pt_user SET password_hash = @passwordHash WHERE id = @id" [ "@id", Sql.uuid user.Id.Value; "@passwordHash", Sql.string user.PasswordHash ] - + /// Update a user's authorized small groups let updateSmallGroups (userId : UserId) groupIds = backgroundTask { let! existingGroupIds = groupIdsByUserId userId diff --git a/src/PrayerTracker.Data/Entities.fs b/src/PrayerTracker.Data/Entities.fs index aaf7e0a..610a2fd 100644 --- a/src/PrayerTracker.Data/Entities.fs +++ b/src/PrayerTracker.Data/Entities.fs @@ -174,14 +174,11 @@ type SmallGroupId = /// PK type for the TimeZone entity -type TimeZoneId = TimeZoneId of string +type TimeZoneId = + | TimeZoneId of string -/// Functions to support time zone IDs -module TimeZoneId = - - /// Convert a time zone ID to its string value - let toString = - function + override this.ToString() = + match this with | TimeZoneId it -> it @@ -259,12 +256,9 @@ type Church = InterfaceAddress: string option } -/// Functions to support churches -module Church = - /// An empty church // aww... how sad :( - let empty = + static member Empty = { Id = ChurchId Guid.Empty Name = "" City = "" @@ -339,11 +333,8 @@ type ListPreferences = else this.Fonts -/// Functions to support list preferences -module ListPreferences = - /// A set of preferences with their default values - let empty = + static member Empty = { SmallGroupId = SmallGroupId Guid.Empty DaysToExpire = 14 DaysToKeepNew = 7 @@ -384,11 +375,8 @@ type Member = Format: EmailFormat option } -/// Functions to support small group members -module Member = - /// An empty member - let empty = + static member Empty = { Id = MemberId Guid.Empty SmallGroupId = SmallGroupId Guid.Empty Name = "" @@ -396,6 +384,50 @@ module Member = Format = None } +/// This represents a small group (Sunday School class, Bible study group, etc.) +[] +type SmallGroup = + { + /// The ID of this small group + Id: SmallGroupId + + /// The church to which this group belongs + ChurchId: ChurchId + + /// The name of the group + Name: string + + /// The preferences for the request list + Preferences: ListPreferences + } + + /// The DateTimeZone for the time zone ID for this small group + member this.TimeZone = + let tzId = string this.Preferences.TimeZoneId + + if DateTimeZoneProviders.Tzdb.Ids.Contains tzId then + DateTimeZoneProviders.Tzdb[tzId] + else + DateTimeZone.Utc + + /// Get the local date/time for this group + member this.LocalTimeNow(clock: IClock) = + if isNull clock then + nullArg (nameof clock) + + clock.GetCurrentInstant().InZone(this.TimeZone).LocalDateTime + + /// Get the local date for this group + member this.LocalDateNow clock = this.LocalTimeNow(clock).Date + + /// An empty small group + static member Empty = + { Id = SmallGroupId Guid.Empty + ChurchId = ChurchId Guid.Empty + Name = "" + Preferences = ListPreferences.Empty } + + /// This represents a single prayer request [] type PrayerRequest = @@ -430,61 +462,31 @@ type PrayerRequest = /// Is this request expired? Expiration: Expiration } -// functions are below small group functions + /// Is this request expired? + member this.IsExpired (asOf: LocalDate) (group: SmallGroup) = + match this.Expiration, this.RequestType with + | Forced, _ -> true + | Manual, _ + | Automatic, LongTermRequest + | Automatic, Expecting -> false + | Automatic, _ -> + // Automatic expiration + Period + .Between(this.UpdatedDate.InZone(group.TimeZone).Date, asOf, PeriodUnits.Days) + .Days + >= group.Preferences.DaysToExpire -/// This represents a small group (Sunday School class, Bible study group, etc.) -[] -type SmallGroup = - { - /// The ID of this small group - Id: SmallGroupId - - /// The church to which this group belongs - ChurchId: ChurchId - - /// The name of the group - Name: string - - /// The preferences for the request list - Preferences: ListPreferences - } - -/// Functions to support small groups -module SmallGroup = - - /// An empty small group - let empty = - { Id = SmallGroupId Guid.Empty - ChurchId = ChurchId Guid.Empty - Name = "" - Preferences = ListPreferences.empty } - - /// The DateTimeZone for the time zone ID for this small group - let timeZone group = - let tzId = TimeZoneId.toString group.Preferences.TimeZoneId - - if DateTimeZoneProviders.Tzdb.Ids.Contains tzId then - DateTimeZoneProviders.Tzdb[tzId] + /// Is an update required for this long-term request? + member this.UpdateRequired asOf group = + if this.IsExpired asOf group then + false else - DateTimeZone.Utc - - /// Get the local date/time for this group - let localTimeNow (clock: IClock) group = - if isNull clock then - nullArg (nameof clock) - - clock.GetCurrentInstant().InZone(timeZone group).LocalDateTime - - /// Get the local date for this group - let localDateNow clock group = (localTimeNow clock group).Date - - -/// Functions to support prayer requests -module PrayerRequest = + asOf.PlusWeeks -group.Preferences.LongTermUpdateWeeks + >= this.UpdatedDate.InZone(group.TimeZone).Date /// An empty request - let empty = + static member Empty = { Id = PrayerRequestId Guid.Empty RequestType = CurrentRequest UserId = UserId Guid.Empty @@ -496,28 +498,6 @@ module PrayerRequest = NotifyChaplain = false Expiration = Automatic } - /// Is this request expired? - let isExpired (asOf: LocalDate) group req = - match req.Expiration, req.RequestType with - | Forced, _ -> true - | Manual, _ - | Automatic, LongTermRequest - | Automatic, Expecting -> false - | Automatic, _ -> - // Automatic expiration - Period - .Between(req.UpdatedDate.InZone(SmallGroup.timeZone group).Date, asOf, PeriodUnits.Days) - .Days - >= group.Preferences.DaysToExpire - - /// Is an update required for this long-term request? - let updateRequired asOf group req = - if isExpired asOf group req then - false - else - asOf.PlusWeeks -group.Preferences.LongTermUpdateWeeks - >= req.UpdatedDate.InZone(SmallGroup.timeZone group).Date - /// This represents a user of PrayerTracker [] @@ -548,11 +528,8 @@ type User = /// The full name of the user member this.Name = $"{this.FirstName} {this.LastName}" -/// Functions to support users -module User = - /// An empty user - let empty = + static member Empty = { Id = UserId Guid.Empty FirstName = "" LastName = "" @@ -573,10 +550,7 @@ type UserSmallGroup = SmallGroupId: SmallGroupId } -/// Functions to support user/small group cross-reference -module UserSmallGroup = - /// An empty user/small group xref - let empty = + static member Empty = { UserId = UserId Guid.Empty SmallGroupId = SmallGroupId Guid.Empty } diff --git a/src/PrayerTracker.Tests/Data/EntitiesTests.fs b/src/PrayerTracker.Tests/Data/EntitiesTests.fs index 8e8e045..2efcaa1 100644 --- a/src/PrayerTracker.Tests/Data/EntitiesTests.fs +++ b/src/PrayerTracker.Tests/Data/EntitiesTests.fs @@ -39,8 +39,8 @@ let asOfDateDisplayTests = [] let churchTests = testList "Church" [ - test "empty is as expected" { - let mt = Church.empty + test "Empty is as expected" { + let mt = Church.Empty Expect.equal mt.Id.Value Guid.Empty "The church ID should have been an empty GUID" Expect.equal mt.Name "" "The name should have been blank" Expect.equal mt.City "" "The city should have been blank" @@ -111,16 +111,16 @@ let expirationTests = let listPreferencesTests = testList "ListPreferences" [ test "FontStack is correct for native fonts" { - Expect.equal ListPreferences.empty.FontStack + Expect.equal ListPreferences.Empty.FontStack """system-ui,-apple-system,"Segoe UI",Roboto,Ubuntu,"Liberation Sans",Cantarell,"Helvetica Neue",sans-serif""" "The expected native font stack was incorrect" } test "FontStack is correct for specific fonts" { - Expect.equal { ListPreferences.empty with Fonts = "Arial,sans-serif" }.FontStack "Arial,sans-serif" + Expect.equal { ListPreferences.Empty with Fonts = "Arial,sans-serif" }.FontStack "Arial,sans-serif" "The specified fonts were not returned correctly" } - test "empty is as expected" { - let mt = ListPreferences.empty + test "Empty is as expected" { + let mt = ListPreferences.Empty Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID" Expect.equal mt.DaysToExpire 14 "The default days to expire should have been 14" Expect.equal mt.DaysToKeepNew 7 "The default days to keep new should have been 7" @@ -137,8 +137,7 @@ let listPreferencesTests = Expect.equal mt.GroupPassword "" "The default group password should have been blank" Expect.equal mt.DefaultEmailType HtmlFormat "The default e-mail type should have been HTML" Expect.isFalse mt.IsPublic "The isPublic flag should not have been set" - Expect.equal (TimeZoneId.toString mt.TimeZoneId) "America/Denver" - "The default time zone should have been America/Denver" + Expect.equal (string 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.AsOfDateDisplay NoDisplay "The as-of date display should have been No Display" } @@ -147,8 +146,8 @@ let listPreferencesTests = [] let memberTests = testList "Member" [ - test "empty is as expected" { - let mt = Member.empty + test "Empty is as expected" { + let mt = Member.Empty Expect.equal mt.Id.Value Guid.Empty "The member ID should have been an empty GUID" Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID" Expect.equal mt.Name "" "The member name should have been blank" @@ -162,8 +161,8 @@ let prayerRequestTests = let instantNow = SystemClock.Instance.GetCurrentInstant let localDateNow () = (instantNow ()).InUtc().Date testList "PrayerRequest" [ - test "empty is as expected" { - let mt = PrayerRequest.empty + test "Empty is as expected" { + let mt = PrayerRequest.Empty Expect.equal mt.Id.Value Guid.Empty "The request ID should have been an empty GUID" Expect.equal mt.RequestType CurrentRequest "The request type should have been Current" Expect.equal mt.UserId.Value Guid.Empty "The user ID should have been an empty GUID" @@ -175,59 +174,60 @@ let prayerRequestTests = Expect.isFalse mt.NotifyChaplain "The notify chaplain flag should not have been set" Expect.equal mt.Expiration Automatic "The expiration should have been Automatic" } - test "isExpired always returns false for expecting requests" { - PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty - { PrayerRequest.empty with RequestType = Expecting } + test "IsExpired always returns false for expecting requests" { + { PrayerRequest.Empty with RequestType = Expecting }.IsExpired (localDateNow ()) SmallGroup.Empty |> Flip.Expect.isFalse "An expecting request should never be considered expired" } - test "isExpired always returns false for manually-expired requests" { - PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty - { PrayerRequest.empty with UpdatedDate = (instantNow ()) - Duration.FromDays 1; Expiration = Manual } + test "IsExpired always returns false for manually-expired requests" { + { PrayerRequest.Empty with + UpdatedDate = (instantNow ()) - Duration.FromDays 1 + Expiration = Manual }.IsExpired (localDateNow ()) SmallGroup.Empty |> Flip.Expect.isFalse "A never-expired request should never be considered expired" } - test "isExpired always returns false for long term/recurring requests" { - PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty - { PrayerRequest.empty with RequestType = LongTermRequest } + test "IsExpired always returns false for long term/recurring requests" { + { PrayerRequest.Empty with RequestType = LongTermRequest }.IsExpired (localDateNow ()) SmallGroup.Empty |> Flip.Expect.isFalse "A recurring/long-term request should never be considered expired" } - test "isExpired always returns true for force-expired requests" { - PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty - { PrayerRequest.empty with UpdatedDate = (instantNow ()); Expiration = Forced } + test "IsExpired always returns true for force-expired requests" { + { PrayerRequest.Empty with UpdatedDate = (instantNow ()); Expiration = Forced }.IsExpired + (localDateNow ()) SmallGroup.Empty |> Flip.Expect.isTrue "A force-expired request should always be considered expired" } - test "isExpired returns false for non-expired requests" { + test "IsExpired returns false for non-expired requests" { let now = instantNow () - PrayerRequest.isExpired (now.InUtc().Date) SmallGroup.empty - { PrayerRequest.empty with UpdatedDate = now - Duration.FromDays 5 } + { PrayerRequest.Empty with UpdatedDate = now - Duration.FromDays 5 }.IsExpired + (now.InUtc().Date) SmallGroup.Empty |> Flip.Expect.isFalse "A request updated 5 days ago should not be considered expired" } - test "isExpired returns true for expired requests" { + test "IsExpired returns true for expired requests" { let now = instantNow () - PrayerRequest.isExpired (now.InUtc().Date) SmallGroup.empty - { PrayerRequest.empty with UpdatedDate = now - Duration.FromDays 15 } + { PrayerRequest.Empty with UpdatedDate = now - Duration.FromDays 15 }.IsExpired + (now.InUtc().Date) SmallGroup.Empty |> Flip.Expect.isTrue "A request updated 15 days ago should be considered expired" } - test "isExpired returns true for same-day expired requests" { + test "IsExpired returns true for same-day expired requests" { let now = instantNow () - PrayerRequest.isExpired (now.InUtc().Date) SmallGroup.empty - { PrayerRequest.empty with UpdatedDate = now - (Duration.FromDays 14) - (Duration.FromSeconds 1L) } + { PrayerRequest.Empty with + UpdatedDate = now - (Duration.FromDays 14) - (Duration.FromSeconds 1L) }.IsExpired + (now.InUtc().Date) SmallGroup.Empty |> Flip.Expect.isTrue "A request entered a second before midnight should be considered expired" } - test "updateRequired returns false for expired requests" { - PrayerRequest.updateRequired (localDateNow ()) SmallGroup.empty - { PrayerRequest.empty with Expiration = Forced } + test "UpdateRequired returns false for expired requests" { + { PrayerRequest.Empty with Expiration = Forced }.UpdateRequired (localDateNow ()) SmallGroup.Empty |> Flip.Expect.isFalse "An expired request should not require an update" } - test "updateRequired returns false when an update is not required for an active request" { + test "UpdateRequired returns false when an update is not required for an active request" { let now = instantNow () - PrayerRequest.updateRequired (localDateNow ()) SmallGroup.empty - { PrayerRequest.empty with RequestType = LongTermRequest; UpdatedDate = now - Duration.FromDays 14 } + { PrayerRequest.Empty with + RequestType = LongTermRequest + UpdatedDate = now - Duration.FromDays 14 }.UpdateRequired (localDateNow ()) SmallGroup.Empty |> Flip.Expect.isFalse "An active request updated 14 days ago should not require an update until 28 days" } test "UpdateRequired returns true when an update is required for an active request" { let now = instantNow () - PrayerRequest.updateRequired (localDateNow ()) SmallGroup.empty - { PrayerRequest.empty with RequestType = LongTermRequest; UpdatedDate = now - Duration.FromDays 34 } + { PrayerRequest.Empty with + RequestType = LongTermRequest + UpdatedDate = now - Duration.FromDays 34 }.UpdateRequired (localDateNow ()) SmallGroup.Empty |> Flip.Expect.isTrue "An active request updated 34 days ago should require an update (past 28 days)" } ] @@ -311,8 +311,8 @@ let smallGroupTests = let now = Instant.FromDateTimeUtc (DateTime (2017, 5, 12, 12, 15, 0, DateTimeKind.Utc)) let withFakeClock f () = FakeClock now |> f - yield test "empty is as expected" { - let mt = SmallGroup.empty + yield test "Empty is as expected" { + let mt = SmallGroup.Empty Expect.equal mt.Id.Value Guid.Empty "The small group ID should have been an empty GUID" Expect.equal mt.ChurchId.Value Guid.Empty "The church ID should have been an empty GUID" Expect.equal mt.Name "" "The name should have been blank" @@ -321,31 +321,31 @@ let smallGroupTests = "LocalTimeNow adjusts the time ahead of UTC", fun clock -> let grp = - { SmallGroup.empty with - Preferences = { ListPreferences.empty with TimeZoneId = TimeZoneId "Europe/Berlin" } + { SmallGroup.Empty with + Preferences = { ListPreferences.Empty with TimeZoneId = TimeZoneId "Europe/Berlin" } } - Expect.isGreaterThan (SmallGroup.localTimeNow clock grp) (now.InUtc().LocalDateTime) + Expect.isGreaterThan (grp.LocalTimeNow clock) (now.InUtc().LocalDateTime) "UTC to Europe/Berlin should have added hours" "LocalTimeNow adjusts the time behind UTC", fun clock -> - Expect.isLessThan (SmallGroup.localTimeNow clock SmallGroup.empty) (now.InUtc().LocalDateTime) + Expect.isLessThan (SmallGroup.Empty.LocalTimeNow clock) (now.InUtc().LocalDateTime) "UTC to America/Denver should have subtracted hours" "LocalTimeNow returns UTC when the time zone is invalid", fun clock -> let grp = - { SmallGroup.empty with - Preferences = { ListPreferences.empty with TimeZoneId = TimeZoneId "garbage" } + { SmallGroup.Empty with + Preferences = { ListPreferences.Empty with TimeZoneId = TimeZoneId "garbage" } } - Expect.equal (SmallGroup.localTimeNow clock grp) (now.InUtc().LocalDateTime) + Expect.equal (grp.LocalTimeNow clock) (now.InUtc().LocalDateTime) "UTC should have been returned for an invalid time zone" ] yield test "localTimeNow fails when clock is not passed" { - Expect.throws (fun () -> (SmallGroup.localTimeNow null SmallGroup.empty |> ignore)) + Expect.throws (fun () -> SmallGroup.Empty.LocalTimeNow null |> ignore) "Should have raised an exception for null clock" } yield test "LocalDateNow returns the date portion" { let clock = FakeClock (Instant.FromDateTimeUtc (DateTime (2017, 5, 12, 1, 15, 0, DateTimeKind.Utc))) - Expect.isLessThan (SmallGroup.localDateNow clock SmallGroup.empty) (now.InUtc().Date) + Expect.isLessThan (SmallGroup.Empty.LocalDateNow clock) (now.InUtc().Date) "The date should have been a day earlier" } ] @@ -353,8 +353,8 @@ let smallGroupTests = [] let userTests = testList "User" [ - test "empty is as expected" { - let mt = User.empty + test "Empty is as expected" { + let mt = User.Empty Expect.equal mt.Id.Value Guid.Empty "The user ID should have been an empty GUID" Expect.equal mt.FirstName "" "The first name should have been blank" Expect.equal mt.LastName "" "The last name should have been blank" @@ -363,7 +363,7 @@ let userTests = Expect.equal mt.PasswordHash "" "The password hash should have been blank" } test "Name 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.Name "Unit Test" "The full name should be the first and last, separated by a space" } ] @@ -371,8 +371,8 @@ let userTests = [] let userSmallGroupTests = testList "UserSmallGroup" [ - test "empty is as expected" { - let mt = UserSmallGroup.empty + test "Empty is as expected" { + let mt = UserSmallGroup.Empty Expect.equal mt.UserId.Value Guid.Empty "The user ID should have been an empty GUID" Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID" } diff --git a/src/PrayerTracker.Tests/UI/ViewModelsTests.fs b/src/PrayerTracker.Tests/UI/ViewModelsTests.fs index 03fb399..8f06e47 100644 --- a/src/PrayerTracker.Tests/UI/ViewModelsTests.fs +++ b/src/PrayerTracker.Tests/UI/ViewModelsTests.fs @@ -15,7 +15,7 @@ let countAll _ = true module ReferenceListTests = - + [] let asOfDateListTests = testList "ReferenceList.asOfDateList" [ @@ -43,7 +43,7 @@ module ReferenceListTests = Expect.equal (fst lst) (string PlainTextFormat) "The 3rd option should have been plain text" } ] - + [] let expirationListTests = testList "ReferenceList.expirationList" [ @@ -66,7 +66,7 @@ module ReferenceListTests = "The option for immediate expiration was not found" } ] - + [] let requestTypeListTests = testList "ReferenceList.requestTypeList" [ @@ -129,7 +129,7 @@ let appViewInfoTests = let assignGroupsTests = testList "AssignGroups" [ test "fromUser populates correctly" { - let usr = { User.empty with Id = (Guid.NewGuid >> UserId) (); FirstName = "Alice"; LastName = "Bob" } + let usr = { User.Empty with Id = (Guid.NewGuid >> UserId) (); FirstName = "Alice"; LastName = "Bob" } let asg = AssignGroups.fromUser usr Expect.equal asg.UserId (shortGuid usr.Id.Value) "The user ID was not filled correctly" Expect.equal asg.UserName usr.Name "The user's name was not filled correctly" @@ -142,7 +142,7 @@ let editChurchTests = testList "EditChurch" [ test "fromChurch populates correctly when interface exists" { let church = - { Church.empty with + { Church.Empty with Id = (Guid.NewGuid >> ChurchId) () Name = "Unit Test" City = "Testlandia" @@ -163,7 +163,7 @@ let editChurchTests = test "fromChurch populates correctly when interface does not exist" { let edit = EditChurch.fromChurch - { Church.empty with + { Church.Empty with Id = (Guid.NewGuid >> ChurchId) () Name = "Unit Test" City = "Testlandia" @@ -198,7 +198,7 @@ let editChurchTests = HasInterface = Some true InterfaceAddress = Some "https://test.units" } - let church = edit.PopulateChurch Church.empty + let church = edit.PopulateChurch Church.Empty Expect.notEqual (shortGuid church.Id.Value) edit.ChurchId "The church ID should not have been modified" Expect.equal church.Name edit.Name "The church name was not updated correctly" Expect.equal church.City edit.City "The church's city was not updated correctly" @@ -213,7 +213,7 @@ let editChurchTests = Name = "Test Baptist Church" City = "Testerville" State = "TE" - }.PopulateChurch Church.empty + }.PopulateChurch Church.Empty Expect.isFalse church.HasVpsInterface "The church should show that it has an interface" Expect.isNone church.InterfaceAddress "The interface address should exist" } @@ -224,7 +224,7 @@ let editMemberTests = testList "EditMember" [ test "fromMember populates with group default format" { let mbr = - { Member.empty with + { Member.Empty with Id = (Guid.NewGuid >> MemberId) () Name = "Test Name" Email = "test_units@example.com" @@ -236,7 +236,7 @@ let editMemberTests = Expect.equal edit.Format "" "The e-mail format should have been blank for group default" } test "fromMember populates with specific format" { - let edit = EditMember.fromMember { Member.empty with Format = Some HtmlFormat } + let edit = EditMember.fromMember { Member.Empty with Format = Some HtmlFormat } Expect.equal edit.Format (string HtmlFormat) "The e-mail format was not filled correctly" } test "empty is as expected" { @@ -259,7 +259,7 @@ let editMemberTests = let editPreferencesTests = testList "EditPreferences" [ test "fromPreferences succeeds for native fonts, named colors, and private list" { - let prefs = ListPreferences.empty + let prefs = ListPreferences.Empty let edit = EditPreferences.fromPreferences prefs Expect.equal edit.ExpireDays prefs.DaysToExpire "The expiration days were not filled correctly" Expect.equal edit.DaysToKeepNew prefs.DaysToKeepNew "The days to keep new were not filled correctly" @@ -278,7 +278,7 @@ let editPreferencesTests = Expect.isNone edit.Fonts "The list fonts should not exist for native font stack" Expect.equal edit.HeadingFontSize prefs.HeadingFontSize "The heading font size was not filled correctly" Expect.equal edit.ListFontSize prefs.TextFontSize "The list text font size was not filled correctly" - Expect.equal edit.TimeZone (TimeZoneId.toString prefs.TimeZoneId) "The time zone was not filled correctly" + Expect.equal edit.TimeZone (string prefs.TimeZoneId) "The time zone was not filled correctly" Expect.isSome edit.GroupPassword "The group password should have been set" Expect.equal edit.GroupPassword (Some prefs.GroupPassword) "The group password was not filled correctly" Expect.equal edit.Visibility GroupVisibility.PrivateList @@ -287,7 +287,7 @@ let editPreferencesTests = Expect.equal edit.AsOfDate (string prefs.AsOfDateDisplay) "The as-of date display was not filled correctly" } test "fromPreferences succeeds for RGB line color and password-protected list" { - let prefs = { ListPreferences.empty with LineColor = "#ff0000"; GroupPassword = "pw" } + let prefs = { ListPreferences.Empty with LineColor = "#ff0000"; GroupPassword = "pw" } let edit = EditPreferences.fromPreferences prefs Expect.equal edit.LineColorType "RGB" "The heading line color type was not derived correctly" Expect.equal edit.LineColor prefs.LineColor "The heading line color was not filled correctly" @@ -297,7 +297,7 @@ let editPreferencesTests = "The list visibility was not derived correctly" } test "fromPreferences succeeds for RGB text color and public list" { - let prefs = { ListPreferences.empty with HeadingColor = "#0000ff"; IsPublic = true } + let prefs = { ListPreferences.Empty with HeadingColor = "#0000ff"; IsPublic = true } let edit = EditPreferences.fromPreferences prefs Expect.equal edit.HeadingColorType "RGB" "The heading text color type was not derived correctly" Expect.equal edit.HeadingColor prefs.HeadingColor "The heading text color was not filled correctly" @@ -307,7 +307,7 @@ let editPreferencesTests = "The list visibility was not derived correctly" } test "fromPreferences succeeds for non-native fonts" { - let prefs = { ListPreferences.empty with Fonts = "Arial,sans-serif" } + let prefs = { ListPreferences.Empty with Fonts = "Arial,sans-serif" } let edit = EditPreferences.fromPreferences prefs Expect.isFalse edit.IsNative "The IsNative flag should have been false" Expect.isSome edit.Fonts "The fonts should have been filled for non-native fonts" @@ -330,7 +330,7 @@ let editRequestTests = } test "fromRequest succeeds" { let req = - { PrayerRequest.empty with + { PrayerRequest.Empty with Id = (Guid.NewGuid >> PrayerRequestId) () RequestType = CurrentRequest Requestor = Some "Me" @@ -358,7 +358,7 @@ let editSmallGroupTests = testList "EditSmallGroup" [ test "fromGroup succeeds" { let grp = - { SmallGroup.empty with + { SmallGroup.Empty with Id = (Guid.NewGuid >> SmallGroupId) () Name = "test group" ChurchId = (Guid.NewGuid >> ChurchId) () @@ -387,7 +387,7 @@ let editSmallGroupTests = Name = "test name" ChurchId = (Guid.NewGuid >> shortGuid) () } - let grp = edit.populateGroup SmallGroup.empty + let grp = edit.populateGroup SmallGroup.Empty Expect.equal grp.Name edit.Name "The name was not populated correctly" Expect.equal grp.ChurchId (idFromShort ChurchId edit.ChurchId) "The church ID was not populated correctly" } @@ -408,7 +408,7 @@ let editUserTests = } test "fromUser succeeds" { let usr = - { User.empty with + { User.Empty with Id = (Guid.NewGuid >> UserId) () FirstName = "user" LastName = "test" @@ -438,7 +438,7 @@ let editUserTests = Password = "testpw" } let hasher = fun x -> x + "+" - let usr = edit.PopulateUser User.empty hasher + let usr = edit.PopulateUser User.Empty hasher Expect.equal usr.FirstName edit.FirstName "The first name was not populated correctly" Expect.equal usr.LastName edit.LastName "The last name was not populated correctly" Expect.equal usr.Email edit.Email "The e-mail address was not populated correctly" @@ -500,26 +500,26 @@ let requestListTests = let withRequestList f () = let today = SystemClock.Instance.GetCurrentInstant () { Requests = [ - { PrayerRequest.empty with + { PrayerRequest.Empty with RequestType = CurrentRequest Requestor = Some "Zeb" Text = "zyx" UpdatedDate = today } - { PrayerRequest.empty with + { PrayerRequest.Empty with RequestType = CurrentRequest Requestor = Some "Aaron" Text = "abc" UpdatedDate = today - Duration.FromDays 9 } - { PrayerRequest.empty with + { PrayerRequest.Empty with RequestType = PraiseReport Text = "nmo" UpdatedDate = today } ] Date = today.InUtc().Date - SmallGroup = SmallGroup.empty + SmallGroup = SmallGroup.Empty ShowHeader = false Recipients = [] CanEmail = false @@ -596,10 +596,10 @@ let requestListTests = } let html = htmlList.AsHtml _s let expected = - htmlList.Requests[0].UpdatedDate.InZone(SmallGroup.timeZone reqList.SmallGroup).Date.ToString ("d", null) + htmlList.Requests[0].UpdatedDate.InZone(reqList.SmallGroup.TimeZone).Date.ToString ("d", null) |> sprintf """Zeb – zyx  (as of %s)""" // spot check; if one request has it, they all should - Expect.stringContains html expected "Expected short as-of date not found" + Expect.stringContains html expected "Expected short as-of date not found" "AsHtml succeeds with long as-of date", fun reqList -> let htmlList = @@ -611,10 +611,10 @@ let requestListTests = } let html = htmlList.AsHtml _s let expected = - htmlList.Requests[0].UpdatedDate.InZone(SmallGroup.timeZone reqList.SmallGroup).Date.ToString ("D", null) + htmlList.Requests[0].UpdatedDate.InZone(reqList.SmallGroup.TimeZone).Date.ToString ("D", null) |> sprintf """Zeb – zyx  (as of %s)""" // spot check; if one request has it, they all should - Expect.stringContains html expected "Expected long as-of date not found" + Expect.stringContains html expected "Expected long as-of date not found" "AsText succeeds with no as-of date", fun reqList -> let textList = { reqList with SmallGroup = { reqList.SmallGroup with Name = "Test Group" } } @@ -642,10 +642,10 @@ let requestListTests = } let text = textList.AsText _s let expected = - textList.Requests[0].UpdatedDate.InZone(SmallGroup.timeZone reqList.SmallGroup).Date.ToString ("d", null) + textList.Requests[0].UpdatedDate.InZone(reqList.SmallGroup.TimeZone).Date.ToString ("d", null) |> sprintf " + Zeb - zyx (as of %s)" // spot check; if one request has it, they all should - Expect.stringContains text expected "Expected short as-of date not found" + Expect.stringContains text expected "Expected short as-of date not found" "AsText succeeds with long as-of date", fun reqList -> let textList = @@ -657,10 +657,10 @@ let requestListTests = } let text = textList.AsText _s let expected = - textList.Requests[0].UpdatedDate.InZone(SmallGroup.timeZone reqList.SmallGroup).Date.ToString ("D", null) + textList.Requests[0].UpdatedDate.InZone(reqList.SmallGroup.TimeZone).Date.ToString ("D", null) |> sprintf " + Zeb - zyx (as of %s)" // spot check; if one request has it, they all should - Expect.stringContains text expected "Expected long as-of date not found" + Expect.stringContains text expected "Expected long as-of date not found" "IsNew succeeds for both old and new requests", fun reqList -> let allReqs = reqList.RequestsByType _s diff --git a/src/PrayerTracker.UI/CommonFunctions.fs b/src/PrayerTracker.UI/CommonFunctions.fs index 1c85fbc..cbb62b3 100644 --- a/src/PrayerTracker.UI/CommonFunctions.fs +++ b/src/PrayerTracker.UI/CommonFunctions.fs @@ -51,7 +51,7 @@ let tableSummary itemCount (s: IStringLocalizer) = |> locStr ] ] - + /// Generate a list of named HTML colors let namedColorList name selected attrs (s: IStringLocalizer) = // The list of HTML named colors (name, display, text color) @@ -104,7 +104,7 @@ let colorToHex (color: string) = | "white" -> "#ffffff" | "yellow" -> "#ffff00" | it -> it - + /// Generate an input type=radio that is selected if its value is the current value let radio name domId value current = input [ _type "radio" @@ -197,7 +197,7 @@ let renderHtmlString = renderHtmlNode >> HtmlString /// Utility methods to help with time zones (and localization of their names) module TimeZones = - + open PrayerTracker.Entities /// Cross-reference between time zone Ids and their English names @@ -215,9 +215,9 @@ module TimeZones = match xref |> List.tryFind (fun it -> fst it = timeZoneId) with | Some tz -> s[snd tz] | None -> - let tzId = TimeZoneId.toString timeZoneId + let tzId = string timeZoneId LocalizedString (tzId, tzId) - + /// All known time zones in their defined order let all = xref |> List.map fst @@ -226,9 +226,9 @@ open Giraffe.ViewEngine.Htmx /// Known htmx targets module Target = - + /// htmx links target the body element let body = _hxTarget "body" - + /// htmx links target the #pt-body element let content = _hxTarget "#pt-body" diff --git a/src/PrayerTracker.UI/PrayerRequest.fs b/src/PrayerTracker.UI/PrayerRequest.fs index 440486e..767ef37 100644 --- a/src/PrayerTracker.UI/PrayerRequest.fs +++ b/src/PrayerTracker.UI/PrayerRequest.fs @@ -98,7 +98,7 @@ let email model viewInfo = /// View for a small group's public prayer request list let list (model : RequestList) viewInfo = [ br [] - I18N.localizer.Force () |> (model.AsHtml >> rawText) + I18N.localizer.Force () |> (model.AsHtml >> rawText) ] |> Layout.Content.standard |> Layout.standard viewInfo "View Request List" @@ -156,7 +156,7 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo = use sw = new StringWriter () let raw = rawLocText sw let group = model.SmallGroup - let now = SmallGroup.localDateNow (ctx.GetService ()) group + let now = group.LocalDateNow (ctx.GetService()) let types = ReferenceList.requestTypeList s |> Map.ofList let vi = AppViewInfo.withScopedStyles [ "#requestList { grid-template-columns: repeat(5, auto); }" ] viewInfo /// Iterate the sequence once, before we render, so we can get the count of it at the top of the table @@ -164,8 +164,8 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo = model.Requests |> List.map (fun req -> let updateClass = - _class (if PrayerRequest.updateRequired now group req then "cell pt-request-update" else "cell") - let isExpired = PrayerRequest.isExpired now group req + _class (if req.UpdateRequired now group then "cell pt-request-update" else "cell") + let isExpired = req.IsExpired now group let expiredClass = _class (if isExpired then "cell pt-request-expired" else "cell") let reqId = shortGuid req.Id.Value let reqText = htmlToPlainText req.Text diff --git a/src/PrayerTracker.UI/SmallGroup.fs b/src/PrayerTracker.UI/SmallGroup.fs index 70d0947..a09c54b 100644 --- a/src/PrayerTracker.UI/SmallGroup.fs +++ b/src/PrayerTracker.UI/SmallGroup.fs @@ -99,7 +99,7 @@ let edit (model : EditSmallGroup) (churches : Church list) ctx viewInfo = "", selectDefault s["Select Church"].Value yield! churches |> List.map (fun c -> shortGuid c.Id.Value, c.Name) } - |> selectList (nameof model.ChurchId) model.ChurchId [ _required ] + |> selectList (nameof model.ChurchId) model.ChurchId [ _required ] ] ] div [ _fieldRow ] [ submit [] "save" s["Save Group"] ] @@ -476,7 +476,7 @@ let preferences (model : EditPreferences) ctx viewInfo = locStr s["Custom Color"] ] space - input [ _type "color" + input [ _type "color" _name (nameof model.LineColor) _id $"{nameof model.LineColor}_Color" _value (colorToHex model.LineColor) @@ -589,7 +589,7 @@ let preferences (model : EditPreferences) ctx viewInfo = "", selectDefault s["Select"].Value yield! TimeZones.all - |> List.map (fun tz -> TimeZoneId.toString tz, (TimeZones.name tz s).Value) + |> List.map (fun tz -> string tz, (TimeZones.name tz s).Value) } |> selectList (nameof model.TimeZone) model.TimeZone [ _required ] ] diff --git a/src/PrayerTracker.UI/ViewModels.fs b/src/PrayerTracker.UI/ViewModels.fs index e847727..9ae1f1a 100644 --- a/src/PrayerTracker.UI/ViewModels.fs +++ b/src/PrayerTracker.UI/ViewModels.fs @@ -54,14 +54,14 @@ type MessageLevel = /// 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() @@ -70,31 +70,31 @@ module MessageLevel = type UserMessage = { /// The type Level : MessageLevel - + /// The actual message Text : HtmlString - + /// The description (further information) Description : HtmlString option } /// Support for the UserMessage type module UserMessage = - + /// Error message template let error = { Level = Error Text = HtmlString.Empty Description = None } - + /// Warning message template let warning = { Level = Warning Text = HtmlString.Empty Description = None } - + /// Info message template let info = { Level = Info @@ -104,13 +104,13 @@ module UserMessage = /// The template with which the content will be rendered type LayoutType = - + /// A full page load | FullPage - - /// A response that will provide a new body tag + + /// A response that will provide a new body tag | PartialPage - + /// A response that will replace the page content | ContentOnly @@ -122,38 +122,38 @@ open NodaTime type AppViewInfo = { /// CSS files for the page Style : string list - + /// The link for help on this page HelpLink : string option - + /// Messages to be displayed to the user Messages : UserMessage list - + /// The current version of PrayerTracker Version : string - + /// The ticks when the request started RequestStart : Instant - + /// The currently logged on user, if there is one User : User option - + /// The currently logged on small group, if there is one Group : SmallGroup option - + /// The layout with which the content will be rendered Layout : LayoutType - + /// Scoped styles for this view ScopedStyle : string list - + /// A JavaScript function to run on page load OnLoadScript : string option } /// Support for the AppViewInfo type module AppViewInfo = - + /// A fresh version that can be populated to process the current request let fresh = { Style = [] @@ -167,11 +167,11 @@ module AppViewInfo = ScopedStyle = [] OnLoadScript = None } - + /// Add scoped styles to the given view info object let withScopedStyles styles viewInfo = { viewInfo with ScopedStyle = styles } - + /// Add an onload action to the given view info object let withOnLoadScript script viewInfo = { viewInfo with OnLoadScript = Some script } @@ -182,18 +182,18 @@ module AppViewInfo = type Announcement = { /// Whether the announcement should be sent to the class or to PrayerTracker users SendToClass : string - + /// The text of the announcement Text : string - + /// Whether this announcement should be added to the "Announcements" of the prayer list AddToRequestList : bool option - + /// The ID of the request type to which this announcement should be added RequestType : string option } with - + /// The text of the announcement, in plain text member this.PlainText with get () = (htmlToPlainText >> wordWrap 74) this.Text @@ -204,17 +204,17 @@ with type AssignGroups = { /// The Id of the user being assigned UserId : string - + /// The full name of the user being assigned UserName : string - + /// The Ids of the small groups to which the user is authorized SmallGroups : string } /// Support for the AssignGroups type module AssignGroups = - + /// Create an instance of this form from an existing user let fromUser (user: User) = { UserId = shortGuid user.Id.Value @@ -228,10 +228,10 @@ module AssignGroups = type ChangePassword = { /// The user's current password OldPassword : string - + /// The user's new password NewPassword : string - + /// The user's new password, confirmed NewPasswordConfirm : string } @@ -242,27 +242,27 @@ type ChangePassword = type EditChurch = { /// The ID of the church ChurchId : string - + /// The name of the church Name : string - + /// The city for the church City : string - + /// The state or province for the church State : string - + /// Whether the church has an active Virtual Prayer Room interface HasInterface : bool option - + /// The address for the interface InterfaceAddress : string option } with - + /// Is this a new church? member this.IsNew = emptyGuid = this.ChurchId - + /// Populate a church from this form member this.PopulateChurch (church: Church) = { church with @@ -275,7 +275,7 @@ with /// Support for the EditChurch type module EditChurch = - + /// Create an instance from an existing church let fromChurch (church: Church) = { ChurchId = shortGuid church.Id.Value @@ -285,7 +285,7 @@ module EditChurch = HasInterface = match church.HasVpsInterface with true -> Some true | false -> None InterfaceAddress = church.InterfaceAddress } - + /// An instance to use for adding churches let empty = { ChurchId = emptyGuid @@ -296,30 +296,30 @@ module EditChurch = InterfaceAddress = None } - + /// Form for adding/editing small group members [] type EditMember = { /// The Id for this small group member (not user-entered) MemberId : string - + /// The name of the member Name : string - + /// The e-mail address Email : string - + /// The e-mail format Format : string } with - + /// Is this a new member? member this.IsNew = emptyGuid = this.MemberId /// Support for the EditMember type module EditMember = - + /// Create an instance from an existing member let fromMember (mbr: Member) = { MemberId = shortGuid mbr.Id.Value @@ -327,7 +327,7 @@ module EditMember = Email = mbr.Email Format = mbr.Format |> Option.map string |> Option.defaultValue "" } - + /// An empty instance let empty = { MemberId = emptyGuid @@ -342,66 +342,66 @@ module EditMember = type EditPreferences = { /// The number of days after which requests are automatically expired ExpireDays : int - + /// The number of days requests are considered "new" DaysToKeepNew : int - + /// The number of weeks after which a long-term requests is flagged as requiring an update LongTermUpdateWeeks : int - + /// Whether to sort by updated date or requestor/subject RequestSort : string - + /// The name from which e-mail will be sent EmailFromName : string - + /// The e-mail address from which e-mail will be sent EmailFromAddress : string - + /// The default e-mail type for this group DefaultEmailType : string - + /// Whether the heading line color uses named colors or R/G/B LineColorType : string - + /// The named color for the heading lines LineColor : string - + /// Whether the heading text color uses named colors or R/G/B HeadingColorType : string - + /// The named color for the heading text HeadingColor : string - + /// Whether the class uses the native font stack IsNative : bool - + /// The fonts to use for the list Fonts : string option - + /// The font size for the heading text HeadingFontSize : int - + /// The font size for the list text ListFontSize : int - + /// The time zone for the class TimeZone : string - + /// The list visibility Visibility : int - + /// The small group password GroupPassword : string option - + /// The page size for search / inactive requests PageSize : int - + /// How the as-of date should be displayed AsOfDate : string } with - + /// Set the properties of a small group based on the form's properties member this.PopulatePreferences (prefs: ListPreferences) = let isPublic, grpPw = @@ -448,7 +448,7 @@ module EditPreferences = Fonts = if prefs.Fonts = "native" then None else Some prefs.Fonts HeadingFontSize = prefs.HeadingFontSize ListFontSize = prefs.TextFontSize - TimeZone = TimeZoneId.toString prefs.TimeZoneId + TimeZone = string prefs.TimeZoneId GroupPassword = Some prefs.GroupPassword PageSize = prefs.PageSize AsOfDate = string prefs.AsOfDateDisplay @@ -464,33 +464,33 @@ module EditPreferences = type EditRequest = { /// The ID of the request RequestId : string - + /// The type of the request RequestType : string - + /// The date of the request EnteredDate : string option - + /// Whether to update the date or not SkipDateUpdate : bool option - + /// The requestor or subject Requestor : string option - + /// How this request is expired Expiration : string - + /// The text of the request Text : string } with - + /// Is this a new request? member this.IsNew = emptyGuid = this.RequestId /// Support for the EditRequest type module EditRequest = - + /// An empty instance to use for new requests let empty = { RequestId = emptyGuid @@ -501,7 +501,7 @@ module EditRequest = Expiration = string Automatic Text = "" } - + /// Create an instance from an existing request let fromRequest (req: PrayerRequest) = { empty with @@ -518,18 +518,18 @@ module EditRequest = type EditSmallGroup = { /// The ID of the small group SmallGroupId : string - + /// The name of the small group Name : string - + /// The ID of the church to which this small group belongs ChurchId : string } with - + /// Is this a new small group? member this.IsNew = emptyGuid = this.SmallGroupId - + /// Populate a small group from this form member this.populateGroup (grp: SmallGroup) = { grp with @@ -539,14 +539,14 @@ with /// Support for the EditSmallGroup type module EditSmallGroup = - + /// Create an instance from an existing small group let fromGroup (grp: SmallGroup) = { SmallGroupId = shortGuid grp.Id.Value Name = grp.Name ChurchId = shortGuid grp.ChurchId.Value } - + /// An empty instance (used when adding a new group) let empty = { SmallGroupId = emptyGuid @@ -560,30 +560,30 @@ module EditSmallGroup = type EditUser = { /// The ID of the user UserId : string - + /// The first name of the user FirstName : string - + /// The last name of the user LastName : string - + /// The e-mail address for the user Email : string - + /// The password for the user Password : string - + /// The password hash for the user a second time PasswordConfirm : string - + /// Is this user a PrayerTracker administrator? IsAdmin : bool option } with - + /// Is this a new user? member this.IsNew = emptyGuid = this.UserId - + /// Populate a user from the form member this.PopulateUser (user: User) hasher = { user with @@ -598,7 +598,7 @@ with /// Support for the EditUser type module EditUser = - + /// An empty instance let empty = { UserId = emptyGuid @@ -609,7 +609,7 @@ module EditUser = PasswordConfirm = "" IsAdmin = None } - + /// Create an instance from an existing user let fromUser (user: User) = { empty with @@ -626,17 +626,17 @@ module EditUser = type GroupLogOn = { /// The ID of the small group to which the user is logging on SmallGroupId : string - + /// The password entered Password : string - + /// Whether to remember the login RememberMe : bool option } /// Support for the GroupLogOn type module GroupLogOn = - + /// An empty instance let empty = { SmallGroupId = emptyGuid @@ -650,27 +650,27 @@ module GroupLogOn = type MaintainRequests = { /// The requests to be displayed Requests : PrayerRequest list - + /// The small group to which the requests belong SmallGroup : SmallGroup - + /// Whether only active requests are included OnlyActive : bool option - + /// The search term for the requests SearchTerm : string option - + /// The page number of the results PageNbr : int option } /// Support for the MaintainRequests type module MaintainRequests = - + /// An empty instance let empty = { Requests = [] - SmallGroup = SmallGroup.empty + SmallGroup = SmallGroup.Empty OnlyActive = None SearchTerm = None PageNbr = None @@ -682,16 +682,16 @@ module MaintainRequests = type Overview = { /// The total number of active requests TotalActiveReqs : int - + /// The numbers of active requests by request type ActiveReqsByType : Map - + /// A count of all requests AllReqs : int - + /// A count of all members TotalMembers : int - + /// The users authorized to administer this group Admins : User list } @@ -702,23 +702,23 @@ type Overview = type UserLogOn = { /// The e-mail address of the user Email : string - + /// The password entered Password : string - + /// The ID of the small group to which the user is logging on SmallGroupId : string - + /// Whether to remember the login RememberMe : bool option - + /// The URL to which the user should be redirected once login is successful RedirectUrl : string option } /// Support for the UserLogOn type module UserLogOn = - + /// An empty instance let empty = { Email = "" @@ -736,19 +736,19 @@ open Giraffe.ViewEngine type RequestList = { /// The prayer request list Requests : PrayerRequest list - + /// The date for which this list is being generated Date : LocalDate - + /// The small group to which this list belongs SmallGroup : SmallGroup - + /// Whether to show the class header ShowHeader : bool - + /// The list of recipients (populated if requests are e-mailed) Recipients : Member list - + /// Whether the user can e-mail this list CanEmail : bool } @@ -770,12 +770,12 @@ with |> List.ofSeq typ, name, reqs) |> List.filter (fun (_, _, reqs) -> not (List.isEmpty reqs)) - + /// Is this request new? member this.IsNew (req: PrayerRequest) = - let reqDate = req.UpdatedDate.InZone(SmallGroup.timeZone this.SmallGroup).Date + let reqDate = req.UpdatedDate.InZone(this.SmallGroup.TimeZone).Date Period.Between(reqDate, this.Date, PeriodUnits.Days).Days <= this.SmallGroup.Preferences.DaysToKeepNew - + /// Generate this list as HTML member this.AsHtml (s: IStringLocalizer) = let p = this.SmallGroup.Preferences @@ -803,7 +803,7 @@ with ] ] ] - let tz = SmallGroup.timeZone this.SmallGroup + let tz = this.SmallGroup.TimeZone reqs |> List.map (fun req -> let bullet = if this.IsNew req then "circle" else "disc" @@ -835,7 +835,7 @@ with /// Generate this list as plain text member this.AsText (s: IStringLocalizer) = - let tz = SmallGroup.timeZone this.SmallGroup + let tz = this.SmallGroup.TimeZone seq { this.SmallGroup.Name s["Prayer Requests"].Value diff --git a/src/PrayerTracker/Church.fs b/src/PrayerTracker/Church.fs index e3c6978..6296868 100644 --- a/src/PrayerTracker/Church.fs +++ b/src/PrayerTracker/Church.fs @@ -40,7 +40,7 @@ let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> ta |> renderHtml next ctx else match! Churches.tryById (ChurchId churchId) with - | Some church -> + | Some church -> return! viewInfo ctx |> Views.Church.edit (EditChurch.fromChurch church) ctx @@ -63,7 +63,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c match! ctx.TryBindFormAsync () with | Ok model -> let! church = - if model.IsNew then Task.FromResult(Some { Church.empty with Id = (Guid.NewGuid >> ChurchId) () }) + if model.IsNew then Task.FromResult(Some { Church.Empty with Id = (Guid.NewGuid >> ChurchId) () }) else Churches.tryById (idFromShort ChurchId model.ChurchId) match church with | Some ch -> diff --git a/src/PrayerTracker/PrayerRequest.fs b/src/PrayerTracker/PrayerRequest.fs index d48c764..bcc368e 100644 --- a/src/PrayerTracker/PrayerRequest.fs +++ b/src/PrayerTracker/PrayerRequest.fs @@ -20,7 +20,7 @@ let private findRequest (ctx: HttpContext) reqId = task { /// Generate a list of requests for the given date let private generateRequestList (ctx: HttpContext) date = task { let group = ctx.Session.CurrentGroup.Value - let listDate = match date with Some d -> d | None -> SmallGroup.localDateNow ctx.Clock group + let listDate = defaultArg date (group.LocalDateNow ctx.Clock) let! reqs = PrayerRequests.forGroup { SmallGroup = group @@ -50,7 +50,7 @@ open System // GET /prayer-request/[request-id]/edit let edit reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let group = ctx.Session.CurrentGroup.Value - let now = SmallGroup.localDateNow ctx.Clock group + let now = group.LocalDateNow ctx.Clock let requestId = PrayerRequestId reqId if requestId.Value = Guid.Empty then return! @@ -61,7 +61,7 @@ let edit reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { match! findRequest ctx requestId with | Ok req -> let s = ctx.Strings - if PrayerRequest.isExpired now group req then + if req.IsExpired now group then { UserMessage.warning with Text = htmlLocString s["This request is expired."] Description = @@ -139,7 +139,7 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne viewInfo ctx |> Views.PrayerRequest.list { Requests = reqs - Date = SmallGroup.localDateNow ctx.Clock group + Date = group.LocalDateNow ctx.Clock SmallGroup = group ShowHeader = true CanEmail = Option.isSome ctx.User.UserId @@ -226,7 +226,7 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct let group = ctx.Session.CurrentGroup.Value let! req = if model.IsNew then - { PrayerRequest.empty with + { PrayerRequest.Empty with Id = (Guid.NewGuid >> PrayerRequestId) () SmallGroupId = group.Id UserId = ctx.User.UserId.Value @@ -235,7 +235,7 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct else PrayerRequests.tryById (idFromShort PrayerRequestId model.RequestId) match req with | Some pr when pr.SmallGroupId = group.Id -> - let now = SmallGroup.localDateNow ctx.Clock group + let now = group.LocalDateNow ctx.Clock let updated = { pr with RequestType = PrayerRequestType.Parse model.RequestType @@ -247,7 +247,7 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct | it when model.IsNew -> let dt = (defaultArg (parseListDate model.EnteredDate) now) - .AtStartOfDayInZone(SmallGroup.timeZone group) + .AtStartOfDayInZone(group.TimeZone) .ToInstant() { it with EnteredDate = dt; UpdatedDate = dt } | it when defaultArg model.SkipDateUpdate false -> it diff --git a/src/PrayerTracker/SmallGroup.fs b/src/PrayerTracker/SmallGroup.fs index ed365c6..7d69df1 100644 --- a/src/PrayerTracker/SmallGroup.fs +++ b/src/PrayerTracker/SmallGroup.fs @@ -183,7 +183,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c match! ctx.TryBindFormAsync() with | Ok model -> let! tryGroup = - if model.IsNew then Task.FromResult(Some { SmallGroup.empty with Id = (Guid.NewGuid >> SmallGroupId) () }) + if model.IsNew then Task.FromResult(Some { SmallGroup.Empty with Id = (Guid.NewGuid >> SmallGroupId) () }) else SmallGroups.tryById (idFromShort SmallGroupId model.SmallGroupId) match tryGroup with | Some group -> @@ -202,7 +202,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun n let group = ctx.Session.CurrentGroup.Value let! tryMbr = if model.IsNew then - Task.FromResult(Some { Member.empty with Id = (Guid.NewGuid >> MemberId) (); SmallGroupId = group.Id }) + Task.FromResult(Some { Member.Empty with Id = (Guid.NewGuid >> MemberId) (); SmallGroupId = group.Id }) else Members.tryById (idFromShort MemberId model.MemberId) match tryMbr with | Some mbr when mbr.SmallGroupId = group.Id -> @@ -250,7 +250,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> let group = ctx.Session.CurrentGroup.Value let pref = group.Preferences let usr = ctx.Session.CurrentUser.Value - let now = SmallGroup.localTimeNow ctx.Clock group + let now = group.LocalTimeNow ctx.Clock let s = ctx.Strings // Reformat the text to use the class's font stylings let requestText = ckEditorToText model.Text @@ -262,7 +262,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> let! recipients = task { if model.SendToClass = "N" && usr.IsAdmin then let! users = Users.all () - return users |> List.map (fun u -> { Member.empty with Name = u.Name; Email = u.Email }) + return users |> List.map (fun u -> { Member.Empty with Name = u.Name; Email = u.Email }) else return! Members.forGroup group.Id } use! client = Email.getConnection () @@ -282,9 +282,9 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> | _, None -> () | _, Some x when not x -> () | _, _ -> - let zone = SmallGroup.timeZone group + let zone = group.TimeZone do! PrayerRequests.save - { PrayerRequest.empty with + { PrayerRequest.Empty with Id = (Guid.NewGuid >> PrayerRequestId) () SmallGroupId = group.Id UserId = usr.Id diff --git a/src/PrayerTracker/User.fs b/src/PrayerTracker/User.fs index 7e16833..d6a83f5 100644 --- a/src/PrayerTracker/User.fs +++ b/src/PrayerTracker/User.fs @@ -14,20 +14,20 @@ open PrayerTracker.ViewModels /// Password hashing implementation extending ASP.NET Core's identity implementation [] module Hashing = - + open System.Security.Cryptography open System.Text - + /// Custom password hasher used to verify and upgrade old password hashes type PrayerTrackerPasswordHasher() = inherit PasswordHasher() - + override this.VerifyHashedPassword(user, hashedPassword, providedPassword) = if isNull hashedPassword then nullArg (nameof hashedPassword) if isNull providedPassword then nullArg (nameof providedPassword) - + let hashBytes = Convert.FromBase64String hashedPassword - + match hashBytes[0] with | 255uy -> // v2 hashes - PBKDF2 (RFC 2898), 1,024 rounds @@ -53,7 +53,7 @@ module Hashing = PasswordVerificationResult.Failed | _ -> base.VerifyHashedPassword(user, hashedPassword, providedPassword) - + /// Retrieve a user from the database by password, upgrading password hashes if required let private findUserByPassword model = task { match! Users.tryByEmailAndGroup model.Email (idFromShort SmallGroupId model.SmallGroupId) with @@ -125,7 +125,7 @@ open Microsoft.AspNetCore.Html // POST /user/log-on let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task { match! ctx.TryBindFormAsync() with - | Ok model -> + | Ok model -> let s = ctx.Strings match! findUserByPassword model with | Some user -> @@ -218,7 +218,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c match! ctx.TryBindFormAsync() with | Ok model -> let! user = - if model.IsNew then Task.FromResult(Some { User.empty with Id = (Guid.NewGuid >> UserId) () }) + if model.IsNew then Task.FromResult(Some { User.Empty with Id = (Guid.NewGuid >> UserId) () }) else Users.tryById (idFromShort UserId model.UserId) match user with | Some usr -> @@ -230,7 +230,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c let h = CommonFunctions.htmlString { UserMessage.info with 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", updatedUser.Name] |> Some } @@ -267,7 +267,7 @@ let smallGroups usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx - let! groups = SmallGroups.listAll () let! groupIds = Users.groupIdsByUserId userId let curGroups = groupIds |> List.map (fun g -> shortGuid g.Value) - return! + return! viewInfo ctx |> Views.User.assignGroups (AssignGroups.fromUser user) groups curGroups ctx |> renderHtml next ctx