WIP on doc queries (#55)

This commit is contained in:
Daniel J. Summers 2025-01-31 17:24:12 -05:00
parent bade89dd37
commit 14b0a58d98
6 changed files with 263 additions and 274 deletions

View File

@ -1,9 +1,5 @@
namespace PrayerTracker.Data
open System
open NodaTime
open PrayerTracker.Entities
/// Table names
[<RequireQualifiedAccess>]
module Table =
@ -29,6 +25,10 @@ module Table =
let User = "pt_user"
open System
open NodaTime
open PrayerTracker.Entities
/// JSON serialization customizations
[<RequireQualifiedAccess>]
module Json =
@ -36,12 +36,10 @@ module Json =
open System.Text.Json.Serialization
/// 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>()
override _.Read(reader, _, _) =
wrap (reader.GetString())
override _.Write(writer, value, _) =
writer.WriteStringValue(unwrap value)
override _.Read(reader, _, _) = wrap (reader.GetString())
override _.Write(writer, value, _) = writer.WriteStringValue(unwrap value)
open System.Text.Json
open NodaTime.Serialization.SystemTextJson
@ -49,6 +47,7 @@ module Json =
/// JSON serializer options to support the target domain
let options =
let opts = JsonSerializerOptions()
[ WrappedJsonConverter<AsOfDateDisplay>(AsOfDateDisplay.Parse, string) :> JsonConverter
WrappedJsonConverter<EmailFormat>(EmailFormat.Parse, string)
WrappedJsonConverter<Expiration>(Expiration.Parse, string)
@ -62,11 +61,13 @@ module Json =
WrappedJsonConverter<UserId>(Guid.Parse >> UserId, string)
JsonFSharpConverter() ]
|> List.iter opts.Converters.Add
let _ = opts.ConfigureForNodaTime DateTimeZoneProviders.Tzdb
opts.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase
opts.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull
opts
open BitBadger.Documents
open BitBadger.Documents.Sqlite
@ -77,106 +78,187 @@ module Connection =
open System.Text.Json
/// Ensure tables and indexes are defined
let setUp () = backgroundTask {
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)
}
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
[<AutoOpen>]
module private Helpers =
/// Map a row to a Prayer Request instance
let mapToPrayerRequest (row : RowReader) =
{ Id = PrayerRequestId (row.uuid "id")
UserId = UserId (row.uuid "user_id")
SmallGroupId = SmallGroupId (row.uuid "small_group_id")
let mapToPrayerRequest (row: RowReader) =
{ Id = PrayerRequestId(row.uuid "id")
UserId = UserId(row.uuid "user_id")
SmallGroupId = SmallGroupId(row.uuid "small_group_id")
EnteredDate = row.fieldValue<Instant> "entered_date"
UpdatedDate = row.fieldValue<Instant> "updated_date"
Requestor = row.stringOrNone "requestor"
Text = row.string "request_text"
NotifyChaplain = row.bool "notify_chaplain"
RequestType = PrayerRequestType.Parse (row.string "request_type")
Expiration = Expiration.Parse (row.string "expiration")
}
RequestType = PrayerRequestType.Parse(row.string "request_type")
Expiration = Expiration.Parse(row.string "expiration") }
/// Map a row to a Small Group information set
let mapToSmallGroupInfo (row : RowReader) =
{ Id = Giraffe.ShortGuid.fromGuid (row.uuid "id")
Name = row.string "group_name"
ChurchName = row.string "church_name"
TimeZoneId = TimeZoneId (row.string "time_zone_id")
IsPublic = row.bool "is_public"
}
open Npgsql
/// Functions to retrieve small group information
module SmallGroups =
/// 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
let mapToSmallGroupItem (row : RowReader) =
Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}"""
let private toSmallGroupItem (rdr: SqliteDataReader) =
(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
let mapToUser (row : RowReader) =
{ Id = UserId (row.uuid "id")
FirstName = row.string "first_name"
LastName = row.string "last_name"
Email = row.string "email"
IsAdmin = row.bool "is_admin"
PasswordHash = row.string "password_hash"
LastSeen = row.fieldValueOrNone<Instant> "last_seen"
SmallGroups = []
/// Get the group IDs for the given church
let internal groupIdsByChurch (churchId: ChurchId) =
backgroundTask {
let! groups = Find.byFields<SmallGroup> Table.Group All [ Field.Equal "churchId" churchId ]
return groups |> List.map _.Id
}
/// 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
module Churches =
/// Get a list of all churches
let all () =
Find.all<Church> Table.Church
let all () = Find.all<Church> Table.Church
/// Delete a church by its ID
let deleteById (churchId: ChurchId) = backgroundTask {
let idParam = [ [ "@churchId", Sql.uuid churchId.Value ] ]
let where = "WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)"
let! _ =
BitBadger.Documents.Postgres.Configuration.dataSource ()
|> Sql.fromDataSource
|> Sql.executeTransactionAsync
[ $"DELETE FROM pt.prayer_request {where}", idParam
$"DELETE FROM pt.user_small_group {where}", idParam
$"DELETE FROM pt.list_preference {where}", idParam
"DELETE FROM pt.small_group WHERE church_id = @churchId", idParam
"DELETE FROM pt.church WHERE id = @churchId", idParam ]
()
let deleteById churchId =
backgroundTask {
use conn = Configuration.dbConn ()
use! txn = conn.BeginTransactionAsync()
let! groupIds = SmallGroups.groupIdsByChurch churchId
do! Delete.byFields Table.Request All [ Field.In "smallGroupId" groupIds ]
let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User groupIds ]
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
let save church =
save<Church> Table.Church church
let save church = save<Church> Table.Church church
/// Find a church by its ID
let tryById churchId =
@ -191,17 +273,18 @@ module Members =
Count.byFields Table.Member All [ Field.Equal "smallGroupId" groupId ]
/// Delete a small group member by its ID
let deleteById (memberId: MemberId) =
Delete.byId Table.Member memberId
let deleteById (memberId: MemberId) = Delete.byId Table.Member memberId
/// Retrieve all members for a given small group
let forGroup (groupId : SmallGroupId) =
let forGroup (groupId: SmallGroupId) =
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
let save mbr =
save<Member> Table.Member mbr
let save mbr = save<Member> Table.Member mbr
/// Retrieve a small group member by its ID
let tryById memberId =
@ -210,20 +293,21 @@ module Members =
/// Options to retrieve a list of requests
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
Clock : IClock
Clock: IClock
/// The date for which the list is being retrieved
ListDate : LocalDate option
ListDate: LocalDate option
/// Whether only active requests should be retrieved
ActiveOnly : bool
ActiveOnly: bool
/// The page number, for paged lists
PageNumber : int
PageNumber: int
}
@ -237,35 +321,42 @@ module PrayerRequests =
| 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 ""
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) =
let countByChurch (churchId: ChurchId) =
BitBadger.Documents.Postgres.Custom.scalar
"SELECT COUNT(id) AS req_count
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")
[ "@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) =
Count.byFields Table.Request All [ Field.Equal "smallGroupId" groupId ]
/// Delete a prayer request by its ID
let deleteById (reqId: PrayerRequestId) =
Delete.byId Table.Request reqId
let deleteById (reqId: PrayerRequestId) = Delete.byId Table.Request reqId
/// 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 where, parameters =
if opts.ActiveOnly then
let asOf = NpgsqlParameter (
let asOf =
NpgsqlParameter(
"@asOf",
(theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone)
- Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire)
.ToInstant ())
.ToInstant()
)
" AND ( updated_date > @asOf
OR expiration = @manual
OR request_type = @longTerm
@ -276,18 +367,20 @@ module PrayerRequests =
"@longTerm", Sql.string (string LongTermRequest)
"@expecting", Sql.string (string Expecting)
"@forced", Sql.string (string Forced) ]
else "", []
else
"", []
BitBadger.Documents.Postgres.Custom.list
$"SELECT *
FROM pt.prayer_request
WHERE small_group_id = @groupId {where}
ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort}
{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
let save req =
save<PrayerRequest> Table.Request req
let save req = save<PrayerRequest> Table.Request req
/// Search prayer requests for the given term
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
ORDER BY {orderBy group.Preferences.RequestSort}
{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
let tryById reqId =
@ -306,92 +401,15 @@ module PrayerRequests =
/// Update the expiration for the given prayer request
let updateExpiration (req: PrayerRequest) withTime =
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
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
module Users =
@ -400,44 +418,37 @@ module Users =
Find.allOrdered<User> Table.User [ Field.Named "lastName"; Field.Named "firstName" ]
/// Count the number of users for a church
let countByChurch (churchId : ChurchId) =
BitBadger.Documents.Postgres.Custom.scalar
"SELECT COUNT(u.id) AS user_count
FROM pt.pt_user u
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")
let countByChurch churchId =
backgroundTask {
let! groupIds = SmallGroups.groupIdsByChurch churchId
return! Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User groupIds ]
}
/// Count the number of users for a small group
let countByGroup (groupId: SmallGroupId) =
Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ]
/// Delete a user by its database ID
let deleteById (userId: UserId) =
Delete.byId Table.User userId
let deleteById (userId: UserId) = Delete.byId Table.User userId
/// Get a list of users authorized to administer the given small group
let listByGroupId (groupId : SmallGroupId) =
BitBadger.Documents.Postgres.Custom.list
"SELECT u.*
FROM pt.pt_user u
INNER JOIN pt.user_small_group usg ON usg.user_id = u.id
WHERE usg.small_group_id = @groupId
ORDER BY u.last_name, u.first_name"
[ "@groupId", Sql.uuid groupId.Value ] mapToUser
let listByGroupId (groupId: SmallGroupId) =
Find.byFieldsOrdered<User>
Table.User
All
[ Field.InArray "smallGroups" Table.User [ groupId ] ]
[ Field.Named "lastName"; Field.Named "firstName" ]
/// Save a user's information
let save user =
save<User> Table.User user
let save user = save<User> Table.User user
/// Find a user by its e-mail address and authorized small group
let tryByEmailAndGroup (email: string) (groupId: SmallGroupId) =
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
let tryById userId =

View File

@ -209,6 +209,8 @@ type UserId =
(*-- SPECIFIC VIEW TYPES --*)
open Microsoft.Data.Sqlite
/// Statistics for churches
[<NoComparison; NoEquality>]
type ChurchStats =
@ -225,7 +227,7 @@ type ChurchStats =
/// Information needed to display the public/protected request list and small group maintenance pages
[<NoComparison; NoEquality>]
[<CLIMutable; NoComparison; NoEquality>]
type SmallGroupInfo =
{
/// The ID of the small group
@ -244,12 +246,21 @@ type SmallGroupInfo =
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 --*)
open NodaTime
/// This represents a church
[<NoComparison; NoEquality>]
[<CLIMutable; NoComparison; NoEquality>]
type Church =
{
/// The ID of this church
@ -286,9 +297,6 @@ type Church =
[<NoComparison; NoEquality>]
type ListPreferences =
{
/// The Id of the small group to which these preferences belong
SmallGroupId: SmallGroupId
/// The days after which regular requests expire
DaysToExpire: int
@ -350,8 +358,7 @@ type ListPreferences =
/// A set of preferences with their default values
static member Empty =
{ SmallGroupId = SmallGroupId Guid.Empty
DaysToExpire = 14
{ DaysToExpire = 14
DaysToKeepNew = 7
LongTermUpdateWeeks = 4
EmailFromName = "PrayerTracker"
@ -371,7 +378,7 @@ type ListPreferences =
/// A member of a small group
[<NoComparison; NoEquality>]
[<CLIMutable; NoComparison; NoEquality>]
type 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.)
[<NoComparison; NoEquality>]
[<CLIMutable; NoComparison; NoEquality>]
type SmallGroup =
{
/// The ID of this small group
@ -444,7 +451,7 @@ type SmallGroup =
/// This represents a single prayer request
[<NoComparison; NoEquality>]
[<CLIMutable; NoComparison; NoEquality>]
type PrayerRequest =
{
/// The ID of this request
@ -515,7 +522,7 @@ type PrayerRequest =
/// This represents a user of PrayerTracker
[<NoComparison; NoEquality>]
[<CLIMutable; NoComparison; NoEquality>]
type User =
{
/// The ID of this user
@ -556,20 +563,3 @@ type User =
PasswordHash = ""
LastSeen = None
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 }

View File

@ -39,8 +39,7 @@ module PgMappings =
ChurchId = ChurchId (row.uuid "church_id")
Name = row.string "group_name"
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"
LongTermUpdateWeeks = row.int "long_term_update_weeks"
EmailFromName = row.string "email_from_name"

View File

@ -12,7 +12,7 @@ let private findStats churchId = task {
let! groups = SmallGroups.countByChurch churchId
let! requests = PrayerRequests.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

View File

@ -230,7 +230,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
match! SmallGroups.tryById group.Id with
| Some group ->
let pref = model.PopulatePreferences group.Preferences
do! SmallGroups.savePreferences pref
do! SmallGroups.savePreferences group.Id pref
// Refresh session instance
ctx.Session.CurrentGroup <- Some { group with Preferences = pref }
addInfo ctx ctx.Strings["Group preferences updated successfully"]

View File

@ -121,7 +121,6 @@ let listPreferencesTests =
}
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"
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"
}
]
[<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"
}
]