Release 7.4 #21

Merged
danieljsummers merged 7 commits from release-7.4 into master 2019-10-19 17:22:52 +00:00
Showing only changes of commit 4dbd58fb92 - Show all commits

View File

@ -3,7 +3,6 @@ module PrayerTracker.DataAccess
open FSharp.Control.Tasks.ContextInsensitive open FSharp.Control.Tasks.ContextInsensitive
open Microsoft.EntityFrameworkCore open Microsoft.EntityFrameworkCore
open Microsoft.FSharpLu
open PrayerTracker.Entities open PrayerTracker.Entities
open System.Collections.Generic open System.Collections.Generic
open System.Linq open System.Linq
@ -11,17 +10,29 @@ open System.Linq
[<AutoOpen>] [<AutoOpen>]
module private Helpers = module private Helpers =
open Microsoft.FSharpLu
open System.Threading.Tasks
/// Central place to append sort criteria for prayer request queries /// Central place to append sort criteria for prayer request queries
let reqSort sort (query : IQueryable<PrayerRequest>) = let reqSort sort (q : IQueryable<PrayerRequest>) =
match sort with match sort with
| SortByDate -> | SortByDate ->
query.OrderByDescending(fun pr -> pr.updatedDate) query {
.ThenByDescending(fun pr -> pr.enteredDate) for req in q do
.ThenBy(fun pr -> pr.requestor) sortByDescending req.updatedDate
thenByDescending req.enteredDate
thenBy req.requestor
}
| SortByRequestor -> | SortByRequestor ->
query.OrderBy(fun pr -> pr.requestor) query {
.ThenByDescending(fun pr -> pr.updatedDate) for req in q do
.ThenByDescending(fun pr -> pr.enteredDate) sortBy req.requestor
thenByDescending req.updatedDate
thenByDescending req.enteredDate
}
/// Convert a possibly-null object to an option, wrapped as a task
let toOptionTask<'T> (item : 'T) = (Option.fromObject >> Task.FromResult) item
type AppDbContext with type AppDbContext with
@ -44,15 +55,22 @@ type AppDbContext with
/// Find a church by its Id /// Find a church by its Id
member this.TryChurchById cId = member this.TryChurchById cId =
task { query {
let! church = this.Churches.AsNoTracking().FirstOrDefaultAsync (fun c -> c.churchId = cId) for ch in this.Churches.AsNoTracking () do
return Option.fromObject church where (ch.churchId = cId)
exactlyOneOrDefault
} }
|> toOptionTask
/// Find all churches /// Find all churches
member this.AllChurches () = member this.AllChurches () =
task { task {
let! churches = this.Churches.AsNoTracking().OrderBy(fun c -> c.name).ToListAsync () let q =
query {
for ch in this.Churches.AsNoTracking () do
sortBy ch.name
}
let! churches = q.ToListAsync ()
return List.ofSeq churches return List.ofSeq churches
} }
@ -60,19 +78,24 @@ type AppDbContext with
/// Get a small group member by its Id /// Get a small group member by its Id
member this.TryMemberById mId = member this.TryMemberById mId =
task { query {
let! mbr = this.Members.AsNoTracking().FirstOrDefaultAsync (fun m -> m.memberId = mId) for mbr in this.Members.AsNoTracking () do
return Option.fromObject mbr where (mbr.memberId = mId)
select mbr
exactlyOneOrDefault
} }
|> toOptionTask
/// Find all members for a small group /// Find all members for a small group
member this.AllMembersForSmallGroup gId = member this.AllMembersForSmallGroup gId =
task { task {
let! mbrs = let q =
this.Members.AsNoTracking() query {
.Where(fun m -> m.smallGroupId = gId) for mbr in this.Members.AsNoTracking () do
.OrderBy(fun m -> m.memberName) where (mbr.smallGroupId = gId)
.ToListAsync () sortBy mbr.memberName
}
let! mbrs = q.ToListAsync ()
return List.ofSeq mbrs return List.ofSeq mbrs
} }
@ -84,32 +107,44 @@ type AppDbContext with
/// Get a prayer request by its Id /// Get a prayer request by its Id
member this.TryRequestById reqId = member this.TryRequestById reqId =
task { query {
let! req = this.PrayerRequests.AsNoTracking().FirstOrDefaultAsync (fun pr -> pr.prayerRequestId = reqId) for req in this.PrayerRequests.AsNoTracking () do
return Option.fromObject req where (req.prayerRequestId = reqId)
exactlyOneOrDefault
} }
|> toOptionTask
/// 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
// TODO: why not make this an async list like the rest of these methods?
member this.AllRequestsForSmallGroup (grp : SmallGroup) clock listDate activeOnly pageNbr : PrayerRequest seq = member this.AllRequestsForSmallGroup (grp : SmallGroup) clock listDate activeOnly pageNbr : PrayerRequest seq =
let theDate = match listDate with Some dt -> dt | _ -> grp.localDateNow clock let theDate = match listDate with Some dt -> dt | _ -> grp.localDateNow clock
upcast ( query {
this.PrayerRequests.AsNoTracking().Where(fun pr -> pr.smallGroupId = grp.smallGroupId) for req in this.PrayerRequests.AsNoTracking () do
where (req.smallGroupId = grp.smallGroupId)
}
|> function |> function
| query when activeOnly -> | q when activeOnly ->
let asOf = theDate.AddDays(-(float grp.preferences.daysToExpire)).Date let asOf = theDate.AddDays(-(float grp.preferences.daysToExpire)).Date
query.Where(fun pr -> query {
( pr.updatedDate > asOf for req in q do
|| pr.expiration = Manual where ( ( req.updatedDate > asOf
|| pr.requestType = LongTermRequest || req.expiration = Manual
|| pr.requestType = Expecting) || req.requestType = LongTermRequest
&& pr.expiration <> Forced) || req.requestType = Expecting)
| query -> query && req.expiration <> Forced)
}
| q -> q
|> reqSort grp.preferences.requestSort |> reqSort grp.preferences.requestSort
|> function |> function
| query -> | q ->
match activeOnly with match activeOnly with
| true -> query.Skip 0 | true -> upcast q
| false -> query.Skip((pageNbr - 1) * grp.preferences.pageSize).Take grp.preferences.pageSize) | false ->
upcast query {
for req in q do
skip ((pageNbr - 1) * grp.preferences.pageSize)
take grp.preferences.pageSize
}
/// Count prayer requests for the given small group Id /// Count prayer requests for the given small group Id
member this.CountRequestsBySmallGroup gId = member this.CountRequestsBySmallGroup gId =
@ -120,57 +155,64 @@ type AppDbContext with
this.PrayerRequests.CountAsync (fun pr -> pr.smallGroup.churchId = cId) this.PrayerRequests.CountAsync (fun pr -> pr.smallGroup.churchId = cId)
/// 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
// TODO: same as above...
member this.SearchRequestsForSmallGroup (grp : SmallGroup) (searchTerm : string) pageNbr : PrayerRequest seq = member this.SearchRequestsForSmallGroup (grp : SmallGroup) (searchTerm : string) pageNbr : PrayerRequest seq =
let pgSz = grp.preferences.pageSize let pgSz = grp.preferences.pageSize
let skip = (pageNbr - 1) * pgSz let toSkip = (pageNbr - 1) * pgSz
let sql = let sql =
""" SELECT * FROM pt."PrayerRequest" WHERE "SmallGroupId" = {0} AND "Text" ILIKE {1} """ SELECT * FROM pt."PrayerRequest" WHERE "SmallGroupId" = {0} AND "Text" ILIKE {1}
UNION UNION
SELECT * FROM pt."PrayerRequest" WHERE "SmallGroupId" = {0} AND COALESCE("Requestor", '') ILIKE {1}""" SELECT * FROM pt."PrayerRequest" WHERE "SmallGroupId" = {0} AND COALESCE("Requestor", '') ILIKE {1}"""
|> RawSqlString |> RawSqlString
let like = sprintf "%%%s%%" let like = sprintf "%%%s%%"
upcast (
this.PrayerRequests.FromSql(sql, grp.smallGroupId, like searchTerm).AsNoTracking () this.PrayerRequests.FromSql(sql, grp.smallGroupId, like searchTerm).AsNoTracking ()
|> reqSort grp.preferences.requestSort |> reqSort grp.preferences.requestSort
|> function query -> (query.Skip skip).Take pgSz) |> function
| q ->
upcast query {
for req in q do
skip toSkip
take pgSz
}
(*-- SMALL GROUP EXTENSIONS --*) (*-- SMALL GROUP EXTENSIONS --*)
/// Find a small group by its Id /// Find a small group by its Id
member this.TryGroupById gId = member this.TryGroupById gId =
task { query {
let! grp = for grp in this.SmallGroups.AsNoTracking().Include (fun sg -> sg.preferences) do
this.SmallGroups.AsNoTracking() where (grp.smallGroupId = gId)
.Include(fun sg -> sg.preferences) exactlyOneOrDefault
.FirstOrDefaultAsync (fun sg -> sg.smallGroupId = gId)
return Option.fromObject grp
} }
|> toOptionTask
/// Get small groups that are public or password protected /// Get small groups that are public or password protected
member this.PublicAndProtectedGroups () = member this.PublicAndProtectedGroups () =
task { task {
let! grps = let smallGroups = this.SmallGroups.AsNoTracking().Include(fun sg -> sg.preferences).Include (fun sg -> sg.church)
this.SmallGroups.AsNoTracking() let q =
.Include(fun sg -> sg.preferences) query {
.Include(fun sg -> sg.church) for grp in smallGroups do
.Where(fun sg -> where ( grp.preferences.isPublic
sg.preferences.isPublic || (sg.preferences.groupPassword <> null && sg.preferences.groupPassword <> "")) || (grp.preferences.groupPassword <> null && grp.preferences.groupPassword <> ""))
.OrderBy(fun sg -> sg.church.name) sortBy grp.church.name
.ThenBy(fun sg -> sg.name) thenBy grp.name
.ToListAsync () }
let! grps = q.ToListAsync ()
return List.ofSeq grps return List.ofSeq grps
} }
/// Get small groups that are password protected /// Get small groups that are password protected
member this.ProtectedGroups () = member this.ProtectedGroups () =
task { task {
let! grps = let q =
this.SmallGroups.AsNoTracking() query {
.Include(fun sg -> sg.church) for grp in this.SmallGroups.AsNoTracking().Include (fun sg -> sg.church) do
.Where(fun sg -> sg.preferences.groupPassword <> null && sg.preferences.groupPassword <> "") where (grp.preferences.groupPassword <> null && grp.preferences.groupPassword <> "")
.OrderBy(fun sg -> sg.church.name) sortBy grp.church.name
.ThenBy(fun sg -> sg.name) thenBy grp.name
.ToListAsync () }
let! grps = q.ToListAsync ()
return List.ofSeq grps return List.ofSeq grps
} }
@ -190,12 +232,13 @@ type AppDbContext with
/// Get a small group list by their Id, with their church prepended to their name /// Get a small group list by their Id, with their church prepended to their name
member this.GroupList () = member this.GroupList () =
task { task {
let! grps = let q =
this.SmallGroups.AsNoTracking() query {
.Include(fun sg -> sg.church) for grp in this.SmallGroups.AsNoTracking().Include (fun sg -> sg.church) do
.OrderBy(fun sg -> sg.church.name) sortBy grp.church.name
.ThenBy(fun sg -> sg.name) thenBy grp.name
.ToListAsync () }
let! grps = q.ToListAsync ()
return grps return grps
|> Seq.map (fun grp -> grp.smallGroupId.ToString "N", sprintf "%s | %s" grp.church.name grp.name) |> Seq.map (fun grp -> grp.smallGroupId.ToString "N", sprintf "%s | %s" grp.church.name grp.name)
|> List.ofSeq |> List.ofSeq
@ -204,24 +247,22 @@ type AppDbContext with
/// Log on a small group /// Log on a small group
member this.TryGroupLogOnByPassword gId pw = member this.TryGroupLogOnByPassword gId pw =
task { task {
let! grp = this.TryGroupById gId match! this.TryGroupById gId with
match grp with
| None -> return None | None -> return None
| Some g -> | Some grp ->
match pw = g.preferences.groupPassword with match pw = grp.preferences.groupPassword with
| true -> return grp | true -> return Some grp
| _ -> return None | _ -> return None
} }
/// Check a cookie log on for a small group /// Check a cookie log on for a small group
member this.TryGroupLogOnByCookie gId pwHash (hasher : string -> string) = member this.TryGroupLogOnByCookie gId pwHash (hasher : string -> string) =
task { task {
let! grp = this.TryGroupById gId match! this.TryGroupById gId with
match grp with
| None -> return None | None -> return None
| Some g -> | Some grp ->
match pwHash = hasher g.preferences.groupPassword with match pwHash = hasher grp.preferences.groupPassword with
| true -> return grp | true -> return Some grp
| _ -> return None | _ -> return None
} }
@ -233,15 +274,22 @@ type AppDbContext with
/// Get a time zone by its Id /// Get a time zone by its Id
member this.TryTimeZoneById tzId = member this.TryTimeZoneById tzId =
task { query {
let! tz = this.TimeZones.FirstOrDefaultAsync (fun t -> t.timeZoneId = tzId) for tz in this.TimeZones do
return Option.fromObject tz where (tz.timeZoneId = tzId)
exactlyOneOrDefault
} }
|> toOptionTask
/// Get all time zones /// Get all time zones
member this.AllTimeZones () = member this.AllTimeZones () =
task { task {
let! tzs = this.TimeZones.OrderBy(fun t -> t.sortOrder).ToListAsync () let q =
query {
for tz in this.TimeZones do
sortBy tz.sortOrder
}
let! tzs = q.ToListAsync ()
return List.ofSeq tzs return List.ofSeq tzs
} }
@ -249,67 +297,79 @@ type AppDbContext with
/// Find a user by its Id /// Find a user by its Id
member this.TryUserById uId = member this.TryUserById uId =
task { query {
let! user = this.Users.AsNoTracking().FirstOrDefaultAsync (fun u -> u.userId = uId) for usr in this.Users.AsNoTracking () do
return Option.fromObject user where (usr.userId = uId)
exactlyOneOrDefault
} }
|> toOptionTask
/// Find a user by its e-mail address and authorized small group /// Find a user by its e-mail address and authorized small group
member this.TryUserByEmailAndGroup email gId = member this.TryUserByEmailAndGroup email gId =
task { query {
let! user = for usr in this.Users.AsNoTracking () do
this.Users.AsNoTracking().FirstOrDefaultAsync (fun u -> where (usr.emailAddress = email && usr.smallGroups.Any (fun xref -> xref.smallGroupId = gId))
u.emailAddress = email exactlyOneOrDefault
&& u.smallGroups.Any (fun xref -> xref.smallGroupId = gId))
return Option.fromObject user
} }
|> toOptionTask
/// Find a user by its Id (tracked entity), eagerly loading the user's groups /// Find a user by its Id (tracked entity), eagerly loading the user's groups
member this.TryUserByIdWithGroups uId = member this.TryUserByIdWithGroups uId =
task { query {
let! user = this.Users.Include(fun u -> u.smallGroups).FirstOrDefaultAsync (fun u -> u.userId = uId) for usr in this.Users.AsNoTracking().Include (fun u -> u.smallGroups) do
return Option.fromObject user where (usr.userId = uId)
exactlyOneOrDefault
} }
|> toOptionTask
/// Get a list of all users /// Get a list of all users
member this.AllUsers () = member this.AllUsers () =
task { task {
let! usrs = this.Users.AsNoTracking().OrderBy(fun u -> u.lastName).ThenBy(fun u -> u.firstName).ToListAsync () let q =
query {
for usr in this.Users.AsNoTracking () do
sortBy usr.lastName
thenBy usr.firstName
}
let! usrs = q.ToListAsync ()
return List.ofSeq usrs return List.ofSeq usrs
} }
/// Get all PrayerTracker users as members (used to send e-mails) /// Get all PrayerTracker users as members (used to send e-mails)
member this.AllUsersAsMembers () = member this.AllUsersAsMembers () =
task { task {
let! usrs = let q =
this.Users.AsNoTracking().OrderBy(fun u -> u.lastName).ThenBy(fun u -> u.firstName).ToListAsync () query {
return usrs for usr in this.Users.AsNoTracking () do
|> Seq.map (fun u -> { Member.empty with email = u.emailAddress; memberName = u.fullName }) sortBy usr.lastName
|> List.ofSeq thenBy usr.firstName
select { Member.empty with email = usr.emailAddress; memberName = usr.fullName }
}
let! usrs = q.ToListAsync ()
return List.ofSeq usrs
} }
/// Find a user based on their credentials /// Find a user based on their credentials
member this.TryUserLogOnByPassword email pwHash gId = member this.TryUserLogOnByPassword email pwHash gId =
task { query {
let! user = for usr in this.Users.AsNoTracking () do
this.Users.FirstOrDefaultAsync (fun u -> where ( usr.emailAddress = email
u.emailAddress = email && usr.passwordHash = pwHash
&& u.passwordHash = pwHash && usr.smallGroups.Any (fun xref -> xref.smallGroupId = gId))
&& u.smallGroups.Any (fun xref -> xref.smallGroupId = gId)) exactlyOneOrDefault
return Option.fromObject user
} }
|> toOptionTask
/// Find a user based on credentials stored in a cookie /// Find a user based on credentials stored in a cookie
member this.TryUserLogOnByCookie uId gId pwHash = member this.TryUserLogOnByCookie uId gId pwHash =
task { task {
let! user = this.TryUserByIdWithGroups uId match! this.TryUserByIdWithGroups uId with
match user with
| None -> return None | None -> return None
| Some u -> | Some usr ->
match pwHash = u.passwordHash && u.smallGroups |> Seq.exists (fun xref -> xref.smallGroupId = gId) with match pwHash = usr.passwordHash && usr.smallGroups |> Seq.exists (fun xref -> xref.smallGroupId = gId) with
| true -> | true ->
this.Entry<User>(u).State <- EntityState.Detached this.Entry<User>(usr).State <- EntityState.Detached
return Some { u with passwordHash = ""; salt = None; smallGroups = List<UserSmallGroup>() } return Some { usr with passwordHash = ""; salt = None; smallGroups = List<UserSmallGroup>() }
| _ -> return None | _ -> return None
} }