WIP on doc queries (#55)
This commit is contained in:
		
							parent
							
								
									bade89dd37
								
							
						
					
					
						commit
						14b0a58d98
					
				| @ -1,9 +1,5 @@ | |||||||
| namespace PrayerTracker.Data | namespace PrayerTracker.Data | ||||||
| 
 | 
 | ||||||
| open System |  | ||||||
| open NodaTime |  | ||||||
| open PrayerTracker.Entities |  | ||||||
| 
 |  | ||||||
| /// Table names | /// Table names | ||||||
| [<RequireQualifiedAccess>] | [<RequireQualifiedAccess>] | ||||||
| module Table = | module Table = | ||||||
| @ -29,6 +25,10 @@ module Table = | |||||||
|     let User = "pt_user" |     let User = "pt_user" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | open System | ||||||
|  | open NodaTime | ||||||
|  | open PrayerTracker.Entities | ||||||
|  | 
 | ||||||
| /// JSON serialization customizations | /// JSON serialization customizations | ||||||
| [<RequireQualifiedAccess>] | [<RequireQualifiedAccess>] | ||||||
| module Json = | module Json = | ||||||
| @ -36,12 +36,10 @@ module Json = | |||||||
|     open System.Text.Json.Serialization |     open System.Text.Json.Serialization | ||||||
| 
 | 
 | ||||||
|     /// Convert a wrapped DU to/from its string representation |     /// Convert a wrapped DU to/from its string representation | ||||||
|     type WrappedJsonConverter<'T>(wrap : string -> 'T, unwrap : 'T -> string) = |     type WrappedJsonConverter<'T>(wrap: string -> 'T, unwrap: 'T -> string) = | ||||||
|         inherit JsonConverter<'T>() |         inherit JsonConverter<'T>() | ||||||
|         override _.Read(reader, _, _) = |         override _.Read(reader, _, _) = wrap (reader.GetString()) | ||||||
|             wrap (reader.GetString()) |         override _.Write(writer, value, _) = writer.WriteStringValue(unwrap value) | ||||||
|         override _.Write(writer, value, _) = |  | ||||||
|             writer.WriteStringValue(unwrap value) |  | ||||||
| 
 | 
 | ||||||
|     open System.Text.Json |     open System.Text.Json | ||||||
|     open NodaTime.Serialization.SystemTextJson |     open NodaTime.Serialization.SystemTextJson | ||||||
| @ -49,6 +47,7 @@ module Json = | |||||||
|     /// JSON serializer options to support the target domain |     /// JSON serializer options to support the target domain | ||||||
|     let options = |     let options = | ||||||
|         let opts = JsonSerializerOptions() |         let opts = JsonSerializerOptions() | ||||||
|  | 
 | ||||||
|         [ WrappedJsonConverter<AsOfDateDisplay>(AsOfDateDisplay.Parse, string) :> JsonConverter |         [ WrappedJsonConverter<AsOfDateDisplay>(AsOfDateDisplay.Parse, string) :> JsonConverter | ||||||
|           WrappedJsonConverter<EmailFormat>(EmailFormat.Parse, string) |           WrappedJsonConverter<EmailFormat>(EmailFormat.Parse, string) | ||||||
|           WrappedJsonConverter<Expiration>(Expiration.Parse, string) |           WrappedJsonConverter<Expiration>(Expiration.Parse, string) | ||||||
| @ -62,121 +61,204 @@ module Json = | |||||||
|           WrappedJsonConverter<UserId>(Guid.Parse >> UserId, string) |           WrappedJsonConverter<UserId>(Guid.Parse >> UserId, string) | ||||||
|           JsonFSharpConverter() ] |           JsonFSharpConverter() ] | ||||||
|         |> List.iter opts.Converters.Add |         |> List.iter opts.Converters.Add | ||||||
|  | 
 | ||||||
|         let _ = opts.ConfigureForNodaTime DateTimeZoneProviders.Tzdb |         let _ = opts.ConfigureForNodaTime DateTimeZoneProviders.Tzdb | ||||||
|         opts.PropertyNamingPolicy   <- JsonNamingPolicy.CamelCase |         opts.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase | ||||||
|         opts.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull |         opts.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull | ||||||
|         opts |         opts | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| open BitBadger.Documents | open BitBadger.Documents | ||||||
| open BitBadger.Documents.Sqlite | open BitBadger.Documents.Sqlite | ||||||
| 
 | 
 | ||||||
| /// Establish the required data environment | /// Establish the required data environment | ||||||
| [<RequireQualifiedAccess>] | [<RequireQualifiedAccess>] | ||||||
| module Connection = | module Connection = | ||||||
|      |  | ||||||
|     open System.Text.Json |  | ||||||
|      |  | ||||||
|     /// Ensure tables and indexes are defined |  | ||||||
|     let setUp () = backgroundTask { |  | ||||||
|         Configuration.useIdField "id" |  | ||||||
|         Configuration.useSerializer |  | ||||||
|             { new IDocumentSerializer with |  | ||||||
|                 member _.Serialize<'T>(it : 'T)       = JsonSerializer.Serialize(it, Json.options) |  | ||||||
|                 member _.Deserialize<'T>(it : string) = JsonSerializer.Deserialize<'T>(it, Json.options) |  | ||||||
|             } |  | ||||||
|          |  | ||||||
|         let! tables = Custom.list<string> "SELECT table_name FROM sqlite_master" [] _.GetString(0) |  | ||||||
|         if not (List.contains Table.Church tables) then |  | ||||||
|             do! Definition.ensureTable Table.Church |  | ||||||
|         if not (List.contains Table.Group tables) then |  | ||||||
|             do! Definition.ensureTable Table.Group |  | ||||||
|             do! Definition.ensureFieldIndex Table.Group "church" [ "churchId" ] |  | ||||||
|         if not (List.contains Table.Member tables) then |  | ||||||
|             do! Definition.ensureTable Table.Member |  | ||||||
|             do! Definition.ensureFieldIndex Table.Member "group" [ "smallGroupId" ] |  | ||||||
|         if not (List.contains Table.Request tables) then |  | ||||||
|             do! Definition.ensureTable Table.Request |  | ||||||
|             do! Definition.ensureFieldIndex Table.Request "group" [ "smallGroupId" ] |  | ||||||
|         if not (List.contains Table.User tables) then |  | ||||||
|             do! Definition.ensureTable Table.User |  | ||||||
|             do! Definition.ensureFieldIndex Table.User "email" [ "email" ] |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|  |     open System.Text.Json | ||||||
|  | 
 | ||||||
|  |     /// Ensure tables and indexes are defined | ||||||
|  |     let setUp () = | ||||||
|  |         backgroundTask { | ||||||
|  |             Configuration.useIdField "id" | ||||||
|  | 
 | ||||||
|  |             Configuration.useSerializer | ||||||
|  |                 { new IDocumentSerializer with | ||||||
|  |                     member _.Serialize<'T>(it: 'T) = | ||||||
|  |                         JsonSerializer.Serialize(it, Json.options) | ||||||
|  | 
 | ||||||
|  |                     member _.Deserialize<'T>(it: string) = | ||||||
|  |                         JsonSerializer.Deserialize<'T>(it, Json.options) } | ||||||
|  | 
 | ||||||
|  |             let! tables = Custom.list<string> "SELECT table_name FROM sqlite_master" [] _.GetString(0) | ||||||
|  | 
 | ||||||
|  |             if not (List.contains Table.Church tables) then | ||||||
|  |                 do! Definition.ensureTable Table.Church | ||||||
|  | 
 | ||||||
|  |             if not (List.contains Table.Group tables) then | ||||||
|  |                 do! Definition.ensureTable Table.Group | ||||||
|  |                 do! Definition.ensureFieldIndex Table.Group "church" [ "churchId" ] | ||||||
|  | 
 | ||||||
|  |             if not (List.contains Table.Member tables) then | ||||||
|  |                 do! Definition.ensureTable Table.Member | ||||||
|  |                 do! Definition.ensureFieldIndex Table.Member "group" [ "smallGroupId" ] | ||||||
|  | 
 | ||||||
|  |             if not (List.contains Table.Request tables) then | ||||||
|  |                 do! Definition.ensureTable Table.Request | ||||||
|  |                 do! Definition.ensureFieldIndex Table.Request "group" [ "smallGroupId" ] | ||||||
|  | 
 | ||||||
|  |             if not (List.contains Table.User tables) then | ||||||
|  |                 do! Definition.ensureTable Table.User | ||||||
|  |                 do! Definition.ensureFieldIndex Table.User "email" [ "email" ] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | open Microsoft.Data.Sqlite | ||||||
| 
 | 
 | ||||||
| /// Helper functions for the PostgreSQL data implementation | /// Helper functions for the PostgreSQL data implementation | ||||||
| [<AutoOpen>] | [<AutoOpen>] | ||||||
| module private Helpers = | module private Helpers = | ||||||
| 
 | 
 | ||||||
|     /// Map a row to a Prayer Request instance |     /// Map a row to a Prayer Request instance | ||||||
|     let mapToPrayerRequest (row : RowReader) = |     let mapToPrayerRequest (row: RowReader) = | ||||||
|         {   Id             = PrayerRequestId         (row.uuid "id") |         { Id = PrayerRequestId(row.uuid "id") | ||||||
|             UserId         = UserId                  (row.uuid "user_id") |           UserId = UserId(row.uuid "user_id") | ||||||
|             SmallGroupId   = SmallGroupId            (row.uuid "small_group_id") |           SmallGroupId = SmallGroupId(row.uuid "small_group_id") | ||||||
|             EnteredDate    = row.fieldValue<Instant> "entered_date" |           EnteredDate = row.fieldValue<Instant> "entered_date" | ||||||
|             UpdatedDate    = row.fieldValue<Instant> "updated_date" |           UpdatedDate = row.fieldValue<Instant> "updated_date" | ||||||
|             Requestor      = row.stringOrNone        "requestor" |           Requestor = row.stringOrNone "requestor" | ||||||
|             Text           = row.string              "request_text" |           Text = row.string "request_text" | ||||||
|             NotifyChaplain = row.bool                "notify_chaplain" |           NotifyChaplain = row.bool "notify_chaplain" | ||||||
|             RequestType    = PrayerRequestType.Parse (row.string "request_type") |           RequestType = PrayerRequestType.Parse(row.string "request_type") | ||||||
|             Expiration     = Expiration.Parse        (row.string "expiration") |           Expiration = Expiration.Parse(row.string "expiration") } | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|     /// Map a row to a Small Group information set | 
 | ||||||
|     let mapToSmallGroupInfo (row : RowReader) = | open Npgsql | ||||||
|         {   Id         = Giraffe.ShortGuid.fromGuid (row.uuid "id") | 
 | ||||||
|             Name       = row.string "group_name" | /// Functions to retrieve small group information | ||||||
|             ChurchName = row.string "church_name" | module SmallGroups = | ||||||
|             TimeZoneId = TimeZoneId (row.string "time_zone_id") | 
 | ||||||
|             IsPublic   = row.bool   "is_public" |     /// Query to retrieve data for a small group info instance | ||||||
|         } |     let private infoQuery = | ||||||
|  |         $"SELECT g.data->>'id' AS id, g.data->>'groupName' AS groupName, c.data->>'churchName' AS churchName, | ||||||
|  |                  g.data->'preferences'->>'timeZoneId' AS timeZoneId, g.data->'preferences'->>'isPublic' AS isPublic | ||||||
|  |             FROM {Table.Group} g | ||||||
|  |                  INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'" | ||||||
|  | 
 | ||||||
|  |     /// Query to retrieve data for a small group select list item | ||||||
|  |     let private itemQuery = | ||||||
|  |         $"SELECT g.data->>'groupName' AS groupName, g.data->>'id' AS id, c.data->>'churchName' AS churchName | ||||||
|  |             FROM {Table.Group} g | ||||||
|  |                  INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'" | ||||||
|  | 
 | ||||||
|  |     /// The ORDER BY clause for select list item queries | ||||||
|  |     let private itemOrderBy = "ORDER BY c.data->>'churchName', g.data->>'groupName'" | ||||||
| 
 | 
 | ||||||
|     /// Map a row to a Small Group list item |     /// Map a row to a Small Group list item | ||||||
|     let mapToSmallGroupItem (row : RowReader) = |     let private toSmallGroupItem (rdr: SqliteDataReader) = | ||||||
|         Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}""" |         (rdr.GetOrdinal >> rdr.GetString >> Guid.Parse >> Giraffe.ShortGuid.fromGuid) "id", | ||||||
|  |         $"""{(rdr.GetOrdinal >> rdr.GetString) "churchName"} | {(rdr.GetOrdinal >> rdr.GetString) "groupName"}""" | ||||||
| 
 | 
 | ||||||
|     /// Map a row to a User instance |     /// Get the group IDs for the given church | ||||||
|     let mapToUser (row : RowReader) = |     let internal groupIdsByChurch (churchId: ChurchId) = | ||||||
|         {   Id           = UserId     (row.uuid "id") |         backgroundTask { | ||||||
|             FirstName    = row.string "first_name" |             let! groups = Find.byFields<SmallGroup> Table.Group All [ Field.Equal "churchId" churchId ] | ||||||
|             LastName     = row.string "last_name" |             return groups |> List.map _.Id | ||||||
|             Email        = row.string "email" |  | ||||||
|             IsAdmin      = row.bool   "is_admin" |  | ||||||
|             PasswordHash = row.string "password_hash" |  | ||||||
|             LastSeen     = row.fieldValueOrNone<Instant> "last_seen" |  | ||||||
|             SmallGroups  = [] |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |     /// Count the number of small groups for a church | ||||||
|  |     let countByChurch (churchId: ChurchId) = | ||||||
|  |         Count.byFields Table.Group All [ Field.Equal "churchId" churchId ] | ||||||
|  | 
 | ||||||
|  |     /// Delete a small group by its ID | ||||||
|  |     let deleteById (groupId: SmallGroupId) = | ||||||
|  |         backgroundTask { | ||||||
|  |             use conn = Configuration.dbConn () | ||||||
|  |             use! txn = conn.BeginTransactionAsync() | ||||||
|  | 
 | ||||||
|  |             let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||||
|  | 
 | ||||||
|  |             for user in users do | ||||||
|  |                 do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |} | ||||||
|  | 
 | ||||||
|  |             do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" groupId ] | ||||||
|  |             do! conn.deleteById Table.Group groupId | ||||||
|  | 
 | ||||||
|  |             do! txn.CommitAsync() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     /// Get information for all small groups | ||||||
|  |     let infoForAll () = | ||||||
|  |         Custom.list $"{infoQuery} ORDER BY g.data->>'groupName'" [] SmallGroupInfo.FromReader | ||||||
|  | 
 | ||||||
|  |     /// Get a list of small group IDs along with a description that includes the church name | ||||||
|  |     let listAll () = | ||||||
|  |         Custom.list $"{itemQuery} {itemOrderBy}" [] toSmallGroupItem | ||||||
|  | 
 | ||||||
|  |     /// Get a list of small group IDs and descriptions for groups with a group password | ||||||
|  |     let listProtected () = | ||||||
|  |         Custom.list | ||||||
|  |             $"{itemQuery} WHERE COALESCE(g.data->'preferences'->>'groupPassword', '') <> '' {itemOrderBy}" | ||||||
|  |             [] | ||||||
|  |             toSmallGroupItem | ||||||
|  | 
 | ||||||
|  |     /// Get a list of small group IDs and descriptions for groups that are public or have a group password | ||||||
|  |     let listPublicAndProtected () = | ||||||
|  |         Custom.list | ||||||
|  |             $"{infoQuery} | ||||||
|  |               WHERE g.data->'preferences'->>'isPublic' = TRUE | ||||||
|  |                  OR COALESCE(g.data->'preferences'->>'groupPassword', '') <> '' | ||||||
|  |               ORDER BY c.data->>'churchName', g.data->>'groupName'" | ||||||
|  |             [] | ||||||
|  |             SmallGroupInfo.FromReader | ||||||
|  | 
 | ||||||
|  |     /// Log on for a small group (includes list preferences) | ||||||
|  |     let logOn (groupId: SmallGroupId) (password: string) = | ||||||
|  |         Find.firstByFields<SmallGroup> | ||||||
|  |             Table.Group | ||||||
|  |             All | ||||||
|  |             [ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ] | ||||||
|  | 
 | ||||||
|  |     /// Save a small group | ||||||
|  |     let save group = save<SmallGroup> Table.Group group | ||||||
|  | 
 | ||||||
|  |     /// Save a small group's list preferences | ||||||
|  |     let savePreferences (groupId: SmallGroupId) (pref: ListPreferences) = | ||||||
|  |         Patch.byId Table.Group groupId {| Preferences = pref |} | ||||||
|  | 
 | ||||||
|  |     /// Get a small group by its ID (including list preferences) | ||||||
|  |     let tryById groupId = | ||||||
|  |         Find.byId<SmallGroupId, SmallGroup> Table.Group groupId | ||||||
| 
 | 
 | ||||||
| open BitBadger.Documents |  | ||||||
| open Npgsql |  | ||||||
| open Npgsql.FSharp |  | ||||||
| 
 | 
 | ||||||
| /// Functions to manipulate churches | /// Functions to manipulate churches | ||||||
| module Churches = | module Churches = | ||||||
| 
 | 
 | ||||||
|     /// Get a list of all churches |     /// Get a list of all churches | ||||||
|     let all () = |     let all () = Find.all<Church> Table.Church | ||||||
|         Find.all<Church> Table.Church |  | ||||||
| 
 | 
 | ||||||
|     /// Delete a church by its ID |     /// Delete a church by its ID | ||||||
|     let deleteById (churchId: ChurchId) = backgroundTask { |     let deleteById churchId = | ||||||
|         let idParam = [ [ "@churchId", Sql.uuid churchId.Value ] ] |         backgroundTask { | ||||||
|         let where   = "WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)" |             use conn = Configuration.dbConn () | ||||||
|         let! _ = |             use! txn = conn.BeginTransactionAsync() | ||||||
|             BitBadger.Documents.Postgres.Configuration.dataSource () | 
 | ||||||
|             |> Sql.fromDataSource |             let! groupIds = SmallGroups.groupIdsByChurch churchId | ||||||
|             |> Sql.executeTransactionAsync | 
 | ||||||
|                 [   $"DELETE FROM pt.prayer_request {where}", idParam |             do! Delete.byFields Table.Request All [ Field.In "smallGroupId" groupIds ] | ||||||
|                     $"DELETE FROM pt.user_small_group {where}", idParam | 
 | ||||||
|                     $"DELETE FROM pt.list_preference {where}", idParam |             let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User groupIds ] | ||||||
|                     "DELETE FROM pt.small_group WHERE church_id = @churchId", idParam | 
 | ||||||
|                     "DELETE FROM pt.church WHERE id = @churchId", idParam ] |             for user in users do | ||||||
|         () |                 do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except groupIds |} | ||||||
|     } | 
 | ||||||
|  |             do! Delete.byFields Table.Group All [ Field.Equal "churchId" churchId ] | ||||||
|  |             do! Delete.byId Table.Church churchId | ||||||
|  |             do! txn.CommitAsync() | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|     /// Save a church's information |     /// Save a church's information | ||||||
|     let save church = |     let save church = save<Church> Table.Church church | ||||||
|         save<Church> Table.Church church |  | ||||||
| 
 | 
 | ||||||
|     /// Find a church by its ID |     /// Find a church by its ID | ||||||
|     let tryById churchId = |     let tryById churchId = | ||||||
| @ -191,17 +273,18 @@ module Members = | |||||||
|         Count.byFields Table.Member All [ Field.Equal "smallGroupId" groupId ] |         Count.byFields Table.Member All [ Field.Equal "smallGroupId" groupId ] | ||||||
| 
 | 
 | ||||||
|     /// Delete a small group member by its ID |     /// Delete a small group member by its ID | ||||||
|     let deleteById (memberId: MemberId) = |     let deleteById (memberId: MemberId) = Delete.byId Table.Member memberId | ||||||
|         Delete.byId Table.Member memberId |  | ||||||
| 
 | 
 | ||||||
|     /// Retrieve all members for a given small group |     /// Retrieve all members for a given small group | ||||||
|     let forGroup (groupId : SmallGroupId) = |     let forGroup (groupId: SmallGroupId) = | ||||||
|         Find.byFieldsOrdered<Member> |         Find.byFieldsOrdered<Member> | ||||||
|             Table.Member All [ Field.Equal "smallGroupId" groupId ] [ Field.Named "memberName" ] |             Table.Member | ||||||
|  |             All | ||||||
|  |             [ Field.Equal "smallGroupId" groupId ] | ||||||
|  |             [ Field.Named "memberName" ] | ||||||
| 
 | 
 | ||||||
|     /// Save a small group member |     /// Save a small group member | ||||||
|     let save mbr = |     let save mbr = save<Member> Table.Member mbr | ||||||
|         save<Member> Table.Member mbr |  | ||||||
| 
 | 
 | ||||||
|     /// Retrieve a small group member by its ID |     /// Retrieve a small group member by its ID | ||||||
|     let tryById memberId = |     let tryById memberId = | ||||||
| @ -210,20 +293,21 @@ module Members = | |||||||
| 
 | 
 | ||||||
| /// Options to retrieve a list of requests | /// Options to retrieve a list of requests | ||||||
| type PrayerRequestOptions = | type PrayerRequestOptions = | ||||||
|     {   /// The small group for which requests should be retrieved |     { | ||||||
|         SmallGroup : SmallGroup |         /// The small group for which requests should be retrieved | ||||||
|  |         SmallGroup: SmallGroup | ||||||
| 
 | 
 | ||||||
|         /// The clock instance to use for date/time manipulation |         /// The clock instance to use for date/time manipulation | ||||||
|         Clock : IClock |         Clock: IClock | ||||||
| 
 | 
 | ||||||
|         /// The date for which the list is being retrieved |         /// The date for which the list is being retrieved | ||||||
|         ListDate : LocalDate option |         ListDate: LocalDate option | ||||||
| 
 | 
 | ||||||
|         /// Whether only active requests should be retrieved |         /// Whether only active requests should be retrieved | ||||||
|         ActiveOnly : bool |         ActiveOnly: bool | ||||||
| 
 | 
 | ||||||
|         /// The page number, for paged lists |         /// The page number, for paged lists | ||||||
|         PageNumber : int |         PageNumber: int | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -237,57 +321,66 @@ module PrayerRequests = | |||||||
|         | SortByRequestor -> "requestor, updated_date DESC, entered_date DESC" |         | SortByRequestor -> "requestor, updated_date DESC, entered_date DESC" | ||||||
| 
 | 
 | ||||||
|     /// Paginate a prayer request query |     /// Paginate a prayer request query | ||||||
|     let private paginate (pageNbr : int) pageSize = |     let private paginate (pageNbr: int) pageSize = | ||||||
|         if pageNbr > 0 then $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" else "" |         if pageNbr > 0 then | ||||||
|  |             $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" | ||||||
|  |         else | ||||||
|  |             "" | ||||||
| 
 | 
 | ||||||
|     /// Count the number of prayer requests for a church |     /// Count the number of prayer requests for a church | ||||||
|     let countByChurch (churchId : ChurchId) = |     let countByChurch (churchId: ChurchId) = | ||||||
|         BitBadger.Documents.Postgres.Custom.scalar |         BitBadger.Documents.Postgres.Custom.scalar | ||||||
|             "SELECT COUNT(id) AS req_count |             "SELECT COUNT(id) AS req_count | ||||||
|                FROM pt.prayer_request |                FROM pt.prayer_request | ||||||
|               WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)" |               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") |             [ "@churchId", Sql.uuid churchId.Value ] | ||||||
|  |             (fun row -> row.int "req_count") | ||||||
| 
 | 
 | ||||||
|     /// Count the number of prayer requests for a small group |     /// Count the number of prayer requests for a small group | ||||||
|     let countByGroup (groupId: SmallGroupId) = |     let countByGroup (groupId: SmallGroupId) = | ||||||
|         Count.byFields Table.Request All [ Field.Equal "smallGroupId" groupId ] |         Count.byFields Table.Request All [ Field.Equal "smallGroupId" groupId ] | ||||||
| 
 | 
 | ||||||
|     /// Delete a prayer request by its ID |     /// Delete a prayer request by its ID | ||||||
|     let deleteById (reqId: PrayerRequestId) = |     let deleteById (reqId: PrayerRequestId) = Delete.byId Table.Request reqId | ||||||
|         Delete.byId Table.Request reqId |  | ||||||
| 
 | 
 | ||||||
|     /// Get all (or active) requests for a small group as of now or the specified date |     /// Get all (or active) requests for a small group as of now or the specified date | ||||||
|     let forGroup (opts : PrayerRequestOptions) = |     let forGroup (opts: PrayerRequestOptions) = | ||||||
|         let theDate = defaultArg opts.ListDate (opts.SmallGroup.LocalDateNow opts.Clock) |         let theDate = defaultArg opts.ListDate (opts.SmallGroup.LocalDateNow opts.Clock) | ||||||
|  | 
 | ||||||
|         let where, parameters = |         let where, parameters = | ||||||
|             if opts.ActiveOnly then |             if opts.ActiveOnly then | ||||||
|                 let asOf = NpgsqlParameter ( |                 let asOf = | ||||||
|                     "@asOf", |                     NpgsqlParameter( | ||||||
|                     (theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone) |                         "@asOf", | ||||||
|                             - Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire) |                         (theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone) | ||||||
|                         .ToInstant ()) |                          - Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire) | ||||||
|  |                             .ToInstant() | ||||||
|  |                     ) | ||||||
|  | 
 | ||||||
|                 "   AND (   updated_date > @asOf |                 "   AND (   updated_date > @asOf | ||||||
|                          OR expiration   = @manual |                          OR expiration   = @manual | ||||||
|                          OR request_type = @longTerm |                          OR request_type = @longTerm | ||||||
|                          OR request_type = @expecting) |                          OR request_type = @expecting) | ||||||
|                     AND expiration <> @forced", |                     AND expiration <> @forced", | ||||||
|                 [   "@asOf",      Sql.parameter asOf |                 [ "@asOf", Sql.parameter asOf | ||||||
|                     "@manual",    Sql.string    (string Manual) |                   "@manual", Sql.string (string Manual) | ||||||
|                     "@longTerm",  Sql.string    (string LongTermRequest) |                   "@longTerm", Sql.string (string LongTermRequest) | ||||||
|                     "@expecting", Sql.string    (string Expecting) |                   "@expecting", Sql.string (string Expecting) | ||||||
|                     "@forced",    Sql.string    (string Forced) ] |                   "@forced", Sql.string (string Forced) ] | ||||||
|             else "", [] |             else | ||||||
|  |                 "", [] | ||||||
|  | 
 | ||||||
|         BitBadger.Documents.Postgres.Custom.list |         BitBadger.Documents.Postgres.Custom.list | ||||||
|             $"SELECT * |             $"SELECT * | ||||||
|                 FROM pt.prayer_request |                 FROM pt.prayer_request | ||||||
|                WHERE small_group_id = @groupId {where} |                WHERE small_group_id = @groupId {where} | ||||||
|                ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort} |                ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort} | ||||||
|                {paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}" |                {paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}" | ||||||
|             (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) mapToPrayerRequest |             (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) | ||||||
|  |             mapToPrayerRequest | ||||||
| 
 | 
 | ||||||
|     /// Save a prayer request |     /// Save a prayer request | ||||||
|     let save req = |     let save req = save<PrayerRequest> Table.Request req | ||||||
|         save<PrayerRequest> Table.Request req |  | ||||||
| 
 | 
 | ||||||
|     /// Search prayer requests for the given term |     /// Search prayer requests for the given term | ||||||
|     let searchForGroup group searchTerm pageNbr = |     let searchForGroup group searchTerm pageNbr = | ||||||
| @ -297,7 +390,9 @@ module PrayerRequests = | |||||||
|               SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search |               SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search | ||||||
|               ORDER BY {orderBy group.Preferences.RequestSort} |               ORDER BY {orderBy group.Preferences.RequestSort} | ||||||
|               {paginate pageNbr group.Preferences.PageSize}" |               {paginate pageNbr group.Preferences.PageSize}" | ||||||
|             [ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ] mapToPrayerRequest |             [ "@groupId", Sql.uuid group.Id.Value | ||||||
|  |               "@search", Sql.string $"%%%s{searchTerm}%%" ] | ||||||
|  |             mapToPrayerRequest | ||||||
| 
 | 
 | ||||||
|     /// Retrieve a prayer request by its ID |     /// Retrieve a prayer request by its ID | ||||||
|     let tryById reqId = |     let tryById reqId = | ||||||
| @ -306,92 +401,15 @@ module PrayerRequests = | |||||||
|     /// Update the expiration for the given prayer request |     /// Update the expiration for the given prayer request | ||||||
|     let updateExpiration (req: PrayerRequest) withTime = |     let updateExpiration (req: PrayerRequest) withTime = | ||||||
|         if withTime then |         if withTime then | ||||||
|             Patch.byId Table.Request req.Id {| UpdatedDate = req.UpdatedDate; Expiration = req.Expiration |} |             Patch.byId | ||||||
|  |                 Table.Request | ||||||
|  |                 req.Id | ||||||
|  |                 {| UpdatedDate = req.UpdatedDate | ||||||
|  |                    Expiration = req.Expiration |} | ||||||
|         else |         else | ||||||
|             Patch.byId Table.Request req.Id {| Expiration = req.Expiration |} |             Patch.byId Table.Request req.Id {| Expiration = req.Expiration |} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// Functions to retrieve small group information |  | ||||||
| module SmallGroups = |  | ||||||
| 
 |  | ||||||
|     /// Count the number of small groups for a church |  | ||||||
|     let countByChurch (churchId: ChurchId) = |  | ||||||
|         Count.byFields Table.Group All [ Field.Equal "churchId" churchId ] |  | ||||||
| 
 |  | ||||||
|     /// Delete a small group by its ID |  | ||||||
|     let deleteById (groupId: SmallGroupId) = backgroundTask { |  | ||||||
|         use conn = Configuration.dbConn () |  | ||||||
|         use txn  = conn.BeginTransaction() |  | ||||||
|          |  | ||||||
|         let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] |  | ||||||
|         for user in users do |  | ||||||
|             do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |} |  | ||||||
|         do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" groupId ] |  | ||||||
|         do! conn.deleteById Table.Group groupId |  | ||||||
|          |  | ||||||
|         do! txn.CommitAsync() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// Get information for all small groups |  | ||||||
|     let infoForAll () = |  | ||||||
|         BitBadger.Documents.Postgres.Custom.list |  | ||||||
|             "SELECT sg.id, sg.group_name, c.church_name, lp.time_zone_id, lp.is_public |  | ||||||
|                FROM pt.small_group sg |  | ||||||
|                     INNER JOIN pt.church c ON c.id = sg.church_id |  | ||||||
|                     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 () = |  | ||||||
|         BitBadger.Documents.Postgres.Custom.list |  | ||||||
|             "SELECT g.group_name, g.id, c.church_name |  | ||||||
|                FROM pt.small_group g |  | ||||||
|                     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 () = |  | ||||||
|         BitBadger.Documents.Postgres.Custom.list |  | ||||||
|             "SELECT g.group_name, g.id, c.church_name, lp.is_public |  | ||||||
|                FROM pt.small_group g |  | ||||||
|                     INNER JOIN pt.church           c ON c.id = g.church_id |  | ||||||
|                     INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id |  | ||||||
|               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 () = |  | ||||||
|         BitBadger.Documents.Postgres.Custom.list |  | ||||||
|             "SELECT g.group_name, g.id, c.church_name, lp.time_zone_id, lp.is_public |  | ||||||
|                FROM pt.small_group g |  | ||||||
|                     INNER JOIN pt.church           c ON c.id = g.church_id |  | ||||||
|                     INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id |  | ||||||
|               WHERE lp.is_public = TRUE |  | ||||||
|                  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: string) = |  | ||||||
|         Find.firstByFields<SmallGroup> |  | ||||||
|             Table.Group All [ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ] |  | ||||||
| 
 |  | ||||||
|     /// Save a small group |  | ||||||
|     let save group = |  | ||||||
|         save<SmallGroup> Table.Group group |  | ||||||
| 
 |  | ||||||
|     /// Save a small group's list preferences |  | ||||||
|     let savePreferences (pref: ListPreferences) = |  | ||||||
|         Patch.byId Table.Group pref.SmallGroupId {| Preferences = pref |} |  | ||||||
| 
 |  | ||||||
|     /// Get a small group by its ID (including list preferences) |  | ||||||
|     let tryById groupId = |  | ||||||
|         Find.byId<SmallGroupId, SmallGroup> Table.Group groupId |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| /// Functions to manipulate users | /// Functions to manipulate users | ||||||
| module Users = | module Users = | ||||||
| 
 | 
 | ||||||
| @ -400,44 +418,37 @@ module Users = | |||||||
|         Find.allOrdered<User> Table.User [ Field.Named "lastName"; Field.Named "firstName" ] |         Find.allOrdered<User> Table.User [ Field.Named "lastName"; Field.Named "firstName" ] | ||||||
| 
 | 
 | ||||||
|     /// Count the number of users for a church |     /// Count the number of users for a church | ||||||
|     let countByChurch (churchId : ChurchId) = |     let countByChurch churchId = | ||||||
|         BitBadger.Documents.Postgres.Custom.scalar |         backgroundTask { | ||||||
|             "SELECT COUNT(u.id) AS user_count |             let! groupIds = SmallGroups.groupIdsByChurch churchId | ||||||
|                FROM pt.pt_user u |             return! Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User groupIds ] | ||||||
|               WHERE EXISTS ( |         } | ||||||
|                     SELECT 1 |  | ||||||
|                       FROM pt.user_small_group usg |  | ||||||
|                            INNER JOIN pt.small_group sg ON sg.id = usg.small_group_id |  | ||||||
|                      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 |     /// Count the number of users for a small group | ||||||
|     let countByGroup (groupId: SmallGroupId) = |     let countByGroup (groupId: SmallGroupId) = | ||||||
|         Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] |         Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||||
| 
 | 
 | ||||||
|     /// Delete a user by its database ID |     /// Delete a user by its database ID | ||||||
|     let deleteById (userId: UserId) = |     let deleteById (userId: UserId) = Delete.byId Table.User userId | ||||||
|         Delete.byId Table.User userId |  | ||||||
| 
 | 
 | ||||||
|     /// Get a list of users authorized to administer the given small group |     /// Get a list of users authorized to administer the given small group | ||||||
|     let listByGroupId (groupId : SmallGroupId) = |     let listByGroupId (groupId: SmallGroupId) = | ||||||
|         BitBadger.Documents.Postgres.Custom.list |         Find.byFieldsOrdered<User> | ||||||
|             "SELECT u.* |             Table.User | ||||||
|                FROM pt.pt_user u |             All | ||||||
|                     INNER JOIN pt.user_small_group usg ON usg.user_id = u.id |             [ Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||||
|               WHERE usg.small_group_id = @groupId |             [ Field.Named "lastName"; Field.Named "firstName" ] | ||||||
|               ORDER BY u.last_name, u.first_name" |  | ||||||
|             [ "@groupId", Sql.uuid groupId.Value ] mapToUser |  | ||||||
| 
 | 
 | ||||||
|     /// Save a user's information |     /// Save a user's information | ||||||
|     let save user = |     let save user = save<User> Table.User user | ||||||
|         save<User> Table.User user |  | ||||||
| 
 | 
 | ||||||
|     /// Find a user by its e-mail address and authorized small group |     /// Find a user by its e-mail address and authorized small group | ||||||
|     let tryByEmailAndGroup (email: string) (groupId: SmallGroupId) = |     let tryByEmailAndGroup (email: string) (groupId: SmallGroupId) = | ||||||
|         Find.firstByFields<User> |         Find.firstByFields<User> | ||||||
|             Table.User All [ Field.Equal "email" email; Field.InArray "smallGroups" Table.User [ groupId ] ] |             Table.User | ||||||
|  |             All | ||||||
|  |             [ Field.Equal "email" email | ||||||
|  |               Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||||
| 
 | 
 | ||||||
|     /// Find a user by their database ID |     /// Find a user by their database ID | ||||||
|     let tryById userId = |     let tryById userId = | ||||||
|  | |||||||
| @ -209,6 +209,8 @@ type UserId = | |||||||
| 
 | 
 | ||||||
| (*-- SPECIFIC VIEW TYPES --*) | (*-- SPECIFIC VIEW TYPES --*) | ||||||
| 
 | 
 | ||||||
|  | open Microsoft.Data.Sqlite | ||||||
|  | 
 | ||||||
| /// Statistics for churches | /// Statistics for churches | ||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type ChurchStats = | type ChurchStats = | ||||||
| @ -225,7 +227,7 @@ type ChurchStats = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// Information needed to display the public/protected request list and small group maintenance pages | /// Information needed to display the public/protected request list and small group maintenance pages | ||||||
| [<NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type SmallGroupInfo = | type SmallGroupInfo = | ||||||
|     { |     { | ||||||
|         /// The ID of the small group |         /// The ID of the small group | ||||||
| @ -243,13 +245,22 @@ type SmallGroupInfo = | |||||||
|         /// Whether the small group has a publicly-available request list |         /// Whether the small group has a publicly-available request list | ||||||
|         IsPublic: bool |         IsPublic: bool | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     /// Map a row to a Small Group information set | ||||||
|  |     static member FromReader (rdr: SqliteDataReader) = | ||||||
|  |         { Id = Giraffe.ShortGuid.fromGuid ((rdr.GetOrdinal >> rdr.GetString >> Guid.Parse) "id") | ||||||
|  |           Name = (rdr.GetOrdinal >> rdr.GetString) "groupName" | ||||||
|  |           ChurchName = (rdr.GetOrdinal >> rdr.GetString) "churchName" | ||||||
|  |           TimeZoneId = (rdr.GetOrdinal >> rdr.GetString >> TimeZoneId) "timeZoneId" | ||||||
|  |           IsPublic = (rdr.GetOrdinal >> rdr.GetBoolean) "isPublic" } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| (*-- ENTITIES --*) | (*-- ENTITIES --*) | ||||||
| 
 | 
 | ||||||
| open NodaTime | open NodaTime | ||||||
| 
 | 
 | ||||||
| /// This represents a church | /// This represents a church | ||||||
| [<NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type Church = | type Church = | ||||||
|     { |     { | ||||||
|         /// The ID of this church |         /// The ID of this church | ||||||
| @ -286,9 +297,6 @@ type Church = | |||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type ListPreferences = | type ListPreferences = | ||||||
|     { |     { | ||||||
|         /// The Id of the small group to which these preferences belong |  | ||||||
|         SmallGroupId: SmallGroupId |  | ||||||
| 
 |  | ||||||
|         /// The days after which regular requests expire |         /// The days after which regular requests expire | ||||||
|         DaysToExpire: int |         DaysToExpire: int | ||||||
| 
 | 
 | ||||||
| @ -350,8 +358,7 @@ type ListPreferences = | |||||||
| 
 | 
 | ||||||
|     /// A set of preferences with their default values |     /// A set of preferences with their default values | ||||||
|     static member Empty = |     static member Empty = | ||||||
|         { SmallGroupId = SmallGroupId Guid.Empty |         { DaysToExpire = 14 | ||||||
|           DaysToExpire = 14 |  | ||||||
|           DaysToKeepNew = 7 |           DaysToKeepNew = 7 | ||||||
|           LongTermUpdateWeeks = 4 |           LongTermUpdateWeeks = 4 | ||||||
|           EmailFromName = "PrayerTracker" |           EmailFromName = "PrayerTracker" | ||||||
| @ -371,7 +378,7 @@ type ListPreferences = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A member of a small group | /// A member of a small group | ||||||
| [<NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type Member = | type Member = | ||||||
|     { |     { | ||||||
|         /// The ID of the small group member |         /// The ID of the small group member | ||||||
| @ -400,7 +407,7 @@ type Member = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// This represents a small group (Sunday School class, Bible study group, etc.) | /// This represents a small group (Sunday School class, Bible study group, etc.) | ||||||
| [<NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type SmallGroup = | type SmallGroup = | ||||||
|     { |     { | ||||||
|         /// The ID of this small group |         /// The ID of this small group | ||||||
| @ -444,7 +451,7 @@ type SmallGroup = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// This represents a single prayer request | /// This represents a single prayer request | ||||||
| [<NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type PrayerRequest = | type PrayerRequest = | ||||||
|     { |     { | ||||||
|         /// The ID of this request |         /// The ID of this request | ||||||
| @ -515,7 +522,7 @@ type PrayerRequest = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// This represents a user of PrayerTracker | /// This represents a user of PrayerTracker | ||||||
| [<NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type User = | type User = | ||||||
|     { |     { | ||||||
|         /// The ID of this user |         /// The ID of this user | ||||||
| @ -556,20 +563,3 @@ type User = | |||||||
|           PasswordHash = "" |           PasswordHash = "" | ||||||
|           LastSeen = None |           LastSeen = None | ||||||
|           SmallGroups = [] } |           SmallGroups = [] } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| /// Cross-reference between user and small group |  | ||||||
| [<NoComparison; NoEquality>] |  | ||||||
| type UserSmallGroup = |  | ||||||
|     { |  | ||||||
|         /// The Id of the user who has access to the small group |  | ||||||
|         UserId: UserId |  | ||||||
| 
 |  | ||||||
|         /// The Id of the small group to which the user has access |  | ||||||
|         SmallGroupId: SmallGroupId |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// An empty user/small group xref |  | ||||||
|     static member Empty = |  | ||||||
|         { UserId = UserId Guid.Empty |  | ||||||
|           SmallGroupId = SmallGroupId Guid.Empty } |  | ||||||
|  | |||||||
| @ -39,8 +39,7 @@ module PgMappings = | |||||||
|           ChurchId    = ChurchId     (row.uuid "church_id") |           ChurchId    = ChurchId     (row.uuid "church_id") | ||||||
|           Name        = row.string   "group_name" |           Name        = row.string   "group_name" | ||||||
|           Preferences = |           Preferences = | ||||||
|               { SmallGroupId        = SmallGroupId (row.uuid "small_group_id") |               { DaysToKeepNew       = row.int      "days_to_keep_new" | ||||||
|                 DaysToKeepNew       = row.int      "days_to_keep_new" |  | ||||||
|                 DaysToExpire        = row.int      "days_to_expire" |                 DaysToExpire        = row.int      "days_to_expire" | ||||||
|                 LongTermUpdateWeeks = row.int      "long_term_update_weeks" |                 LongTermUpdateWeeks = row.int      "long_term_update_weeks" | ||||||
|                 EmailFromName       = row.string   "email_from_name" |                 EmailFromName       = row.string   "email_from_name" | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ let private findStats churchId = task { | |||||||
|     let! groups   = SmallGroups.countByChurch    churchId |     let! groups   = SmallGroups.countByChurch    churchId | ||||||
|     let! requests = PrayerRequests.countByChurch churchId |     let! requests = PrayerRequests.countByChurch churchId | ||||||
|     let! users    = Users.countByChurch          churchId |     let! users    = Users.countByChurch          churchId | ||||||
|     return shortGuid churchId.Value, { SmallGroups = int groups; PrayerRequests = requests; Users = users } |     return shortGuid churchId.Value, { SmallGroups = int groups; PrayerRequests = requests; Users = int users } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // POST /church/[church-id]/delete | // POST /church/[church-id]/delete | ||||||
|  | |||||||
| @ -230,7 +230,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> | |||||||
|         match! SmallGroups.tryById group.Id with |         match! SmallGroups.tryById group.Id with | ||||||
|         | Some group -> |         | Some group -> | ||||||
|             let pref = model.PopulatePreferences group.Preferences |             let pref = model.PopulatePreferences group.Preferences | ||||||
|             do! SmallGroups.savePreferences pref |             do! SmallGroups.savePreferences group.Id pref | ||||||
|             // Refresh session instance |             // Refresh session instance | ||||||
|             ctx.Session.CurrentGroup <- Some { group with Preferences = pref } |             ctx.Session.CurrentGroup <- Some { group with Preferences = pref } | ||||||
|             addInfo ctx ctx.Strings["Group preferences updated successfully"] |             addInfo ctx ctx.Strings["Group preferences updated successfully"] | ||||||
|  | |||||||
| @ -121,7 +121,6 @@ let listPreferencesTests = | |||||||
|         } |         } | ||||||
|         test "Empty is as expected" { |         test "Empty is as expected" { | ||||||
|             let mt = ListPreferences.Empty |             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.DaysToExpire 14 "The default days to expire should have been 14" | ||||||
|             Expect.equal mt.DaysToKeepNew 7 "The default days to keep new should have been 7" |             Expect.equal mt.DaysToKeepNew 7 "The default days to keep new should have been 7" | ||||||
|             Expect.equal mt.LongTermUpdateWeeks 4 "The default long term update weeks should have been 4" |             Expect.equal mt.LongTermUpdateWeeks 4 "The default long term update weeks should have been 4" | ||||||
| @ -367,13 +366,3 @@ let userTests = | |||||||
|             Expect.equal user.Name "Unit Test" "The full name should be the first and last, separated by a space" |             Expect.equal user.Name "Unit Test" "The full name should be the first and last, separated by a space" | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
| 
 |  | ||||||
| [<Tests>] |  | ||||||
| let userSmallGroupTests = |  | ||||||
|     testList "UserSmallGroup" [ |  | ||||||
|         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" |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user