Move module funcs to properties
This commit is contained in:
		
							parent
							
								
									facc294d66
								
							
						
					
					
						commit
						42e3a58131
					
				| @ -8,7 +8,7 @@ open PrayerTracker.Entities | ||||
| /// Helper functions for the PostgreSQL data implementation | ||||
| [<AutoOpen>] | ||||
| 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 | ||||
|  | ||||
| @ -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.) | ||||
| [<NoComparison; NoEquality>] | ||||
| 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 | ||||
| [<NoComparison; NoEquality>] | ||||
| 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.) | ||||
| [<NoComparison; NoEquality>] | ||||
| 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 | ||||
| [<NoComparison; NoEquality>] | ||||
| @ -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 } | ||||
|  | ||||
| @ -39,8 +39,8 @@ let asOfDateDisplayTests = | ||||
| [<Tests>] | ||||
| 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 = | ||||
| [<Tests>] | ||||
| 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 = | ||||
| [<Tests>] | ||||
| 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 = | ||||
| [<Tests>] | ||||
| 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" | ||||
|         } | ||||
|  | ||||
| @ -15,7 +15,7 @@ let countAll _ = true | ||||
| 
 | ||||
| 
 | ||||
| module ReferenceListTests = | ||||
|    | ||||
| 
 | ||||
|     [<Tests>] | ||||
|     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" | ||||
|             } | ||||
|         ] | ||||
|      | ||||
| 
 | ||||
|     [<Tests>] | ||||
|     let expirationListTests = | ||||
|         testList "ReferenceList.expirationList" [ | ||||
| @ -66,7 +66,7 @@ module ReferenceListTests = | ||||
|                     "The option for immediate expiration was not found" | ||||
|             } | ||||
|         ] | ||||
|      | ||||
| 
 | ||||
|     [<Tests>] | ||||
|     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 """<strong>Zeb</strong> – zyx<i style="font-size:9.60pt">  (as of %s)</i>""" | ||||
|                 // spot check; if one request has it, they all should | ||||
|                 Expect.stringContains html expected "Expected short as-of date not found"     | ||||
|                 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 """<strong>Zeb</strong> – zyx<i style="font-size:9.60pt">  (as of %s)</i>""" | ||||
|                 // spot check; if one request has it, they all should | ||||
|                 Expect.stringContains html expected "Expected long as-of date not found"     | ||||
|                 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 | ||||
|  | ||||
| @ -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 | ||||
|      | ||||
| 
 | ||||
| /// <summary>Generate an <c>input type=radio</c> that is selected if its value is the current value</summary> | ||||
| 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" | ||||
|  | ||||
| @ -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<IClock> ()) group | ||||
|     let now   = group.LocalDateNow (ctx.GetService<IClock>()) | ||||
|     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 | ||||
|  | ||||
| @ -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 ] | ||||
|                     ] | ||||
|  | ||||
| @ -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 | ||||
| [<CLIMutable; NoComparison; NoEquality>] | ||||
| 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<PrayerRequestType, int> | ||||
|          | ||||
| 
 | ||||
|         /// 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 | ||||
|  | ||||
| @ -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<EditChurch> () 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 -> | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -183,7 +183,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c | ||||
|     match! ctx.TryBindFormAsync<EditSmallGroup>() 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 | ||||
|  | ||||
| @ -14,20 +14,20 @@ open PrayerTracker.ViewModels | ||||
| /// Password hashing implementation extending ASP.NET Core's identity implementation | ||||
| [<AutoOpen>] | ||||
| module Hashing = | ||||
|      | ||||
| 
 | ||||
|     open System.Security.Cryptography | ||||
|     open System.Text | ||||
|      | ||||
| 
 | ||||
|     /// Custom password hasher used to verify and upgrade old password hashes | ||||
|     type PrayerTrackerPasswordHasher() = | ||||
|         inherit PasswordHasher<User>() | ||||
|          | ||||
| 
 | ||||
|         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<UserLogOn>() 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<EditUser>() 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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user