Version 8 #43

Merged
danieljsummers merged 37 commits from version-8 into main 2022-08-19 19:08:31 +00:00
14 changed files with 232 additions and 1590 deletions
Showing only changes of commit 0c95078f69 - Show all commits

View File

@ -39,7 +39,6 @@ module private Helpers =
RequestSort = RequestSort.fromCode (row.string "request_sort") RequestSort = RequestSort.fromCode (row.string "request_sort")
DefaultEmailType = EmailFormat.fromCode (row.string "default_email_type") DefaultEmailType = EmailFormat.fromCode (row.string "default_email_type")
AsOfDateDisplay = AsOfDateDisplay.fromCode (row.string "as_of_date_display") AsOfDateDisplay = AsOfDateDisplay.fromCode (row.string "as_of_date_display")
TimeZone = TimeZone.empty
} }
/// Map a row to a Member instance /// Map a row to a Member instance
@ -49,7 +48,6 @@ module private Helpers =
Name = row.string "member_name" Name = row.string "member_name"
Email = row.string "email" Email = row.string "email"
Format = row.stringOrNone "email_format" |> Option.map EmailFormat.fromCode Format = row.stringOrNone "email_format" |> Option.map EmailFormat.fromCode
SmallGroup = SmallGroup.empty
} }
/// Map a row to a Prayer Request instance /// Map a row to a Prayer Request instance
@ -64,8 +62,6 @@ module private Helpers =
NotifyChaplain = row.bool "notify_chaplain" NotifyChaplain = row.bool "notify_chaplain"
RequestType = PrayerRequestType.fromCode (row.string "request_id") RequestType = PrayerRequestType.fromCode (row.string "request_id")
Expiration = Expiration.fromCode (row.string "expiration") Expiration = Expiration.fromCode (row.string "expiration")
User = User.empty
SmallGroup = SmallGroup.empty
} }
/// Map a row to a Small Group instance /// Map a row to a Small Group instance
@ -74,10 +70,6 @@ module private Helpers =
ChurchId = ChurchId (row.uuid "church_id") ChurchId = ChurchId (row.uuid "church_id")
Name = row.string "group_name" Name = row.string "group_name"
Preferences = ListPreferences.empty Preferences = ListPreferences.empty
Church = Church.empty
Members = ResizeArray ()
PrayerRequests = ResizeArray ()
Users = ResizeArray ()
} }
/// Map a row to a Small Group information set /// Map a row to a Small Group information set
@ -107,9 +99,7 @@ module private Helpers =
Email = row.string "email" Email = row.string "email"
IsAdmin = row.bool "is_admin" IsAdmin = row.bool "is_admin"
PasswordHash = row.string "password_hash" PasswordHash = row.string "password_hash"
Salt = None
LastSeen = row.fieldValueOrNone<Instant> "last_seen" LastSeen = row.fieldValueOrNone<Instant> "last_seen"
SmallGroups = ResizeArray ()
} }
@ -373,6 +363,19 @@ module PrayerRequests =
return () return ()
} }
/// Search prayer requests for the given term
let searchForGroup group searchTerm pageNbr conn =
conn
|> Sql.existingConnection
|> Sql.query $"""
SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND request_text ILIKE @search
UNION
SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search
ORDER BY {orderBy group.Preferences.RequestSort}
{paginate pageNbr group.Preferences.PageSize}"""
|> Sql.parameters [ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ]
|> Sql.executeAsync mapToPrayerRequest
/// Retrieve a prayer request by its ID /// Retrieve a prayer request by its ID
let tryById (reqId : PrayerRequestId) conn = backgroundTask { let tryById (reqId : PrayerRequestId) conn = backgroundTask {
let! req = let! req =
@ -385,14 +388,20 @@ module PrayerRequests =
} }
/// Update the expiration for the given prayer request /// Update the expiration for the given prayer request
let updateExpiration (req : PrayerRequest) conn = backgroundTask { let updateExpiration (req : PrayerRequest) withTime conn = backgroundTask {
let sql, parameters =
if withTime then
", updated_date = @updated",
[ "@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate)) ]
else "", []
let! _ = let! _ =
conn conn
|> Sql.existingConnection |> Sql.existingConnection
|> Sql.query "UPDATE pt.prayer_request SET expiration = @expiration WHERE id = @id" |> Sql.query $"UPDATE pt.prayer_request SET expiration = @expiration{sql} WHERE id = @id"
|> Sql.parameters |> Sql.parameters
[ "@expiration", Sql.string (Expiration.toCode req.Expiration) ([ "@expiration", Sql.string (Expiration.toCode req.Expiration)
"@id", Sql.uuid req.Id.Value ] "@id", Sql.uuid req.Id.Value ]
|> List.append parameters)
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
return () return ()
} }

View File

@ -1,86 +0,0 @@
namespace PrayerTracker
open Microsoft.EntityFrameworkCore
open PrayerTracker.Entities
/// EF Core data context for PrayerTracker
[<AllowNullLiteral>]
type AppDbContext (options : DbContextOptions<AppDbContext>) =
inherit DbContext (options)
[<DefaultValue>]
val mutable private churches : DbSet<Church>
[<DefaultValue>]
val mutable private members : DbSet<Member>
[<DefaultValue>]
val mutable private prayerRequests : DbSet<PrayerRequest>
[<DefaultValue>]
val mutable private preferences : DbSet<ListPreferences>
[<DefaultValue>]
val mutable private smallGroups : DbSet<SmallGroup>
[<DefaultValue>]
val mutable private timeZones : DbSet<TimeZone>
[<DefaultValue>]
val mutable private users : DbSet<User>
[<DefaultValue>]
val mutable private userGroupXref : DbSet<UserSmallGroup>
/// Churches
member this.Churches
with get() = this.churches
and set v = this.churches <- v
/// Small group members
member this.Members
with get() = this.members
and set v = this.members <- v
/// Prayer requests
member this.PrayerRequests
with get() = this.prayerRequests
and set v = this.prayerRequests <- v
/// Request list preferences (by class)
member this.Preferences
with get() = this.preferences
and set v = this.preferences <- v
/// Small groups
member this.SmallGroups
with get() = this.smallGroups
and set v = this.smallGroups <- v
/// Time zones
member this.TimeZones
with get() = this.timeZones
and set v = this.timeZones <- v
/// Users
member this.Users
with get() = this.users
and set v = this.users <- v
/// User / small group cross-reference
member this.UserGroupXref
with get() = this.userGroupXref
and set v = this.userGroupXref <- v
override _.OnConfiguring (optionsBuilder : DbContextOptionsBuilder) =
base.OnConfiguring optionsBuilder
optionsBuilder.UseQueryTrackingBehavior QueryTrackingBehavior.NoTracking |> ignore
override _.OnModelCreating (modelBuilder : ModelBuilder) =
base.OnModelCreating modelBuilder
modelBuilder.HasDefaultSchema "pt" |> ignore
[ Church.ConfigureEF
ListPreferences.ConfigureEF
Member.ConfigureEF
PrayerRequest.ConfigureEF
SmallGroup.ConfigureEF
TimeZone.ConfigureEF
User.ConfigureEF
UserSmallGroup.ConfigureEF
]
|> List.iter (fun x -> x modelBuilder)

View File

@ -1,257 +0,0 @@
[<AutoOpen>]
module PrayerTracker.DataAccess
open System.Linq
open NodaTime
open PrayerTracker.Entities
[<AutoOpen>]
module private Helpers =
/// Central place to append sort criteria for prayer request queries
let reqSort sort (q : IQueryable<PrayerRequest>) =
match sort with
| SortByDate ->
q.OrderByDescending(fun req -> req.UpdatedDate)
.ThenByDescending(fun req -> req.EnteredDate)
.ThenBy (fun req -> req.Requestor)
| SortByRequestor ->
q.OrderBy(fun req -> req.Requestor)
.ThenByDescending(fun req -> req.UpdatedDate)
.ThenByDescending (fun req -> req.EnteredDate)
/// Paginate a prayer request query
let paginate (pageNbr : int) pageSize (q : IQueryable<PrayerRequest>) =
if pageNbr > 0 then q.Skip((pageNbr - 1) * pageSize).Take pageSize else q
open Microsoft.EntityFrameworkCore
open Microsoft.FSharpLu
type AppDbContext with
(*-- DISCONNECTED DATA EXTENSIONS --*)
/// Add an entity entry to the tracked data context with the status of Added
member this.AddEntry<'TEntity when 'TEntity : not struct> (e : 'TEntity) =
this.Entry<'TEntity>(e).State <- EntityState.Added
/// Add an entity entry to the tracked data context with the status of Updated
member this.UpdateEntry<'TEntity when 'TEntity : not struct> (e : 'TEntity) =
this.Entry<'TEntity>(e).State <- EntityState.Modified
/// Add an entity entry to the tracked data context with the status of Deleted
member this.RemoveEntry<'TEntity when 'TEntity : not struct> (e : 'TEntity) =
this.Entry<'TEntity>(e).State <- EntityState.Deleted
(*-- CHURCH EXTENSIONS --*)
/// Find a church by its Id
member this.TryChurchById churchId = backgroundTask {
let! church = this.Churches.SingleOrDefaultAsync (fun ch -> ch.Id = churchId)
return Option.fromObject church
}
/// Find all churches
member this.AllChurches () = backgroundTask {
let! churches = this.Churches.OrderBy(fun ch -> ch.Name).ToListAsync ()
return List.ofSeq churches
}
(*-- MEMBER EXTENSIONS --*)
/// Get a small group member by its Id
member this.TryMemberById memberId = backgroundTask {
let! mbr = this.Members.SingleOrDefaultAsync (fun m -> m.Id = memberId)
return Option.fromObject mbr
}
/// Find all members for a small group
member this.AllMembersForSmallGroup groupId = backgroundTask {
let! members =
this.Members.Where(fun mbr -> mbr.SmallGroupId = groupId)
.OrderBy(fun mbr -> mbr.Name)
.ToListAsync ()
return List.ofSeq members
}
/// Count members for a small group
member this.CountMembersForSmallGroup groupId = backgroundTask {
return! this.Members.CountAsync (fun m -> m.SmallGroupId = groupId)
}
(*-- PRAYER REQUEST EXTENSIONS --*)
/// Get a prayer request by its Id
member this.TryRequestById reqId = backgroundTask {
let! req = this.PrayerRequests.SingleOrDefaultAsync (fun r -> r.Id = reqId)
return Option.fromObject req
}
/// Get all (or active) requests for a small group as of now or the specified date
member this.AllRequestsForSmallGroup (grp : SmallGroup) clock listDate activeOnly pageNbr = backgroundTask {
let theDate = match listDate with Some dt -> dt | _ -> SmallGroup.localDateNow clock grp
let query =
this.PrayerRequests.Where(fun req -> req.SmallGroupId = grp.Id)
|> function
| q when activeOnly ->
let asOf =
(theDate.AtStartOfDayInZone(SmallGroup.timeZone grp) - Duration.FromDays grp.Preferences.DaysToExpire)
.ToInstant ()
q.Where(fun req ->
( req.UpdatedDate > asOf
|| req.Expiration = Manual
|| req.RequestType = LongTermRequest
|| req.RequestType = Expecting)
&& req.Expiration <> Forced)
|> reqSort grp.Preferences.RequestSort
|> paginate pageNbr grp.Preferences.PageSize
| q -> reqSort grp.Preferences.RequestSort q
let! reqs = query.ToListAsync ()
return List.ofSeq reqs
}
/// Count prayer requests for the given small group Id
member this.CountRequestsBySmallGroup groupId = backgroundTask {
return! this.PrayerRequests.CountAsync (fun pr -> pr.SmallGroupId = groupId)
}
/// Count prayer requests for the given church Id
member this.CountRequestsByChurch churchId = backgroundTask {
return! this.PrayerRequests.CountAsync (fun pr -> pr.SmallGroup.ChurchId = churchId)
}
/// Search requests for a small group using the given case-insensitive search term
member this.SearchRequestsForSmallGroup (grp : SmallGroup) (searchTerm : string) pageNbr = backgroundTask {
let sql = """
SELECT * FROM pt.prayer_request WHERE small_group_id = {0} AND request_text ILIKE {1}
UNION
SELECT * FROM pt.prayer_request WHERE small_group_id = {0} AND COALESCE(requestor, '') ILIKE {1}"""
let like = sprintf "%%%s%%"
let query =
this.PrayerRequests.FromSqlRaw (sql, grp.Id.Value, like searchTerm)
|> reqSort grp.Preferences.RequestSort
|> paginate pageNbr grp.Preferences.PageSize
let! reqs = query.ToListAsync ()
return List.ofSeq reqs
}
(*-- SMALL GROUP EXTENSIONS --*)
/// Find a small group by its Id
member this.TryGroupById groupId = backgroundTask {
let! grp =
this.SmallGroups.Include(fun sg -> sg.Preferences)
.SingleOrDefaultAsync (fun sg -> sg.Id = groupId)
return Option.fromObject grp
}
/// Get small groups that are public or password protected
member this.PublicAndProtectedGroups () = backgroundTask {
let! groups =
this.SmallGroups.Include(fun sg -> sg.Preferences).Include(fun sg -> sg.Church)
.Where(fun sg ->
sg.Preferences.IsPublic
|| (sg.Preferences.GroupPassword <> null && sg.Preferences.GroupPassword <> ""))
.OrderBy(fun sg -> sg.Church.Name).ThenBy(fun sg -> sg.Name)
.ToListAsync ()
return List.ofSeq groups
}
/// Get small groups that are password protected
member this.ProtectedGroups () = backgroundTask {
let! groups =
this.SmallGroups.Include(fun sg -> sg.Church)
.Where(fun sg -> sg.Preferences.GroupPassword <> null && sg.Preferences.GroupPassword <> "")
.OrderBy(fun sg -> sg.Church.Name).ThenBy(fun sg -> sg.Name)
.ToListAsync ()
return List.ofSeq groups
}
/// Get all small groups
member this.AllGroups () = backgroundTask {
let! groups =
this.SmallGroups
.Include(fun sg -> sg.Church)
.Include(fun sg -> sg.Preferences)
.Include(fun sg -> sg.Preferences.TimeZone)
.OrderBy(fun sg -> sg.Name)
.ToListAsync ()
return List.ofSeq groups
}
/// Get a small group list by their Id, with their church prepended to their name
member this.GroupList () = backgroundTask {
let! groups =
this.SmallGroups.Include(fun sg -> sg.Church)
.OrderBy(fun sg -> sg.Church.Name).ThenBy(fun sg -> sg.Name)
.ToListAsync ()
return
groups
|> Seq.map (fun sg -> Giraffe.ShortGuid.fromGuid sg.Id.Value, $"{sg.Church.Name} | {sg.Name}")
|> List.ofSeq
}
/// Log on a small group
member this.TryGroupLogOnByPassword groupId pw = backgroundTask {
match! this.TryGroupById groupId with
| Some grp when pw = grp.Preferences.GroupPassword -> return Some grp
| _ -> return None
}
/// Count small groups for the given church Id
member this.CountGroupsByChurch churchId = backgroundTask {
return! this.SmallGroups.CountAsync (fun sg -> sg.ChurchId = churchId)
}
(*-- TIME ZONE EXTENSIONS --*)
/// Get all time zones
member this.AllTimeZones () = backgroundTask {
let! zones = this.TimeZones.OrderBy(fun tz -> tz.SortOrder).ToListAsync ()
return List.ofSeq zones
}
(*-- USER EXTENSIONS --*)
/// Find a user by its Id
member this.TryUserById userId = backgroundTask {
let! usr = this.Users.SingleOrDefaultAsync (fun u -> u.Id = userId)
return Option.fromObject usr
}
/// Find a user by its e-mail address and authorized small group
member this.TryUserByEmailAndGroup email groupId = backgroundTask {
let! usr =
this.Users.SingleOrDefaultAsync (fun u ->
u.Email = email && u.SmallGroups.Any (fun xref -> xref.SmallGroupId = groupId))
return Option.fromObject usr
}
/// Find a user by its Id, eagerly loading the user's groups
member this.TryUserByIdWithGroups userId = backgroundTask {
let! usr = this.Users.Include(fun u -> u.SmallGroups).SingleOrDefaultAsync (fun u -> u.Id = userId)
return Option.fromObject usr
}
/// Get a list of all users
member this.AllUsers () = backgroundTask {
let! users = this.Users.OrderBy(fun u -> u.LastName).ThenBy(fun u -> u.FirstName).ToListAsync ()
return List.ofSeq users
}
/// Get all PrayerTracker users as members (used to send e-mails)
member this.AllUsersAsMembers () = backgroundTask {
let! users = this.AllUsers ()
return users |> List.map (fun u -> { Member.empty with Email = u.Email; Name = u.Name })
}
/// Count the number of users for a small group
member this.CountUsersBySmallGroup groupId = backgroundTask {
return! this.Users.CountAsync (fun u -> u.SmallGroups.Any (fun xref -> xref.SmallGroupId = groupId))
}
/// Count the number of users for a church
member this.CountUsersByChurch churchId = backgroundTask {
return! this.Users.CountAsync (fun u -> u.SmallGroups.Any (fun xref -> xref.SmallGroup.ChurchId = churchId))
}

View File

@ -179,184 +179,7 @@ with
/// The GUID value of the user ID /// The GUID value of the user ID
member this.Value = this |> function UserId guid -> guid member this.Value = this |> function UserId guid -> guid
(*-- SPECIFIC VIEW TYPES --*)
/// EF Core value converters for the discriminated union types above
module Converters =
open Microsoft.EntityFrameworkCore.Storage.ValueConversion
open Microsoft.FSharp.Linq.RuntimeHelpers
open System.Linq.Expressions
let private asOfFromDU =
<@ Func<AsOfDateDisplay, string>(AsOfDateDisplay.toCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<AsOfDateDisplay, string>>>
let private asOfToDU =
<@ Func<string, AsOfDateDisplay>(AsOfDateDisplay.fromCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<string, AsOfDateDisplay>>>
let private churchIdFromDU =
<@ Func<ChurchId, Guid>(fun it -> it.Value) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<ChurchId, Guid>>>
let private churchIdToDU =
<@ Func<Guid, ChurchId>(ChurchId) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<Guid, ChurchId>>>
let private emailFromDU =
<@ Func<EmailFormat, string>(EmailFormat.toCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<EmailFormat, string>>>
let private emailToDU =
<@ Func<string, EmailFormat>(EmailFormat.fromCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<string, EmailFormat>>>
let private emailOptionFromDU =
<@ Func<EmailFormat option, string>(fun opt ->
match opt with Some fmt -> EmailFormat.toCode fmt | None -> null) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<EmailFormat option, string>>>
let private emailOptionToDU =
<@ Func<string, EmailFormat option>(fun opt ->
match opt with "" | null -> None | it -> Some (EmailFormat.fromCode it)) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<string, EmailFormat option>>>
let private expFromDU =
<@ Func<Expiration, string>(Expiration.toCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<Expiration, string>>>
let private expToDU =
<@ Func<string, Expiration>(Expiration.fromCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<string, Expiration>>>
let private memberIdFromDU =
<@ Func<MemberId, Guid>(fun it -> it.Value) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<MemberId, Guid>>>
let private memberIdToDU =
<@ Func<Guid, MemberId>(MemberId) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<Guid, MemberId>>>
let private prayerReqIdFromDU =
<@ Func<PrayerRequestId, Guid>(fun it -> it.Value) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<PrayerRequestId, Guid>>>
let private prayerReqIdToDU =
<@ Func<Guid, PrayerRequestId>(PrayerRequestId) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<Guid, PrayerRequestId>>>
let private smallGrpIdFromDU =
<@ Func<SmallGroupId, Guid>(fun it -> it.Value) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<SmallGroupId, Guid>>>
let private smallGrpIdToDU =
<@ Func<Guid, SmallGroupId>(SmallGroupId) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<Guid, SmallGroupId>>>
let private sortFromDU =
<@ Func<RequestSort, string>(RequestSort.toCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<RequestSort, string>>>
let private sortToDU =
<@ Func<string, RequestSort>(RequestSort.fromCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<string, RequestSort>>>
let private typFromDU =
<@ Func<PrayerRequestType, string>(PrayerRequestType.toCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<PrayerRequestType, string>>>
let private typToDU =
<@ Func<string, PrayerRequestType>(PrayerRequestType.fromCode) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<string, PrayerRequestType>>>
let private tzIdFromDU =
<@ Func<TimeZoneId, string>(TimeZoneId.toString) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<TimeZoneId, string>>>
let private tzIdToDU =
<@ Func<string, TimeZoneId>(TimeZoneId) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<string, TimeZoneId>>>
let private userIdFromDU =
<@ Func<UserId, Guid>(fun it -> it.Value) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<UserId, Guid>>>
let private userIdToDU =
<@ Func<Guid, UserId>(UserId) @>
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<Guid, UserId>>>
/// Conversion between a string and an AsOfDateDisplay DU value
type AsOfDateDisplayConverter () =
inherit ValueConverter<AsOfDateDisplay, string> (asOfFromDU, asOfToDU)
/// Conversion between a GUID and a church ID
type ChurchIdConverter () =
inherit ValueConverter<ChurchId, Guid> (churchIdFromDU, churchIdToDU)
/// Conversion between a string and an EmailFormat DU value
type EmailFormatConverter () =
inherit ValueConverter<EmailFormat, string> (emailFromDU, emailToDU)
/// Conversion between a string an an optional EmailFormat DU value
type EmailFormatOptionConverter () =
inherit ValueConverter<EmailFormat option, string> (emailOptionFromDU, emailOptionToDU)
/// Conversion between a string and an Expiration DU value
type ExpirationConverter () =
inherit ValueConverter<Expiration, string> (expFromDU, expToDU)
/// Conversion between a GUID and a member ID
type MemberIdConverter () =
inherit ValueConverter<MemberId, Guid> (memberIdFromDU, memberIdToDU)
/// Conversion between a GUID and a prayer request ID
type PrayerRequestIdConverter () =
inherit ValueConverter<PrayerRequestId, Guid> (prayerReqIdFromDU, prayerReqIdToDU)
/// Conversion between a string and a PrayerRequestType DU value
type PrayerRequestTypeConverter () =
inherit ValueConverter<PrayerRequestType, string> (typFromDU, typToDU)
/// Conversion between a string and a RequestSort DU value
type RequestSortConverter () =
inherit ValueConverter<RequestSort, string> (sortFromDU, sortToDU)
/// Conversion between a GUID and a small group ID
type SmallGroupIdConverter () =
inherit ValueConverter<SmallGroupId, Guid> (smallGrpIdFromDU, smallGrpIdToDU)
/// Conversion between a string and a time zone ID
type TimeZoneIdConverter () =
inherit ValueConverter<TimeZoneId, string> (tzIdFromDU, tzIdToDU)
/// Conversion between a GUID and a user ID
type UserIdConverter () =
inherit ValueConverter<UserId, Guid> (userIdFromDU, userIdToDU)
/// Statistics for churches /// Statistics for churches
[<NoComparison; NoEquality>] [<NoComparison; NoEquality>]
@ -371,14 +194,33 @@ type ChurchStats =
Users : int Users : int
} }
/// Information needed to display the public/protected request list and small group maintenance pages
[<NoComparison; NoEquality>]
type SmallGroupInfo =
{ /// The ID of the small group
Id : string
/// The name of the small group
Name : string
/// The name of the church to which the small group belongs
ChurchName : string
/// The ID of the time zone for the small group
TimeZoneId : TimeZoneId
/// Whether the small group has a publicly-available request list
IsPublic : bool
}
(*-- ENTITIES --*) (*-- ENTITIES --*)
open FSharp.EFCore.OptionConverter
open Microsoft.EntityFrameworkCore
open NodaTime open NodaTime
/// This represents a church /// This represents a church
type [<CLIMutable; NoComparison; NoEquality>] Church = [<NoComparison; NoEquality>]
type Church =
{ /// The ID of this church { /// The ID of this church
Id : ChurchId Id : ChurchId
@ -397,10 +239,13 @@ type [<CLIMutable; NoComparison; NoEquality>] Church =
/// The address for the interface /// The address for the interface
InterfaceAddress : string option InterfaceAddress : string option
} }
with
/// Functions to support churches
module Church =
/// An empty church /// An empty church
// aww... how sad :( // aww... how sad :(
static member empty = let empty =
{ Id = ChurchId Guid.Empty { Id = ChurchId Guid.Empty
Name = "" Name = ""
City = "" City = ""
@ -409,27 +254,10 @@ with
InterfaceAddress = None InterfaceAddress = None
} }
/// Configure EF for this entity
static member internal ConfigureEF (mb : ModelBuilder) =
mb.Entity<Church> (fun it ->
seq<obj> {
it.ToTable "church"
it.Property(fun c -> c.Id).HasColumnName "id"
it.Property(fun c -> c.Name).HasColumnName("church_name").IsRequired ()
it.Property(fun c -> c.City).HasColumnName("city").IsRequired ()
it.Property(fun c -> c.State).HasColumnName("state").IsRequired().HasMaxLength 2
it.Property(fun c -> c.HasVpsInterface).HasColumnName "has_vps_interface"
it.Property(fun c -> c.InterfaceAddress).HasColumnName "interface_address"
} |> List.ofSeq |> ignore)
|> ignore
mb.Model.FindEntityType(typeof<Church>).FindProperty(nameof Church.empty.Id)
.SetValueConverter (Converters.ChurchIdConverter ())
mb.Model.FindEntityType(typeof<Church>).FindProperty(nameof Church.empty.InterfaceAddress)
.SetValueConverter (OptionConverter<string> ())
/// Preferences for the form and format of the prayer request list /// Preferences for the form and format of the prayer request list
and [<CLIMutable; NoComparison; NoEquality>] ListPreferences = [<NoComparison; NoEquality>]
type ListPreferences =
{ /// The Id of the small group to which these preferences belong { /// The Id of the small group to which these preferences belong
SmallGroupId : SmallGroupId SmallGroupId : SmallGroupId
@ -478,19 +306,18 @@ and [<CLIMutable; NoComparison; NoEquality>] ListPreferences =
/// The time zone which this class uses (use tzdata names) /// The time zone which this class uses (use tzdata names)
TimeZoneId : TimeZoneId TimeZoneId : TimeZoneId
/// The time zone information
TimeZone : TimeZone
/// The number of requests displayed per page /// The number of requests displayed per page
PageSize : int PageSize : int
/// How the as-of date should be automatically displayed /// How the as-of date should be automatically displayed
AsOfDateDisplay : AsOfDateDisplay AsOfDateDisplay : AsOfDateDisplay
} }
with
/// Functions to support list preferences
module ListPreferences =
/// A set of preferences with their default values /// A set of preferences with their default values
static member empty = let empty =
{ SmallGroupId = SmallGroupId Guid.Empty { SmallGroupId = SmallGroupId Guid.Empty
DaysToExpire = 14 DaysToExpire = 14
DaysToKeepNew = 7 DaysToKeepNew = 7
@ -507,61 +334,14 @@ with
DefaultEmailType = HtmlFormat DefaultEmailType = HtmlFormat
IsPublic = false IsPublic = false
TimeZoneId = TimeZoneId "America/Denver" TimeZoneId = TimeZoneId "America/Denver"
TimeZone = TimeZone.empty
PageSize = 100 PageSize = 100
AsOfDateDisplay = NoDisplay AsOfDateDisplay = NoDisplay
} }
/// Configure EF for this entity
static member internal ConfigureEF (mb : ModelBuilder) =
mb.Entity<ListPreferences> (fun it ->
seq<obj> {
it.ToTable "list_preference"
it.HasKey (fun lp -> lp.SmallGroupId :> obj)
it.Property(fun lp -> lp.SmallGroupId).HasColumnName "small_group_id"
it.Property(fun lp -> lp.DaysToKeepNew).HasColumnName("days_to_keep_new").IsRequired().HasDefaultValue 7
it.Property(fun lp -> lp.DaysToExpire).HasColumnName("days_to_expire").IsRequired().HasDefaultValue 14
it.Property(fun lp -> lp.LongTermUpdateWeeks).HasColumnName("long_term_update_weeks").IsRequired()
.HasDefaultValue 4
it.Property(fun lp -> lp.EmailFromName).HasColumnName("email_from_name").IsRequired()
.HasDefaultValue "PrayerTracker"
it.Property(fun lp -> lp.EmailFromAddress).HasColumnName("email_from_address").IsRequired()
.HasDefaultValue "prayer@djs-consulting.com"
it.Property(fun lp -> lp.Fonts).HasColumnName("fonts").IsRequired()
.HasDefaultValue "Century Gothic,Tahoma,Luxi Sans,sans-serif"
it.Property(fun lp -> lp.HeadingColor).HasColumnName("heading_color").IsRequired()
.HasDefaultValue "maroon"
it.Property(fun lp -> lp.LineColor).HasColumnName("line_color").IsRequired().HasDefaultValue "navy"
it.Property(fun lp -> lp.HeadingFontSize).HasColumnName("heading_font_size").IsRequired()
.HasDefaultValue 16
it.Property(fun lp -> lp.TextFontSize).HasColumnName("text_font_size").IsRequired().HasDefaultValue 12
it.Property(fun lp -> lp.RequestSort).HasColumnName("request_sort").IsRequired().HasMaxLength(1)
.HasDefaultValue SortByDate
it.Property(fun lp -> lp.GroupPassword).HasColumnName("group_password").IsRequired().HasDefaultValue ""
it.Property(fun lp -> lp.DefaultEmailType).HasColumnName("default_email_type").IsRequired()
.HasDefaultValue HtmlFormat
it.Property(fun lp -> lp.IsPublic).HasColumnName("is_public").IsRequired().HasDefaultValue false
it.Property(fun lp -> lp.TimeZoneId).HasColumnName("time_zone_id").IsRequired()
.HasDefaultValue (TimeZoneId "America/Denver")
it.Property(fun lp -> lp.PageSize).HasColumnName("page_size").IsRequired().HasDefaultValue 100
it.Property(fun lp -> lp.AsOfDateDisplay).HasColumnName("as_of_date_display").IsRequired()
.HasMaxLength(1).HasDefaultValue NoDisplay
} |> List.ofSeq |> ignore)
|> ignore
mb.Model.FindEntityType(typeof<ListPreferences>).FindProperty(nameof ListPreferences.empty.SmallGroupId)
.SetValueConverter (Converters.SmallGroupIdConverter ())
mb.Model.FindEntityType(typeof<ListPreferences>).FindProperty(nameof ListPreferences.empty.RequestSort)
.SetValueConverter (Converters.RequestSortConverter ())
mb.Model.FindEntityType(typeof<ListPreferences>).FindProperty(nameof ListPreferences.empty.DefaultEmailType)
.SetValueConverter (Converters.EmailFormatConverter ())
mb.Model.FindEntityType(typeof<ListPreferences>).FindProperty(nameof ListPreferences.empty.TimeZoneId)
.SetValueConverter (Converters.TimeZoneIdConverter ())
mb.Model.FindEntityType(typeof<ListPreferences>).FindProperty(nameof ListPreferences.empty.AsOfDateDisplay)
.SetValueConverter (Converters.AsOfDateDisplayConverter ())
/// A member of a small group /// A member of a small group
and [<CLIMutable; NoComparison; NoEquality>] Member = [<NoComparison; NoEquality>]
type Member =
{ /// The ID of the small group member { /// The ID of the small group member
Id : MemberId Id : MemberId
@ -576,44 +356,24 @@ and [<CLIMutable; NoComparison; NoEquality>] Member =
/// The type of e-mail preferred by this member /// The type of e-mail preferred by this member
Format : EmailFormat option Format : EmailFormat option
/// The small group to which this member belongs
SmallGroup : SmallGroup
} }
with
/// Functions to support small group members
module Member =
/// An empty member /// An empty member
static member empty = let empty =
{ Id = MemberId Guid.Empty { Id = MemberId Guid.Empty
SmallGroupId = SmallGroupId Guid.Empty SmallGroupId = SmallGroupId Guid.Empty
Name = "" Name = ""
Email = "" Email = ""
Format = None Format = None
SmallGroup = SmallGroup.empty
} }
/// Configure EF for this entity
static member internal ConfigureEF (mb : ModelBuilder) =
mb.Entity<Member> (fun it ->
seq<obj> {
it.ToTable "member"
it.Property(fun m -> m.Id).HasColumnName "id"
it.Property(fun m -> m.SmallGroupId).HasColumnName("small_group_id").IsRequired ()
it.Property(fun m -> m.Name).HasColumnName("member_name").IsRequired ()
it.Property(fun m -> m.Email).HasColumnName("email").IsRequired ()
it.Property(fun m -> m.Format).HasColumnName "email_format"
} |> List.ofSeq |> ignore)
|> ignore
mb.Model.FindEntityType(typeof<Member>).FindProperty(nameof Member.empty.Id)
.SetValueConverter (Converters.MemberIdConverter ())
mb.Model.FindEntityType(typeof<Member>).FindProperty(nameof Member.empty.SmallGroupId)
.SetValueConverter (Converters.SmallGroupIdConverter ())
mb.Model.FindEntityType(typeof<Member>).FindProperty(nameof Member.empty.Format)
.SetValueConverter (Converters.EmailFormatOptionConverter ())
/// This represents a single prayer request /// This represents a single prayer request
and [<CLIMutable; NoComparison; NoEquality>] PrayerRequest = [<NoComparison; NoEquality>]
type PrayerRequest =
{ /// The ID of this request { /// The ID of this request
Id : PrayerRequestId Id : PrayerRequestId
@ -641,66 +401,15 @@ and [<CLIMutable; NoComparison; NoEquality>] PrayerRequest =
/// Whether the chaplain should be notified for this request /// Whether the chaplain should be notified for this request
NotifyChaplain : bool NotifyChaplain : bool
/// The user who entered this request
User : User
/// The small group to which this request belongs
SmallGroup : SmallGroup
/// Is this request expired? /// Is this request expired?
Expiration : Expiration Expiration : Expiration
} }
with // functions are below small group functions
/// An empty request
static member empty =
{ Id = PrayerRequestId Guid.Empty
RequestType = CurrentRequest
UserId = UserId Guid.Empty
SmallGroupId = SmallGroupId Guid.Empty
EnteredDate = Instant.MinValue
UpdatedDate = Instant.MinValue
Requestor = None
Text = ""
NotifyChaplain = false
User = User.empty
SmallGroup = SmallGroup.empty
Expiration = Automatic
}
/// Configure EF for this entity
static member internal ConfigureEF (mb : ModelBuilder) =
mb.Entity<PrayerRequest> (fun it ->
seq<obj> {
it.ToTable "prayer_request"
it.Property(fun pr -> pr.Id).HasColumnName "id"
it.Property(fun pr -> pr.RequestType).HasColumnName("request_type").IsRequired ()
it.Property(fun pr -> pr.UserId).HasColumnName "user_id"
it.Property(fun pr -> pr.SmallGroupId).HasColumnName "small_group_id"
it.Property(fun pr -> pr.EnteredDate).HasColumnName "entered_date"
it.Property(fun pr -> pr.UpdatedDate).HasColumnName "updated_date"
it.Property(fun pr -> pr.Requestor).HasColumnName "requestor"
it.Property(fun pr -> pr.Text).HasColumnName("request_text").IsRequired ()
it.Property(fun pr -> pr.NotifyChaplain).HasColumnName "notify_chaplain"
it.Property(fun pr -> pr.Expiration).HasColumnName "expiration"
} |> List.ofSeq |> ignore)
|> ignore
mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty(nameof PrayerRequest.empty.Id)
.SetValueConverter (Converters.PrayerRequestIdConverter ())
mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty(nameof PrayerRequest.empty.RequestType)
.SetValueConverter (Converters.PrayerRequestTypeConverter ())
mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty(nameof PrayerRequest.empty.UserId)
.SetValueConverter (Converters.UserIdConverter ())
mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty(nameof PrayerRequest.empty.SmallGroupId)
.SetValueConverter (Converters.SmallGroupIdConverter ())
mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty(nameof PrayerRequest.empty.Requestor)
.SetValueConverter (OptionConverter<string> ())
mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty(nameof PrayerRequest.empty.Expiration)
.SetValueConverter (Converters.ExpirationConverter ())
/// This represents a small group (Sunday School class, Bible study group, etc.) /// This represents a small group (Sunday School class, Bible study group, etc.)
and [<CLIMutable; NoComparison; NoEquality>] SmallGroup = [<NoComparison; NoEquality>]
type SmallGroup =
{ /// The ID of this small group { /// The ID of this small group
Id : SmallGroupId Id : SmallGroupId
@ -710,215 +419,21 @@ and [<CLIMutable; NoComparison; NoEquality>] SmallGroup =
/// The name of the group /// The name of the group
Name : string Name : string
/// The church to which this small group belongs
Church : Church
/// The preferences for the request list /// The preferences for the request list
Preferences : ListPreferences Preferences : ListPreferences
/// The members of the group
Members : ResizeArray<Member>
/// Prayer requests for this small group
PrayerRequests : ResizeArray<PrayerRequest>
/// The users authorized to manage this group
Users : ResizeArray<UserSmallGroup>
} }
with
/// Functions to support small groups
module SmallGroup =
/// An empty small group /// An empty small group
static member empty = let empty =
{ Id = SmallGroupId Guid.Empty { Id = SmallGroupId Guid.Empty
ChurchId = ChurchId Guid.Empty ChurchId = ChurchId Guid.Empty
Name = "" Name = ""
Church = Church.empty Preferences = ListPreferences.empty
Preferences = ListPreferences.empty
Members = ResizeArray<Member> ()
PrayerRequests = ResizeArray<PrayerRequest> ()
Users = ResizeArray<UserSmallGroup> ()
} }
/// Configure EF for this entity
static member internal ConfigureEF (mb : ModelBuilder) =
mb.Entity<SmallGroup> (fun it ->
seq<obj> {
it.ToTable "small_group"
it.Property(fun sg -> sg.Id).HasColumnName "id"
it.Property(fun sg -> sg.ChurchId).HasColumnName "church_id"
it.Property(fun sg -> sg.Name).HasColumnName("group_name").IsRequired ()
it.HasOne(fun sg -> sg.Preferences)
.WithOne()
.HasPrincipalKey(fun sg -> sg.Id :> obj)
.HasForeignKey(fun (lp : ListPreferences) -> lp.SmallGroupId :> obj)
} |> List.ofSeq |> ignore)
|> ignore
mb.Model.FindEntityType(typeof<SmallGroup>).FindProperty(nameof SmallGroup.empty.Id)
.SetValueConverter (Converters.SmallGroupIdConverter ())
mb.Model.FindEntityType(typeof<SmallGroup>).FindProperty(nameof SmallGroup.empty.ChurchId)
.SetValueConverter (Converters.ChurchIdConverter ())
/// This represents a time zone in which a class may reside
and [<CLIMutable; NoComparison; NoEquality>] TimeZone =
{ /// The Id for this time zone (uses tzdata names)
Id : TimeZoneId
/// The description of this time zone
Description : string
/// The order in which this timezone should be displayed
SortOrder : int
/// Whether this timezone is active
IsActive : bool
}
with
/// An empty time zone
static member empty =
{ Id = TimeZoneId ""
Description = ""
SortOrder = 0
IsActive = false
}
/// Configure EF for this entity
static member internal ConfigureEF (mb : ModelBuilder) =
mb.Entity<TimeZone> (fun it ->
seq<obj> {
it.ToTable "time_zone"
it.Property(fun tz -> tz.Id).HasColumnName "id"
it.Property(fun tz -> tz.Description).HasColumnName("description").IsRequired ()
it.Property(fun tz -> tz.SortOrder).HasColumnName "sort_order"
it.Property(fun tz -> tz.IsActive).HasColumnName "is_active"
} |> List.ofSeq |> ignore)
|> ignore
mb.Model.FindEntityType(typeof<TimeZone>).FindProperty(nameof TimeZone.empty.Id)
.SetValueConverter (Converters.TimeZoneIdConverter ())
/// This represents a user of PrayerTracker
and [<CLIMutable; NoComparison; NoEquality>] User =
{ /// The ID of this user
Id : UserId
/// The first name of this user
FirstName : string
/// The last name of this user
LastName : string
/// The e-mail address of the user
Email : string
/// Whether this user is a PrayerTracker system administrator
IsAdmin : bool
/// The user's hashed password
PasswordHash : string
/// The salt for the user's hashed password
Salt : Guid option
/// The last time the user was seen (set whenever the user is loaded into a session)
LastSeen : Instant option
/// The small groups which this user is authorized
SmallGroups : ResizeArray<UserSmallGroup>
}
with
/// An empty user
static member empty =
{ Id = UserId Guid.Empty
FirstName = ""
LastName = ""
Email = ""
IsAdmin = false
PasswordHash = ""
Salt = None
LastSeen = None
SmallGroups = ResizeArray<UserSmallGroup> ()
}
/// The full name of the user
member this.Name =
$"{this.FirstName} {this.LastName}"
/// Configure EF for this entity
static member internal ConfigureEF (mb : ModelBuilder) =
mb.Entity<User> (fun it ->
seq<obj> {
it.ToTable "pt_user"
it.Ignore(fun u -> u.Name :> obj)
it.Property(fun u -> u.Id).HasColumnName "id"
it.Property(fun u -> u.FirstName).HasColumnName("first_name").IsRequired ()
it.Property(fun u -> u.LastName).HasColumnName("last_name").IsRequired ()
it.Property(fun u -> u.Email).HasColumnName("email").IsRequired ()
it.Property(fun u -> u.IsAdmin).HasColumnName "is_admin"
it.Property(fun u -> u.PasswordHash).HasColumnName("password_hash").IsRequired ()
it.Property(fun u -> u.Salt).HasColumnName "salt"
it.Property(fun u -> u.LastSeen).HasColumnName "last_seen"
} |> List.ofSeq |> ignore)
|> ignore
mb.Model.FindEntityType(typeof<User>).FindProperty(nameof User.empty.Id)
.SetValueConverter (Converters.UserIdConverter ())
mb.Model.FindEntityType(typeof<User>).FindProperty(nameof User.empty.Salt)
.SetValueConverter (OptionConverter<Guid> ())
mb.Model.FindEntityType(typeof<User>).FindProperty(nameof User.empty.LastSeen)
.SetValueConverter (OptionConverter<Instant> ())
/// Cross-reference between user and small group
and [<CLIMutable; NoComparison; NoEquality>] 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
/// The user who has access to the small group
User : User
/// The small group to which the user has access
SmallGroup : SmallGroup
}
with
/// An empty user/small group xref
static member empty =
{ UserId = UserId Guid.Empty
SmallGroupId = SmallGroupId Guid.Empty
User = User.empty
SmallGroup = SmallGroup.empty
}
/// Configure EF for this entity
static member internal ConfigureEF (mb : ModelBuilder) =
mb.Entity<UserSmallGroup> (fun it ->
seq<obj> {
it.ToTable "user_small_group"
it.HasKey (nameof UserSmallGroup.empty.UserId, nameof UserSmallGroup.empty.SmallGroupId)
it.Property(fun usg -> usg.UserId).HasColumnName "user_id"
it.Property(fun usg -> usg.SmallGroupId).HasColumnName "small_group_id"
it.HasOne(fun usg -> usg.User)
.WithMany(fun u -> u.SmallGroups :> seq<UserSmallGroup>)
.HasForeignKey(fun usg -> usg.UserId :> obj)
it.HasOne(fun usg -> usg.SmallGroup)
.WithMany(fun sg -> sg.Users :> seq<UserSmallGroup>)
.HasForeignKey(fun usg -> usg.SmallGroupId :> obj)
} |> List.ofSeq |> ignore)
|> ignore
mb.Model.FindEntityType(typeof<UserSmallGroup>).FindProperty(nameof UserSmallGroup.empty.UserId)
.SetValueConverter (Converters.UserIdConverter ())
mb.Model.FindEntityType(typeof<UserSmallGroup>).FindProperty(nameof UserSmallGroup.empty.SmallGroupId)
.SetValueConverter (Converters.SmallGroupIdConverter ())
/// Support functions for small groups
module SmallGroup =
/// The DateTimeZone for the time zone ID for this small group /// The DateTimeZone for the time zone ID for this small group
let timeZone group = let timeZone group =
let tzId = TimeZoneId.toString group.Preferences.TimeZoneId let tzId = TimeZoneId.toString group.Preferences.TimeZoneId
@ -935,10 +450,23 @@ module SmallGroup =
(localTimeNow clock group).Date (localTimeNow clock group).Date
/// Functions to support prayer requests
/// Support functions for prayer requests
module PrayerRequest = module PrayerRequest =
/// An empty request
let empty =
{ Id = PrayerRequestId Guid.Empty
RequestType = CurrentRequest
UserId = UserId Guid.Empty
SmallGroupId = SmallGroupId Guid.Empty
EnteredDate = Instant.MinValue
UpdatedDate = Instant.MinValue
Requestor = None
Text = ""
NotifyChaplain = false
Expiration = Automatic
}
/// Is this request expired? /// Is this request expired?
let isExpired (asOf : LocalDate) group req = let isExpired (asOf : LocalDate) group req =
match req.Expiration, req.RequestType with match req.Expiration, req.RequestType with
@ -958,21 +486,65 @@ module PrayerRequest =
>= req.UpdatedDate.InZone(SmallGroup.timeZone group).Date >= req.UpdatedDate.InZone(SmallGroup.timeZone group).Date
/// Information needed to display the public/protected request list and small group maintenance pages /// This represents a user of PrayerTracker
[<NoComparison; NoEquality>] [<NoComparison; NoEquality>]
type SmallGroupInfo = type User =
{ /// The ID of the small group { /// The ID of this user
Id : string Id : UserId
/// The name of the small group /// The first name of this user
Name : string FirstName : string
/// The name of the church to which the small group belongs /// The last name of this user
ChurchName : string LastName : string
/// The ID of the time zone for the small group /// The e-mail address of the user
TimeZoneId : TimeZoneId Email : string
/// Whether the small group has a publicly-available request list /// Whether this user is a PrayerTracker system administrator
IsPublic : bool IsAdmin : bool
/// The user's hashed password
PasswordHash : string
/// The last time the user was seen (set whenever the user is loaded into a session)
LastSeen : Instant option
} }
with
/// The full name of the user
member this.Name =
$"{this.FirstName} {this.LastName}"
/// Functions to support users
module User =
/// An empty user
let empty =
{ Id = UserId Guid.Empty
FirstName = ""
LastName = ""
Email = ""
IsAdmin = false
PasswordHash = ""
LastSeen = None
}
/// 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
}
/// Functions to support user/small group cross-reference
module UserSmallGroup =
/// An empty user/small group xref
let empty =
{ UserId = UserId Guid.Empty
SmallGroupId = SmallGroupId Guid.Empty
}

View File

@ -1,391 +0,0 @@
namespace PrayerTracker.Migrations
open System
open Microsoft.EntityFrameworkCore
open Microsoft.EntityFrameworkCore.Infrastructure
open Microsoft.EntityFrameworkCore.Migrations
open Npgsql.EntityFrameworkCore.PostgreSQL.Metadata
open PrayerTracker
open PrayerTracker.Entities
[<DbContext (typeof<AppDbContext>)>]
[<Migration "20161217153124_InitialDatabase">]
type InitialDatabase () =
inherit Migration ()
override _.Up (migrationBuilder : MigrationBuilder) =
migrationBuilder.EnsureSchema (name = "pt")
|> ignore
migrationBuilder.CreateTable (
name = "church",
schema = "pt",
columns = (fun table ->
{| Id = table.Column<Guid> (name = "id", nullable = false)
City = table.Column<string> (name = "city", nullable = false)
HasVpsInterface = table.Column<bool> (name = "has_vps_interface", nullable = false)
InterfaceAddress = table.Column<string> (name = "interface_address", nullable = true)
Name = table.Column<string> (name = "church_Name", nullable = false)
State = table.Column<string> (name = "state", nullable = false, maxLength = Nullable<int> 2)
|}),
constraints = fun table ->
table.PrimaryKey ("pk_church", fun x -> upcast x.Id) |> ignore)
|> ignore
migrationBuilder.CreateTable (
name = "time_zone",
schema = "pt",
columns = (fun table ->
{| Id = table.Column<string> (name = "id", nullable = false)
Description = table.Column<string> (name = "description", nullable = false)
IsActive = table.Column<bool> (name = "is_active", nullable = false)
SortOrder = table.Column<int> (name = "sort_order", nullable = false)
|}),
constraints = fun table ->
table.PrimaryKey ("pk_time_zone", fun x -> upcast x.Id) |> ignore)
|> ignore
migrationBuilder.CreateTable (
name = "pt_user",
schema = "pt",
columns = (fun table ->
{| Id = table.Column<Guid> (name = "id", nullable = false)
Email = table.Column<string> (name = "email", nullable = false)
FirstName = table.Column<string> (name = "first_name", nullable = false)
IsAdmin = table.Column<bool> (name = "is_admin", nullable = false)
LastName = table.Column<string> (name = "last_name", nullable = false)
PasswordHash = table.Column<string> (name = "password_hash", nullable = false)
Salt = table.Column<Guid> (name = "salt", nullable = true)
LastSeen = table.Column<DateTime> (name = "last_seen", nullable = true)
|}),
constraints = fun table ->
table.PrimaryKey("pk_pt_user", fun x -> upcast x.Id) |> ignore)
|> ignore
migrationBuilder.CreateTable (
name = "small_group",
schema = "pt",
columns = (fun table ->
{| Id = table.Column<Guid> (name = "id", nullable = false)
ChurchId = table.Column<Guid> (name = "church_id", nullable = false)
Name = table.Column<string> (name = "group_name", nullable = false)
|}),
constraints = fun table ->
table.PrimaryKey ("pk_small_group", fun x -> upcast x.Id) |> ignore
table.ForeignKey (
name = "fk_small_group_church_id",
column = (fun x -> upcast x.ChurchId),
principalSchema = "pt",
principalTable = "church",
principalColumn = "id",
onDelete = ReferentialAction.Cascade)
|> ignore)
|> ignore
migrationBuilder.CreateTable (
name = "list_preference",
schema = "pt",
columns = (fun table ->
{| SmallGroupId = table.Column<Guid> (name = "small_group_id", nullable = false)
AsOfDateDisplay = table.Column<string> (name = "as_of_date_display", nullable = false, defaultValue = "N", maxLength = Nullable<int> 1)
DaysToExpire = table.Column<int> (name = "days_to_expire", nullable = false, defaultValue = 14)
DaysToKeepNew = table.Column<int> (name = "days_to_keep_new", nullable = false, defaultValue = 7)
DefaultEmailType = table.Column<string> (name = "default_email_type", nullable = false, defaultValue = "Html")
EmailFromAddress = table.Column<string> (name = "email_from_address", nullable = false, defaultValue = "prayer@djs-consulting.com")
EmailFromName = table.Column<string> (name = "email_from_name", nullable = false, defaultValue = "PrayerTracker")
Fonts = table.Column<string> (name = "fonts", nullable = false, defaultValue = "Century Gothic,Tahoma,Luxi Sans,sans-serif")
GroupPassword = table.Column<string> (name = "group_password", nullable = false, defaultValue = "")
HeadingColor = table.Column<string> (name = "heading_color", nullable = false, defaultValue = "maroon")
HeadingFontSize = table.Column<int> (name = "heading_font_size", nullable = false, defaultValue = 16)
IsPublic = table.Column<bool> (name = "is_public", nullable = false, defaultValue = false)
LineColor = table.Column<string> (name = "line_color", nullable = false, defaultValue = "navy")
LongTermUpdateWeeks = table.Column<int> (name = "long_term_update_weeks", nullable = false, defaultValue = 4)
PageSize = table.Column<int> (name = "page_size", nullable = false, defaultValue = 100)
RequestSort = table.Column<string> (name = "request_sort", nullable = false, defaultValue = "D", maxLength = Nullable<int> 1)
TextFontSize = table.Column<int> (name = "text_font_size", nullable = false, defaultValue = 12)
TimeZoneId = table.Column<string> (name = "time_zone_id", nullable = false, defaultValue = "America/Denver")
|}),
constraints = fun table ->
table.PrimaryKey ("pk_list_preference", fun x -> upcast x.SmallGroupId) |> ignore
table.ForeignKey (
name = "fk_list_preference_small_group_id",
column = (fun x -> upcast x.SmallGroupId),
principalSchema = "pt",
principalTable = "small_group",
principalColumn = "id",
onDelete = ReferentialAction.Cascade)
|> ignore
table.ForeignKey (
name = "fk_list_preference_time_zone_id",
column = (fun x -> upcast x.TimeZoneId),
principalSchema = "pt",
principalTable = "time_zone",
principalColumn = "id",
onDelete = ReferentialAction.Cascade)
|> ignore)
|> ignore
migrationBuilder.CreateTable (
name = "member",
schema = "pt",
columns = (fun table ->
{| Id = table.Column<Guid> (name = "id", nullable = false)
Email = table.Column<string> (name = "email", nullable = false)
Format = table.Column<string> (name = "email_format", nullable = true)
Name = table.Column<string> (name = "member_name", nullable = false)
SmallGroupId = table.Column<Guid> (name = "small_group_id", nullable = false)
|}),
constraints = fun table ->
table.PrimaryKey ("pk_member", fun x -> upcast x.Id) |> ignore
table.ForeignKey (
name = "fk_member_small_group_id",
column = (fun x -> upcast x.SmallGroupId),
principalSchema = "pt",
principalTable = "small_group",
principalColumn = "id",
onDelete = ReferentialAction.Cascade)
|> ignore)
|> ignore
migrationBuilder.CreateTable (
name = "prayer_request",
schema = "pt",
columns = (fun table ->
{| Id = table.Column<Guid> (name = "id", nullable = false)
Expiration = table.Column<bool> (name = "expiration", nullable = false)
EnteredDate = table.Column<DateTime> (name = "entered_date", nullable = false)
NotifyChaplain = table.Column<bool> (name = "notify_chaplain", nullable = false)
RequestType = table.Column<string> (name = "request_type", nullable = false)
Requestor = table.Column<string> (name = "requestor", nullable = true)
SmallGroupId = table.Column<Guid> (name = "small_group_id", nullable = false)
Text = table.Column<string> (name = "request_text", nullable = false)
UpdatedDate = table.Column<DateTime> (name = "updated_date", nullable = false)
UserId = table.Column<Guid> (name = "user_id", nullable = false)
|}),
constraints = fun table ->
table.PrimaryKey ("pk_prayer_request", fun x -> upcast x.Id) |> ignore
table.ForeignKey (
name = "fk_prayer_request_small_group_id",
column = (fun x -> upcast x.SmallGroupId),
principalSchema = "pt",
principalTable = "small_group",
principalColumn = "i",
onDelete = ReferentialAction.Cascade)
|> ignore
table.ForeignKey (
name = "fk_prayer_request_user_id",
column = (fun x -> upcast x.UserId),
principalSchema = "pt",
principalTable = "pt_user",
principalColumn = "id",
onDelete = ReferentialAction.Cascade)
|> ignore)
|> ignore
migrationBuilder.CreateTable(
name = "user_small_group",
schema = "pt",
columns = (fun table ->
{| UserId = table.Column<Guid> (name = "user_id", nullable = false)
SmallGroupId = table.Column<Guid> (name = "small_group_id", nullable = false)
|}),
constraints = fun table ->
table.PrimaryKey ("pk_user_small_group", fun x -> upcast x) |> ignore
table.ForeignKey (
name = "fk_user_small_group_small_group_id",
column = (fun x -> upcast x.SmallGroupId),
principalSchema = "pt",
principalTable = "small_group",
principalColumn = "id",
onDelete = ReferentialAction.Cascade)
|> ignore
table.ForeignKey (
name = "fk_user_small_group_user_id",
column = (fun x -> upcast x.UserId),
principalSchema = "pt",
principalTable = "pt_user",
principalColumn = "id",
onDelete = ReferentialAction.Cascade)
|> ignore)
|> ignore
migrationBuilder.CreateIndex (name = "ix_list_preference_time_zone_id", schema = "pt", table = "list_preference", column = "time_zone_id") |> ignore
migrationBuilder.CreateIndex (name = "ix_member_small_group_id", schema = "pt", table = "member", column = "small_group_id") |> ignore
migrationBuilder.CreateIndex (name = "ix_prayer_request_small_group_id", schema = "pt", table = "prayer_request", column = "small_group_id") |> ignore
migrationBuilder.CreateIndex (name = "ix_prayer_request_user_id", schema = "pt", table = "prayer_request", column = "user_id") |> ignore
migrationBuilder.CreateIndex (name = "ix_small_group_church_id", schema = "pt", table = "small_group", column = "church_id") |> ignore
migrationBuilder.CreateIndex (name = "ix_user_small_group_small_group_id", schema = "pt", table = "user_small_group", column = "small_group_id") |> ignore
override _.Down (migrationBuilder : MigrationBuilder) =
migrationBuilder.DropTable (name = "list_preference", schema = "pt") |> ignore
migrationBuilder.DropTable (name = "member", schema = "pt") |> ignore
migrationBuilder.DropTable (name = "prayer_request", schema = "pt") |> ignore
migrationBuilder.DropTable (name = "user_small_group", schema = "pt") |> ignore
migrationBuilder.DropTable (name = "time_zone", schema = "pt") |> ignore
migrationBuilder.DropTable (name = "small_group", schema = "pt") |> ignore
migrationBuilder.DropTable (name = "pt_user", schema = "pt") |> ignore
migrationBuilder.DropTable (name = "church", schema = "pt") |> ignore
override _.BuildTargetModel (modelBuilder : ModelBuilder) =
modelBuilder
.HasDefaultSchema("pt")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
.HasAnnotation("ProductVersion", "1.1.0-rtm-22752")
|> ignore
modelBuilder.Entity (typeof<Church>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<string>("City").HasColumnName("city").IsRequired() |> ignore
b.Property<bool>("HasVpsInterface").HasColumnName("has_vps_interface") |> ignore
b.Property<string>("InterfaceAddress").HasColumnName("interface_address") |> ignore
b.Property<string>("Name").HasColumnName("church_name").IsRequired() |> ignore
b.Property<string>("State").HasColumnName("state").IsRequired().HasMaxLength(2) |> ignore
b.HasKey("Id") |> ignore
b.ToTable("church") |> ignore)
|> ignore
modelBuilder.Entity (typeof<ListPreferences>, fun b ->
b.Property<Guid>("SmallGroupId").HasColumnName("small_group_id") |> ignore
b.Property<string>("AsOfDateDisplay").HasColumnName("as_of_date_display").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("N").HasMaxLength(1) |> ignore
b.Property<int>("DaysToExpire").HasColumnName("days_to_expire").ValueGeneratedOnAdd().HasDefaultValue(14) |> ignore
b.Property<int>("DaysToKeepNew").HasColumnName("days_to_keep_new").ValueGeneratedOnAdd().HasDefaultValue(7) |> ignore
b.Property<string>("DefaultEmailType").HasColumnName("default_email_type").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("H") |> ignore
b.Property<string>("EmailFromAddress").HasColumnName("email_from_address").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("prayer@djs-consulting.com") |> ignore
b.Property<string>("EmailFromName").HasColumnName("email_from_name").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("PrayerTracker") |> ignore
b.Property<string>("Fonts").HasColumnName("fonts").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("Century Gothic,Tahoma,Luxi Sans,sans-serif") |> ignore
b.Property<string>("GroupPassword").HasColumnName("group_password").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("") |> ignore
b.Property<string>("HeadingColor").HasColumnName("heading_color").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("maroon") |> ignore
b.Property<int>("HeadingFontSize").HasColumnName("heading_font_size").ValueGeneratedOnAdd().HasDefaultValue(16) |> ignore
b.Property<bool>("IsPublic").HasColumnName("is_public").ValueGeneratedOnAdd().HasDefaultValue(false) |> ignore
b.Property<string>("LineColor").HasColumnName("line_color").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("navy") |> ignore
b.Property<int>("LongTermUpdateWeeks").HasColumnName("long_term_update_weeks").ValueGeneratedOnAdd().HasDefaultValue(4) |> ignore
b.Property<int>("PageSize").HasColumnName("page_size").IsRequired().ValueGeneratedOnAdd().HasDefaultValue(100) |> ignore
b.Property<string>("RequestSort").HasColumnName("request_sort").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("D").HasMaxLength(1) |> ignore
b.Property<int>("TextFontSize").HasColumnName("text_font_size").ValueGeneratedOnAdd().HasDefaultValue(12) |> ignore
b.Property<string>("TimeZoneId").HasColumnName("time_zone_id").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("America/Denver") |> ignore
b.HasKey("SmallGroupId") |> ignore
b.HasIndex("TimeZoneId").HasDatabaseName "ix_list_preference_time_zone_id" |> ignore
b.ToTable("list_preference") |> ignore)
|> ignore
modelBuilder.Entity (typeof<Member>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<string>("Email").HasColumnName("email").IsRequired() |> ignore
b.Property<string>("Format").HasColumnName("email_format") |> ignore
b.Property<string>("Name").HasColumnName("member_name").IsRequired() |> ignore
b.Property<Guid>("SmallGroupId").HasColumnName("small_group_id") |> ignore
b.HasKey("Id") |> ignore
b.HasIndex("SmallGroupId").HasDatabaseName "ix_member_small_group_id" |> ignore
b.ToTable("member") |> ignore)
|> ignore
modelBuilder.Entity (typeof<PrayerRequest>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<DateTime>("EnteredDate").HasColumnName("entered_date").IsRequired() |> ignore
b.Property<string>("Expiration").HasColumnName("expiration").IsRequired().HasMaxLength 1 |> ignore
b.Property<bool>("NotifyChaplain").HasColumnName("notify_chaplain") |> ignore
b.Property<string>("RequestType").HasColumnName("request_type").IsRequired().HasMaxLength 1 |> ignore
b.Property<string>("Requestor").HasColumnName("requestor") |> ignore
b.Property<Guid>("SmallGroupId").HasColumnName("small_group_id") |> ignore
b.Property<string>("Text").HasColumnName("request_text").IsRequired() |> ignore
b.Property<DateTime>("UpdatedDate").HasColumnName("updated_date") |> ignore
b.Property<Guid>("UserId").HasColumnName("user_id") |> ignore
b.HasKey("Id") |> ignore
b.HasIndex("SmallGroupId").HasDatabaseName "ix_prayer_request_small_group_id" |> ignore
b.HasIndex("UserId").HasDatabaseName "ix_prayer_request_user_id" |> ignore
b.ToTable("prayer_request") |> ignore)
|> ignore
modelBuilder.Entity (typeof<SmallGroup>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<Guid>("ChurchId").HasColumnName("church_id") |> ignore
b.Property<string>("Name").HasColumnName("group_name").IsRequired() |> ignore
b.HasKey("Id") |> ignore
b.HasIndex("ChurchId").HasDatabaseName "ix_small_group_church_id" |> ignore
b.ToTable("small_group") |> ignore)
|> ignore
modelBuilder.Entity (typeof<TimeZone>, fun b ->
b.Property<string>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<string>("Description").HasColumnName("description").IsRequired() |> ignore
b.Property<bool>("IsActive").HasColumnName("is_active") |> ignore
b.Property<int>("SortOrder").HasColumnName("sort_order") |> ignore
b.HasKey("Id") |> ignore
b.ToTable("time_zone") |> ignore)
|> ignore
modelBuilder.Entity (typeof<User>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<string>("Email").HasColumnName("email").IsRequired() |> ignore
b.Property<string>("FirstName").HasColumnName("first_name").IsRequired() |> ignore
b.Property<bool>("IsAdmin").HasColumnName("is_admin") |> ignore
b.Property<string>("LastName").HasColumnName("last_name").IsRequired() |> ignore
b.Property<string>("PasswordHash").HasColumnName("password_hash").IsRequired() |> ignore
b.Property<Guid>("Salt").HasColumnName("salt") |> ignore
b.Property<DateTime>("LastSeen").HasColumnName("last_seen") |> ignore
b.HasKey("Id") |> ignore
b.ToTable("pt_user") |> ignore)
|> ignore
modelBuilder.Entity (typeof<UserSmallGroup>, fun b ->
b.Property<Guid>("UserId").HasColumnName("user_id") |> ignore
b.Property<Guid>("SmallGroupId").HasColumnName("small_group_id") |> ignore
b.HasKey("UserId", "SmallGroupId") |> ignore
b.HasIndex("SmallGroupId").HasDatabaseName "ix_user_small_group_small_group_id" |> ignore
b.ToTable("user_small_group") |> ignore)
|> ignore
modelBuilder.Entity (typeof<ListPreferences>, fun b ->
b.HasOne("PrayerTracker.Entities.SmallGroup")
.WithOne("Preferences")
.HasForeignKey("PrayerTracker.Entities.ListPreferences", "SmallGroupId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore
b.HasOne("PrayerTracker.Entities.TimeZone", "TimeZone")
.WithMany()
.HasForeignKey("TimeZoneId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore
modelBuilder.Entity (typeof<Member>, fun b ->
b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup")
.WithMany("Members")
.HasForeignKey("SmallGroupId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore
modelBuilder.Entity (typeof<PrayerRequest>, fun b ->
b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup")
.WithMany("PrayerRequests")
.HasForeignKey("SmallGroupId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore
b.HasOne("PrayerTracker.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore
modelBuilder.Entity (typeof<SmallGroup>, fun b ->
b.HasOne("PrayerTracker.Entities.Church", "Church")
.WithMany("SmallGroups")
.HasForeignKey("ChurchId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore
modelBuilder.Entity (typeof<UserSmallGroup>, fun b ->
b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup")
.WithMany("Users")
.HasForeignKey("SmallGroupId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore
b.HasOne("PrayerTracker.Entities.User", "User")
.WithMany("SmallGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore

View File

@ -1,175 +0,0 @@
namespace PrayerTracker.Migrations
open System
open Microsoft.EntityFrameworkCore
open Microsoft.EntityFrameworkCore.Infrastructure
open Npgsql.EntityFrameworkCore.PostgreSQL.Metadata
open PrayerTracker
open PrayerTracker.Entities
[<DbContext (typeof<AppDbContext>)>]
type AppDbContextModelSnapshot () =
inherit ModelSnapshot ()
override _.BuildModel (modelBuilder : ModelBuilder) =
modelBuilder
.HasDefaultSchema("pt")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
.HasAnnotation("ProductVersion", "1.1.0-rtm-22752")
|> ignore
modelBuilder.Entity (typeof<Church>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<string>("City").HasColumnName("city").IsRequired() |> ignore
b.Property<bool>("HasVpsInterface").HasColumnName("has_vps_interface") |> ignore
b.Property<string>("InterfaceAddress").HasColumnName("interface_address") |> ignore
b.Property<string>("Name").HasColumnName("church_name").IsRequired() |> ignore
b.Property<string>("State").HasColumnName("state").IsRequired().HasMaxLength(2) |> ignore
b.HasKey("Id") |> ignore
b.ToTable("church") |> ignore)
|> ignore
modelBuilder.Entity (typeof<ListPreferences>, fun b ->
b.Property<Guid>("SmallGroupId").HasColumnName("small_group_id") |> ignore
b.Property<string>("AsOfDateDisplay").HasColumnName("as_of_date_display").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("N").HasMaxLength(1) |> ignore
b.Property<int>("DaysToExpire").HasColumnName("days_to_expire").ValueGeneratedOnAdd().HasDefaultValue(14) |> ignore
b.Property<int>("DaysToKeepNew").HasColumnName("days_to_keep_new").ValueGeneratedOnAdd().HasDefaultValue(7) |> ignore
b.Property<string>("DefaultEmailType").HasColumnName("default_email_type").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("H") |> ignore
b.Property<string>("EmailFromAddress").HasColumnName("email_from_address").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("prayer@djs-consulting.com") |> ignore
b.Property<string>("EmailFromName").HasColumnName("email_from_name").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("PrayerTracker") |> ignore
b.Property<string>("Fonts").HasColumnName("fonts").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("Century Gothic,Tahoma,Luxi Sans,sans-serif") |> ignore
b.Property<string>("GroupPassword").HasColumnName("group_password").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("") |> ignore
b.Property<string>("HeadingColor").HasColumnName("heading_color").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("maroon") |> ignore
b.Property<int>("HeadingFontSize").HasColumnName("heading_font_size").ValueGeneratedOnAdd().HasDefaultValue(16) |> ignore
b.Property<bool>("IsPublic").HasColumnName("is_public").ValueGeneratedOnAdd().HasDefaultValue(false) |> ignore
b.Property<string>("LineColor").HasColumnName("line_color").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("navy") |> ignore
b.Property<int>("LongTermUpdateWeeks").HasColumnName("long_term_update_weeks").ValueGeneratedOnAdd().HasDefaultValue(4) |> ignore
b.Property<int>("PageSize").HasColumnName("page_size").IsRequired().ValueGeneratedOnAdd().HasDefaultValue(100) |> ignore
b.Property<string>("RequestSort").HasColumnName("request_sort").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("D").HasMaxLength(1) |> ignore
b.Property<int>("TextFontSize").HasColumnName("text_font_size").ValueGeneratedOnAdd().HasDefaultValue(12) |> ignore
b.Property<string>("TimeZoneId").HasColumnName("time_zone_id").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("America/Denver") |> ignore
b.HasKey("SmallGroupId") |> ignore
b.HasIndex("TimeZoneId").HasDatabaseName "ix_list_preference_time_zone_id" |> ignore
b.ToTable("list_preference") |> ignore)
|> ignore
modelBuilder.Entity (typeof<Member>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<string>("Email").HasColumnName("email").IsRequired() |> ignore
b.Property<string>("Format").HasColumnName("email_format") |> ignore
b.Property<string>("Name").HasColumnName("member_name").IsRequired() |> ignore
b.Property<Guid>("SmallGroupId").HasColumnName("small_group_id") |> ignore
b.HasKey("Id") |> ignore
b.HasIndex("SmallGroupId").HasDatabaseName "ix_member_small_group_id" |> ignore
b.ToTable("member") |> ignore)
|> ignore
modelBuilder.Entity (typeof<PrayerRequest>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<DateTime>("EnteredDate").HasColumnName("entered_date").IsRequired() |> ignore
b.Property<string>("Expiration").HasColumnName("expiration").IsRequired().HasMaxLength 1 |> ignore
b.Property<bool>("NotifyChaplain").HasColumnName("notify_chaplain") |> ignore
b.Property<string>("RequestType").HasColumnName("request_type").IsRequired().HasMaxLength 1 |> ignore
b.Property<string>("Requestor").HasColumnName("requestor") |> ignore
b.Property<Guid>("SmallGroupId").HasColumnName("small_group_id") |> ignore
b.Property<string>("Text").HasColumnName("request_text").IsRequired() |> ignore
b.Property<DateTime>("UpdatedDate").HasColumnName("updated_date") |> ignore
b.Property<Guid>("UserId").HasColumnName("user_id") |> ignore
b.HasKey("Id") |> ignore
b.HasIndex("SmallGroupId").HasDatabaseName "ix_prayer_request_small_group_id" |> ignore
b.HasIndex("UserId").HasDatabaseName "ix_prayer_request_user_id" |> ignore
b.ToTable("prayer_request") |> ignore)
|> ignore
modelBuilder.Entity (typeof<SmallGroup>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<Guid>("ChurchId").HasColumnName("church_id") |> ignore
b.Property<string>("Name").HasColumnName("group_name").IsRequired() |> ignore
b.HasKey("Id") |> ignore
b.HasIndex("ChurchId").HasDatabaseName "ix_small_group_church_id" |> ignore
b.ToTable("small_group") |> ignore)
|> ignore
modelBuilder.Entity (typeof<TimeZone>, fun b ->
b.Property<string>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<string>("Description").HasColumnName("description").IsRequired() |> ignore
b.Property<bool>("IsActive").HasColumnName("is_active") |> ignore
b.Property<int>("SortOrder").HasColumnName("sort_order") |> ignore
b.HasKey("Id") |> ignore
b.ToTable("time_zone") |> ignore)
|> ignore
modelBuilder.Entity (typeof<User>, fun b ->
b.Property<Guid>("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore
b.Property<string>("Email").HasColumnName("email").IsRequired() |> ignore
b.Property<string>("FirstName").HasColumnName("first_name").IsRequired() |> ignore
b.Property<bool>("IsAdmin").HasColumnName("is_admin") |> ignore
b.Property<string>("LastName").HasColumnName("last_name").IsRequired() |> ignore
b.Property<string>("PasswordHash").HasColumnName("password_hash").IsRequired() |> ignore
b.Property<Guid>("Salt").HasColumnName("salt") |> ignore
b.Property<DateTime>("LastSeen").HasColumnName("last_seen") |> ignore
b.HasKey("Id") |> ignore
b.ToTable("pt_user") |> ignore)
|> ignore
modelBuilder.Entity (typeof<UserSmallGroup>, fun b ->
b.Property<Guid>("UserId").HasColumnName("user_id") |> ignore
b.Property<Guid>("SmallGroupId").HasColumnName("small_group_id") |> ignore
b.HasKey("UserId", "SmallGroupId") |> ignore
b.HasIndex("SmallGroupId").HasDatabaseName "ix_user_small_group_small_group_id" |> ignore
b.ToTable("user_small_group") |> ignore)
|> ignore
modelBuilder.Entity (typeof<ListPreferences>, fun b ->
b.HasOne("PrayerTracker.Entities.SmallGroup")
.WithOne("Preferences")
.HasForeignKey("PrayerTracker.Entities.ListPreferences", "smallGroupId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore
b.HasOne("PrayerTracker.Entities.TimeZone", "TimeZone")
.WithMany()
.HasForeignKey("TimeZoneId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore
modelBuilder.Entity (typeof<Member>, fun b ->
b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup")
.WithMany("Members")
.HasForeignKey("SmallGroupId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore
modelBuilder.Entity (typeof<PrayerRequest>, fun b ->
b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup")
.WithMany("PrayerRequests")
.HasForeignKey("SmallGroupId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore
b.HasOne("PrayerTracker.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore
modelBuilder.Entity (typeof<SmallGroup>, fun b ->
b.HasOne("PrayerTracker.Entities.Church", "Church")
.WithMany("SmallGroups")
.HasForeignKey("ChurchId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore
modelBuilder.Entity (typeof<UserSmallGroup>, fun b ->
b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup")
.WithMany("Users")
.HasForeignKey("SmallGroupId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore
b.HasOne("PrayerTracker.Entities.User", "User")
.WithMany("SmallGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
|> ignore)
|> ignore

View File

@ -7,18 +7,12 @@
<ItemGroup> <ItemGroup>
<Compile Include="Entities.fs" /> <Compile Include="Entities.fs" />
<Compile Include="Access.fs" /> <Compile Include="Access.fs" />
<Compile Include="AppDbContext.fs" />
<Compile Include="DataAccess.fs" />
<Compile Include="Migrations\20161217153124_InitialDatabase.fs" />
<Compile Include="Migrations\AppDbContextModelSnapshot.fs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FSharp.EFCore.OptionConverter" Version="1.0.0" />
<PackageReference Include="Giraffe" Version="6.0.0" /> <PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Microsoft.FSharpLu" Version="0.11.7" /> <PackageReference Include="Microsoft.FSharpLu" Version="0.11.7" />
<PackageReference Include="NodaTime" Version="3.1.0" /> <PackageReference Include="NodaTime" Version="3.1.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
<PackageReference Update="FSharp.Core" Version="6.0.5" /> <PackageReference Update="FSharp.Core" Version="6.0.5" />
<PackageReference Include="Npgsql.FSharp" Version="5.3.0" /> <PackageReference Include="Npgsql.FSharp" Version="5.3.0" />
<PackageReference Include="Npgsql.NodaTime" Version="6.0.6" /> <PackageReference Include="Npgsql.NodaTime" Version="6.0.6" />

View File

@ -118,8 +118,6 @@ let listPreferencesTests =
Expect.isFalse mt.IsPublic "The isPublic flag should not have been set" Expect.isFalse mt.IsPublic "The isPublic flag should not have been set"
Expect.equal (TimeZoneId.toString mt.TimeZoneId) "America/Denver" Expect.equal (TimeZoneId.toString mt.TimeZoneId) "America/Denver"
"The default time zone should have been America/Denver" "The default time zone should have been America/Denver"
Expect.equal (TimeZoneId.toString mt.TimeZone.Id) ""
"The default preferences should have included an empty time zone"
Expect.equal mt.PageSize 100 "The default page size should have been 100" Expect.equal mt.PageSize 100 "The default page size should have been 100"
Expect.equal mt.AsOfDateDisplay NoDisplay "The as-of date display should have been No Display" Expect.equal mt.AsOfDateDisplay NoDisplay "The as-of date display should have been No Display"
} }
@ -135,7 +133,6 @@ let memberTests =
Expect.equal mt.Name "" "The member name should have been blank" Expect.equal mt.Name "" "The member name should have been blank"
Expect.equal mt.Email "" "The member e-mail address should have been blank" Expect.equal mt.Email "" "The member e-mail address should have been blank"
Expect.isNone mt.Format "The preferred e-mail format should not exist" Expect.isNone mt.Format "The preferred e-mail format should not exist"
Expect.equal mt.SmallGroup.Id.Value Guid.Empty "The small group should have been an empty one"
} }
] ]
@ -156,8 +153,6 @@ let prayerRequestTests =
Expect.equal mt.Text "" "The request text should have been blank" Expect.equal mt.Text "" "The request text should have been blank"
Expect.isFalse mt.NotifyChaplain "The notify chaplain flag should not have been set" Expect.isFalse mt.NotifyChaplain "The notify chaplain flag should not have been set"
Expect.equal mt.Expiration Automatic "The expiration should have been Automatic" Expect.equal mt.Expiration Automatic "The expiration should have been Automatic"
Expect.equal mt.User.Id.Value Guid.Empty "The user should have been an empty one"
Expect.equal mt.SmallGroup.Id.Value Guid.Empty "The small group should have been an empty one"
} }
test "isExpired always returns false for expecting requests" { test "isExpired always returns false for expecting requests" {
PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty
@ -294,13 +289,6 @@ let smallGroupTests =
Expect.equal mt.Id.Value Guid.Empty "The small group ID should have been an empty GUID" Expect.equal mt.Id.Value Guid.Empty "The small group ID should have been an empty GUID"
Expect.equal mt.ChurchId.Value Guid.Empty "The church ID should have been an empty GUID" Expect.equal mt.ChurchId.Value Guid.Empty "The church ID should have been an empty GUID"
Expect.equal mt.Name "" "The name should have been blank" Expect.equal mt.Name "" "The name should have been blank"
Expect.equal mt.Church.Id.Value Guid.Empty "The church should have been an empty one"
Expect.isNotNull mt.Members "The members navigation property should not be null"
Expect.isEmpty mt.Members "There should be no members for an empty small group"
Expect.isNotNull mt.PrayerRequests "The prayer requests navigation property should not be null"
Expect.isEmpty mt.PrayerRequests "There should be no prayer requests for an empty small group"
Expect.isNotNull mt.Users "The users navigation property should not be null"
Expect.isEmpty mt.Users "There should be no users for an empty small group"
} }
yield! testFixture withFakeClock [ yield! testFixture withFakeClock [
"LocalTimeNow adjusts the time ahead of UTC", "LocalTimeNow adjusts the time ahead of UTC",
@ -309,7 +297,6 @@ let smallGroupTests =
{ SmallGroup.empty with { SmallGroup.empty with
Preferences = { ListPreferences.empty with TimeZoneId = TimeZoneId "Europe/Berlin" } Preferences = { ListPreferences.empty with TimeZoneId = TimeZoneId "Europe/Berlin" }
} }
let tz = DateTimeZoneProviders.Tzdb["Europe/Berlin"]
Expect.isGreaterThan (SmallGroup.localTimeNow clock grp) (now.InUtc().LocalDateTime) Expect.isGreaterThan (SmallGroup.localTimeNow clock grp) (now.InUtc().LocalDateTime)
"UTC to Europe/Berlin should have added hours" "UTC to Europe/Berlin should have added hours"
"LocalTimeNow adjusts the time behind UTC", "LocalTimeNow adjusts the time behind UTC",
@ -336,18 +323,6 @@ let smallGroupTests =
} }
] ]
[<Tests>]
let timeZoneTests =
testList "TimeZone" [
test "empty is as expected" {
let mt = TimeZone.empty
Expect.equal (TimeZoneId.toString mt.Id) "" "The time zone ID should have been blank"
Expect.equal mt.Description "" "The description should have been blank"
Expect.equal mt.SortOrder 0 "The sort order should have been zero"
Expect.isFalse mt.IsActive "The is-active flag should not have been set"
}
]
[<Tests>] [<Tests>]
let userTests = let userTests =
testList "User" [ testList "User" [
@ -359,9 +334,6 @@ let userTests =
Expect.equal mt.Email "" "The e-mail address should have been blank" Expect.equal mt.Email "" "The e-mail address should have been blank"
Expect.isFalse mt.IsAdmin "The is admin flag should not have been set" Expect.isFalse mt.IsAdmin "The is admin flag should not have been set"
Expect.equal mt.PasswordHash "" "The password hash should have been blank" Expect.equal mt.PasswordHash "" "The password hash should have been blank"
Expect.isNone mt.Salt "The password salt should not exist"
Expect.isNotNull mt.SmallGroups "The small groups navigation property should not have been null"
Expect.isEmpty mt.SmallGroups "There should be no small groups for an empty user"
} }
test "Name concatenates first and last names" { test "Name concatenates first and last names" {
let user = { User.empty with FirstName = "Unit"; LastName = "Test" } let user = { User.empty with FirstName = "Unit"; LastName = "Test" }
@ -376,7 +348,5 @@ let userSmallGroupTests =
let mt = UserSmallGroup.empty let mt = UserSmallGroup.empty
Expect.equal mt.UserId.Value Guid.Empty "The user ID should have been an empty GUID" 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" Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID"
Expect.equal mt.User.Id.Value Guid.Empty "The user should have been an empty one"
Expect.equal mt.SmallGroup.Id.Value Guid.Empty "The small group should have been an empty one"
} }
] ]

View File

@ -16,7 +16,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="9.0.4" /> <PackageReference Include="Expecto" Version="9.0.4" />
<PackageReference Include="Expecto.VisualStudio.TestAdapter" Version="10.0.2" />
<PackageReference Include="NodaTime.Testing" Version="3.1.0" /> <PackageReference Include="NodaTime.Testing" Version="3.1.0" />
<PackageReference Update="FSharp.Core" Version="6.0.5" /> <PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup> </ItemGroup>

View File

@ -543,9 +543,9 @@ let requestListTests =
let curReqHtml = let curReqHtml =
[ "<ul>" [ "<ul>"
"""<li style="list-style-type:circle;font-family:Century Gothic,Tahoma,Luxi Sans,sans-serif;font-size:12pt;padding-bottom:.25em;">""" """<li style="list-style-type:circle;font-family:Century Gothic,Tahoma,Luxi Sans,sans-serif;font-size:12pt;padding-bottom:.25em;">"""
"<strong>Zeb</strong> &mdash; zyx</li>" "<strong>Zeb</strong> &ndash; zyx</li>"
"""<li style="list-style-type:disc;font-family:Century Gothic,Tahoma,Luxi Sans,sans-serif;font-size:12pt;padding-bottom:.25em;">""" """<li style="list-style-type:disc;font-family:Century Gothic,Tahoma,Luxi Sans,sans-serif;font-size:12pt;padding-bottom:.25em;">"""
"<strong>Aaron</strong> &mdash; abc</li></ul>" "<strong>Aaron</strong> &ndash; abc</li></ul>"
] ]
|> String.concat "" |> String.concat ""
Expect.stringContains html curReqHtml """Expected HTML for "Current Requests" requests not found""" Expect.stringContains html curReqHtml """Expected HTML for "Current Requests" requests not found"""
@ -582,7 +582,7 @@ let requestListTests =
|> String.concat "" |> String.concat ""
Expect.stringContains html lstHeading "Expected HTML for the list heading not found" Expect.stringContains html lstHeading "Expected HTML for the list heading not found"
// spot check; without header test tests this exhaustively // spot check; without header test tests this exhaustively
Expect.stringContains html "<strong>Zeb</strong> &mdash; zyx</li>" "Expected requests not found" Expect.stringContains html "<strong>Zeb</strong> &ndash; zyx</li>" "Expected requests not found"
"AsHtml succeeds with short as-of date", "AsHtml succeeds with short as-of date",
fun reqList -> fun reqList ->
let htmlList = let htmlList =
@ -595,7 +595,7 @@ let requestListTests =
let html = htmlList.AsHtml _s let html = htmlList.AsHtml _s
let expected = let expected =
htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("d", null) htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("d", null)
|> sprintf """<strong>Zeb</strong> &mdash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>""" |> sprintf """<strong>Zeb</strong> &ndash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>"""
// spot check; if one request has it, they all should // spot check; if one request has it, they all should
Expect.stringContains html expected "Expected short as-of date not found" Expect.stringContains html expected "Expected short as-of date not found"
"AsHtml succeeds with long as-of date", "AsHtml succeeds with long as-of date",
@ -610,7 +610,7 @@ let requestListTests =
let html = htmlList.AsHtml _s let html = htmlList.AsHtml _s
let expected = let expected =
htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("D", null) htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("D", null)
|> sprintf """<strong>Zeb</strong> &mdash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>""" |> sprintf """<strong>Zeb</strong> &ndash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>"""
// spot check; if one request has it, they all should // spot check; if one request has it, they all should
Expect.stringContains html expected "Expected long as-of date not found" Expect.stringContains html expected "Expected long as-of date not found"
"AsText succeeds with no as-of date", "AsText succeeds with no as-of date",

View File

@ -14,13 +14,12 @@ type RequestStartMiddleware (next : RequestDelegate) =
open System open System
open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Configuration
/// Module to hold configuration for the web app /// Module to hold configuration for the web app
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Configure = module Configure =
open Microsoft.Extensions.Configuration
/// Set up the configuration for the app /// Set up the configuration for the app
let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) = let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) =
cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath) cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath)
@ -39,7 +38,6 @@ module Configure =
open System.IO open System.IO
open Microsoft.AspNetCore.Authentication.Cookies open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.AspNetCore.Localization open Microsoft.AspNetCore.Localization
open Microsoft.EntityFrameworkCore
open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.DependencyInjection
open NeoSmart.Caching.Sqlite open NeoSmart.Caching.Sqlite
open NodaTime open NodaTime
@ -69,15 +67,6 @@ module Configure =
let _ = svc.AddAntiforgery () let _ = svc.AddAntiforgery ()
let _ = svc.AddRouting () let _ = svc.AddRouting ()
let _ = svc.AddSingleton<IClock> SystemClock.Instance let _ = svc.AddSingleton<IClock> SystemClock.Instance
let config = svc.BuildServiceProvider().GetRequiredService<IConfiguration> ()
let _ =
svc.AddDbContext<AppDbContext> (
(fun options ->
options.UseNpgsql (
config.GetConnectionString "PrayerTracker", fun o -> o.UseNodaTime () |> ignore)
|> ignore),
ServiceLifetime.Scoped, ServiceLifetime.Singleton)
() ()
open Giraffe open Giraffe
@ -194,10 +183,6 @@ module Configure =
let _ = app.UseDeveloperExceptionPage () let _ = app.UseDeveloperExceptionPage ()
() ()
else else
try
use scope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope ()
scope.ServiceProvider.GetService<AppDbContext>().Database.Migrate ()
with _ -> () // om nom nom
let _ = app.UseGiraffeErrorHandler errorHandler let _ = app.UseGiraffeErrorHandler errorHandler
() ()
@ -219,34 +204,55 @@ module Configure =
module App = module App =
open System.Text open System.Text
open Microsoft.EntityFrameworkCore
open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.DependencyInjection
open Npgsql
open Npgsql.FSharp
open PrayerTracker.Entities
let migratePasswords (app : IWebHost) = let migratePasswords (app : IWebHost) =
task { task {
use db = app.Services.GetService<AppDbContext>() let config = app.Services.GetService<IConfiguration> ()
let! v1Users = db.Users.FromSqlRaw("SELECT * FROM pt.pt_user WHERE salt IS NULL").ToListAsync () use conn = new NpgsqlConnection (config.GetConnectionString "PrayerTracker")
for user in v1Users do do! conn.OpenAsync ()
let! v1Users =
conn
|> Sql.existingConnection
|> Sql.query "SELECT id, password_hash FROM pt.pt_user WHERE salt IS NULL"
|> Sql.executeAsync (fun row -> UserId (row.uuid "id"), row.string "password_hash")
for userId, oldHash in v1Users do
let pw = let pw =
[| 254uy [| 254uy
yield! (Encoding.UTF8.GetBytes user.PasswordHash) yield! (Encoding.UTF8.GetBytes oldHash)
|] |]
|> Convert.ToBase64String |> Convert.ToBase64String
db.UpdateEntry { user with PasswordHash = pw } let! _ =
let! v1Count = db.SaveChangesAsync () conn
printfn $"Updated {v1Count} users with version 1 password" |> Sql.existingConnection
|> Sql.query "UPDATE pt.pt_user SET password_hash = @hash WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid userId.Value; "@hash", Sql.string pw ]
|> Sql.executeNonQueryAsync
()
printfn $"Updated {v1Users.Length} users with version 1 password"
let! v2Users = let! v2Users =
db.Users.FromSqlRaw("SELECT * FROM pt.pt_user WHERE salt IS NOT NULL").ToListAsync () conn
for user in v2Users do |> Sql.existingConnection
|> Sql.query "SELECT id, password_hash, salt FROM pt.pt_user WHERE salt IS NOT NULL"
|> Sql.executeAsync (fun row -> UserId (row.uuid "id"), row.string "password_hash", row.uuid "salt")
for userId, oldHash, salt in v2Users do
let pw = let pw =
[| 255uy [| 255uy
yield! (user.Salt.Value.ToByteArray ()) yield! (salt.ToByteArray ())
yield! (Encoding.UTF8.GetBytes user.PasswordHash) yield! (Encoding.UTF8.GetBytes oldHash)
|] |]
|> Convert.ToBase64String |> Convert.ToBase64String
db.UpdateEntry { user with PasswordHash = pw } let! _ =
let! v2Count = db.SaveChangesAsync () conn
printfn $"Updated {v2Count} users with version 2 password" |> Sql.existingConnection
|> Sql.query "UPDATE pt.pt_user SET password_hash = @hash WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid userId.Value; "@hash", Sql.string pw ]
|> Sql.executeNonQueryAsync
()
printfn $"Updated {v2Users.Length} users with version 2 password"
} |> Async.AwaitTask |> Async.RunSynchronously } |> Async.AwaitTask |> Async.RunSynchronously
open System.IO open System.IO

View File

@ -39,8 +39,7 @@ type ISession with
with get () = this.GetObject<User> Key.Session.currentUser |> Option.fromObject with get () = this.GetObject<User> Key.Session.currentUser |> Option.fromObject
and set (v : User option) = and set (v : User option) =
match v with match v with
| Some user -> | Some user -> this.SetObject Key.Session.currentUser { user with PasswordHash = "" }
this.SetObject Key.Session.currentUser { user with PasswordHash = ""; SmallGroups = ResizeArray() }
| None -> this.Remove Key.Session.currentUser | None -> this.Remove Key.Session.currentUser
/// Current messages for the session /// Current messages for the session
@ -74,7 +73,6 @@ open System.Threading.Tasks
open Giraffe open Giraffe
open Microsoft.Extensions.Configuration open Microsoft.Extensions.Configuration
open Npgsql open Npgsql
open PrayerTracker
/// Extensions on the ASP.NET Core HTTP context /// Extensions on the ASP.NET Core HTTP context
type HttpContext with type HttpContext with
@ -87,9 +85,6 @@ type HttpContext with
return conn return conn
}) })
/// The EF Core database context (via DI)
member this.Db = this.GetService<AppDbContext> ()
/// The PostgreSQL connection (configured via DI) /// The PostgreSQL connection (configured via DI)
member this.Conn = backgroundTask { member this.Conn = backgroundTask {
return! this.LazyConn.Force () return! this.LazyConn.Force ()

View File

@ -126,7 +126,7 @@ let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task
| Ok req -> | Ok req ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
let! conn = ctx.Conn let! conn = ctx.Conn
do! PrayerRequests.updateExpiration { req with Expiration = Forced } conn do! PrayerRequests.updateExpiration { req with Expiration = Forced } false conn
addInfo ctx s["Successfully {0} prayer request", s["Expired"].Value.ToLower ()] addInfo ctx s["Successfully {0} prayer request", s["Expired"].Value.ToLower ()]
return! redirectTo false "/prayer-requests" next ctx return! redirectTo false "/prayer-requests" next ctx
| Result.Error e -> return! e | Result.Error e -> return! e
@ -177,16 +177,16 @@ let lists : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx
/// - OR - /// - OR -
/// GET /prayer-requests?search=[search-query] /// GET /prayer-requests?search=[search-query]
let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
// TODO: stopped here
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let pageNbr = let pageNbr =
match ctx.GetQueryStringValue "page" with match ctx.GetQueryStringValue "page" with
| Ok pg -> match Int32.TryParse pg with true, p -> p | false, _ -> 1 | Ok pg -> match Int32.TryParse pg with true, p -> p | false, _ -> 1
| Result.Error _ -> 1 | Result.Error _ -> 1
let! model = backgroundTask { let! model = backgroundTask {
let! conn = ctx.Conn
match ctx.GetQueryStringValue "search" with match ctx.GetQueryStringValue "search" with
| Ok search -> | Ok search ->
let! reqs = ctx.Db.SearchRequestsForSmallGroup group search pageNbr let! reqs = PrayerRequests.searchForGroup group search pageNbr conn
return return
{ MaintainRequests.empty with { MaintainRequests.empty with
Requests = reqs Requests = reqs
@ -194,7 +194,14 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
PageNbr = Some pageNbr PageNbr = Some pageNbr
} }
| Result.Error _ -> | Result.Error _ ->
let! reqs = ctx.Db.AllRequestsForSmallGroup group ctx.Clock None onlyActive pageNbr let! reqs =
PrayerRequests.forGroup
{ SmallGroup = group
Clock = ctx.Clock
ListDate = None
ActiveOnly = onlyActive
PageNumber = pageNbr
} conn
return return
{ MaintainRequests.empty with { MaintainRequests.empty with
Requests = reqs Requests = reqs
@ -221,9 +228,9 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas
let requestId = PrayerRequestId reqId let requestId = PrayerRequestId reqId
match! findRequest ctx requestId with match! findRequest ctx requestId with
| Ok req -> | Ok req ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
ctx.Db.UpdateEntry { req with Expiration = Automatic; UpdatedDate = ctx.Now } let! conn = ctx.Conn
let! _ = ctx.Db.SaveChangesAsync () do! PrayerRequests.updateExpiration { req with Expiration = Automatic; UpdatedDate = ctx.Now } true conn
addInfo ctx s["Successfully {0} prayer request", s["Restored"].Value.ToLower ()] addInfo ctx s["Successfully {0} prayer request", s["Restored"].Value.ToLower ()]
return! redirectTo false "/prayer-requests" next ctx return! redirectTo false "/prayer-requests" next ctx
| Result.Error e -> return! e | Result.Error e -> return! e
@ -235,41 +242,42 @@ open System.Threading.Tasks
let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditRequest> () with match! ctx.TryBindFormAsync<EditRequest> () with
| Ok model -> | Ok model ->
let! req = let group = ctx.Session.CurrentGroup.Value
if model.IsNew then let! conn = ctx.Conn
Task.FromResult (Some { PrayerRequest.empty with Id = (Guid.NewGuid >> PrayerRequestId) () }) let! req =
else ctx.Db.TryRequestById (idFromShort PrayerRequestId model.RequestId) if model.IsNew then
{ PrayerRequest.empty with
Id = (Guid.NewGuid >> PrayerRequestId) ()
SmallGroupId = group.Id
UserId = ctx.User.UserId.Value
}
|> (Some >> Task.FromResult)
else PrayerRequests.tryById (idFromShort PrayerRequestId model.RequestId) conn
match req with match req with
| Some pr -> | Some pr when pr.SmallGroupId = group.Id ->
let upd8 = let now = SmallGroup.localDateNow ctx.Clock group
let updated =
{ pr with { pr with
RequestType = PrayerRequestType.fromCode model.RequestType RequestType = PrayerRequestType.fromCode model.RequestType
Requestor = match model.Requestor with Some x when x.Trim () = "" -> None | x -> x Requestor = match model.Requestor with Some x when x.Trim () = "" -> None | x -> x
Text = ckEditorToText model.Text Text = ckEditorToText model.Text
Expiration = Expiration.fromCode model.Expiration Expiration = Expiration.fromCode model.Expiration
} }
let group = ctx.Session.CurrentGroup.Value |> function
let now = SmallGroup.localDateNow ctx.Clock group | it when model.IsNew ->
match model.IsNew with let dt =
| true -> (defaultArg (parseListDate model.EnteredDate) now)
let dt = .AtStartOfDayInZone(SmallGroup.timeZone group)
(defaultArg (parseListDate model.EnteredDate) now) .ToInstant()
.AtStartOfDayInZone(SmallGroup.timeZone group) { it with EnteredDate = dt; UpdatedDate = dt }
.ToInstant() | it when defaultArg model.SkipDateUpdate false -> it
{ upd8 with | it -> { it with UpdatedDate = ctx.Now }
SmallGroupId = group.Id do! PrayerRequests.save updated conn
UserId = ctx.User.UserId.Value let s = Views.I18N.localizer.Force ()
EnteredDate = dt let act = if model.IsNew then "Added" else "Updated"
UpdatedDate = dt
}
| false when defaultArg model.SkipDateUpdate false -> upd8
| false -> { upd8 with UpdatedDate = ctx.Now }
|> if model.IsNew then ctx.Db.AddEntry else ctx.Db.UpdateEntry
let! _ = ctx.Db.SaveChangesAsync ()
let s = Views.I18N.localizer.Force ()
let act = if model.IsNew then "Added" else "Updated"
addInfo ctx s["Successfully {0} prayer request", s[act].Value.ToLower ()] addInfo ctx s["Successfully {0} prayer request", s[act].Value.ToLower ()]
return! redirectTo false "/prayer-requests" next ctx return! redirectTo false "/prayer-requests" next ctx
| Some _
| None -> return! fourOhFour ctx | None -> return! fourOhFour ctx
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }

View File

@ -28,9 +28,7 @@
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" /> <PackageReference Include="Giraffe.Htmx" Version="1.8.0" />
<PackageReference Include="NeoSmart.Caching.Sqlite" Version="6.0.1" /> <PackageReference Include="NeoSmart.Caching.Sqlite" Version="6.0.1" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
<PackageReference Update="FSharp.Core" Version="6.0.5" /> <PackageReference Update="FSharp.Core" Version="6.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="6.0.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>