Version 8 #43
| @ -39,7 +39,6 @@ module private Helpers = | ||||
|             RequestSort         = RequestSort.fromCode     (row.string "request_sort") | ||||
|             DefaultEmailType    = EmailFormat.fromCode     (row.string "default_email_type") | ||||
|             AsOfDateDisplay     = AsOfDateDisplay.fromCode (row.string "as_of_date_display") | ||||
|             TimeZone = TimeZone.empty | ||||
|         } | ||||
|      | ||||
|     /// Map a row to a Member instance | ||||
| @ -49,7 +48,6 @@ module private Helpers = | ||||
|             Name         = row.string       "member_name" | ||||
|             Email        = row.string       "email" | ||||
|             Format       = row.stringOrNone "email_format" |> Option.map EmailFormat.fromCode | ||||
|             SmallGroup = SmallGroup.empty | ||||
|         } | ||||
|      | ||||
|     /// Map a row to a Prayer Request instance | ||||
| @ -64,8 +62,6 @@ module private Helpers = | ||||
|             NotifyChaplain = row.bool                "notify_chaplain" | ||||
|             RequestType    = PrayerRequestType.fromCode (row.string "request_id") | ||||
|             Expiration     = Expiration.fromCode        (row.string "expiration") | ||||
|             User = User.empty | ||||
|             SmallGroup = SmallGroup.empty | ||||
|         } | ||||
|      | ||||
|     /// Map a row to a Small Group instance | ||||
| @ -74,10 +70,6 @@ module private Helpers = | ||||
|             ChurchId    = ChurchId     (row.uuid "church_id") | ||||
|             Name        = row.string   "group_name" | ||||
|             Preferences = ListPreferences.empty | ||||
|             Church = Church.empty | ||||
|             Members = ResizeArray () | ||||
|             PrayerRequests = ResizeArray () | ||||
|             Users = ResizeArray () | ||||
|         } | ||||
|      | ||||
|     /// Map a row to a Small Group information set | ||||
| @ -107,9 +99,7 @@ module private Helpers = | ||||
|             Email        = row.string "email" | ||||
|             IsAdmin      = row.bool   "is_admin" | ||||
|             PasswordHash = row.string "password_hash" | ||||
|             Salt         = None | ||||
|             LastSeen     = row.fieldValueOrNone<Instant> "last_seen" | ||||
|             SmallGroups = ResizeArray () | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| @ -373,6 +363,19 @@ module PrayerRequests = | ||||
|         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 | ||||
|     let tryById (reqId : PrayerRequestId) conn = backgroundTask { | ||||
|         let! req = | ||||
| @ -385,14 +388,20 @@ module PrayerRequests = | ||||
|     } | ||||
|      | ||||
|     /// 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! _ = | ||||
|             conn | ||||
|             |> 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 | ||||
|                 [   "@expiration", Sql.string (Expiration.toCode req.Expiration) | ||||
|                 ([  "@expiration", Sql.string (Expiration.toCode req.Expiration) | ||||
|                     "@id",         Sql.uuid   req.Id.Value ] | ||||
|                  |> List.append parameters) | ||||
|             |> Sql.executeNonQueryAsync | ||||
|         return () | ||||
|     } | ||||
|  | ||||
| @ -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) | ||||
| @ -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)) | ||||
|     } | ||||
| @ -179,184 +179,7 @@ with | ||||
|     /// The GUID value of the user ID | ||||
|     member this.Value = this |> function UserId guid -> guid | ||||
| 
 | ||||
| 
 | ||||
| /// 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) | ||||
| 
 | ||||
| (*-- SPECIFIC VIEW TYPES --*) | ||||
| 
 | ||||
| /// Statistics for churches | ||||
| [<NoComparison; NoEquality>] | ||||
| @ -371,14 +194,33 @@ type ChurchStats = | ||||
|         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 --*) | ||||
| 
 | ||||
| open FSharp.EFCore.OptionConverter | ||||
| open Microsoft.EntityFrameworkCore | ||||
| open NodaTime | ||||
| 
 | ||||
| /// This represents a church | ||||
| type [<CLIMutable; NoComparison; NoEquality>] Church = | ||||
| [<NoComparison; NoEquality>] | ||||
| type Church = | ||||
|     {   /// The ID of this church | ||||
|         Id : ChurchId | ||||
|          | ||||
| @ -397,10 +239,13 @@ type [<CLIMutable; NoComparison; NoEquality>] Church = | ||||
|         /// The address for the interface | ||||
|         InterfaceAddress : string option | ||||
|     } | ||||
| with | ||||
| 
 | ||||
| /// Functions to support churches | ||||
| module Church = | ||||
|      | ||||
|     /// An empty church | ||||
|     // aww... how sad :( | ||||
|     static member empty = | ||||
|     let empty = | ||||
|         {   Id               = ChurchId Guid.Empty | ||||
|             Name             = "" | ||||
|             City             = "" | ||||
| @ -409,27 +254,10 @@ with | ||||
|             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 | ||||
| and [<CLIMutable; NoComparison; NoEquality>] ListPreferences = | ||||
| [<NoComparison; NoEquality>] | ||||
| type ListPreferences = | ||||
|     {   /// The Id of the small group to which these preferences belong | ||||
|         SmallGroupId : SmallGroupId | ||||
|          | ||||
| @ -478,19 +306,18 @@ and [<CLIMutable; NoComparison; NoEquality>] ListPreferences = | ||||
|         /// The time zone which this class uses (use tzdata names) | ||||
|         TimeZoneId : TimeZoneId | ||||
|          | ||||
|         /// The time zone information | ||||
|         TimeZone : TimeZone | ||||
|          | ||||
|         /// The number of requests displayed per page | ||||
|         PageSize : int | ||||
|          | ||||
|         /// How the as-of date should be automatically displayed | ||||
|         AsOfDateDisplay : AsOfDateDisplay | ||||
|     } | ||||
| with | ||||
| 
 | ||||
| /// Functions to support list preferences | ||||
| module ListPreferences = | ||||
|      | ||||
|     /// A set of preferences with their default values | ||||
|     static member empty = | ||||
|     let empty = | ||||
|         {   SmallGroupId        = SmallGroupId Guid.Empty | ||||
|             DaysToExpire        = 14 | ||||
|             DaysToKeepNew       = 7 | ||||
| @ -507,61 +334,14 @@ with | ||||
|             DefaultEmailType    = HtmlFormat | ||||
|             IsPublic            = false | ||||
|             TimeZoneId          = TimeZoneId "America/Denver" | ||||
|             TimeZone            = TimeZone.empty | ||||
|             PageSize            = 100 | ||||
|             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 | ||||
| and [<CLIMutable; NoComparison; NoEquality>] Member = | ||||
| [<NoComparison; NoEquality>] | ||||
| type Member = | ||||
|     {   /// The ID of the small group member | ||||
|         Id : MemberId | ||||
|          | ||||
| @ -576,44 +356,24 @@ and [<CLIMutable; NoComparison; NoEquality>] Member = | ||||
|          | ||||
|         /// The type of e-mail preferred by this member | ||||
|         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 | ||||
|     static member empty = | ||||
|     let empty = | ||||
|         {   Id           = MemberId Guid.Empty | ||||
|             SmallGroupId = SmallGroupId Guid.Empty | ||||
|             Name         = "" | ||||
|             Email        = "" | ||||
|             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 | ||||
| and [<CLIMutable; NoComparison; NoEquality>] PrayerRequest = | ||||
| [<NoComparison; NoEquality>] | ||||
| type PrayerRequest = | ||||
|     {   /// The ID of this request | ||||
|         Id : PrayerRequestId | ||||
|          | ||||
| @ -641,66 +401,15 @@ and [<CLIMutable; NoComparison; NoEquality>] PrayerRequest = | ||||
|         /// Whether the chaplain should be notified for this request | ||||
|         NotifyChaplain : bool | ||||
|          | ||||
|         /// The user who entered this request | ||||
|         User : User | ||||
|          | ||||
|         /// The small group to which this request belongs | ||||
|         SmallGroup : SmallGroup | ||||
|          | ||||
|         /// Is this request expired? | ||||
|         Expiration : Expiration | ||||
|     } | ||||
| with | ||||
|      | ||||
|     /// 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 ()) | ||||
| // functions are below small group functions | ||||
| 
 | ||||
| 
 | ||||
| /// 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 | ||||
|         Id : SmallGroupId | ||||
|          | ||||
| @ -710,215 +419,21 @@ and [<CLIMutable; NoComparison; NoEquality>] SmallGroup = | ||||
|         /// The name of the group | ||||
|         Name : string | ||||
|          | ||||
|         /// The church to which this small group belongs | ||||
|         Church : Church | ||||
|          | ||||
|         /// The preferences for the request list | ||||
|         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 | ||||
|      | ||||
|     /// An empty small group | ||||
|     static member empty = | ||||
|         {   Id             = SmallGroupId Guid.Empty | ||||
|             ChurchId       = ChurchId Guid.Empty | ||||
|             Name           = ""  | ||||
|             Church         = Church.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 | ||||
| /// Functions to support small groups | ||||
| module SmallGroup = | ||||
|      | ||||
|     /// An empty small group | ||||
|     let empty = | ||||
|         {   Id          = SmallGroupId Guid.Empty | ||||
|             ChurchId    = ChurchId Guid.Empty | ||||
|             Name        = ""  | ||||
|             Preferences = ListPreferences.empty | ||||
|         } | ||||
| 
 | ||||
|     /// The DateTimeZone for the time zone ID for this small group | ||||
|     let timeZone group = | ||||
|         let tzId = TimeZoneId.toString group.Preferences.TimeZoneId | ||||
| @ -933,12 +448,25 @@ module SmallGroup = | ||||
|     /// Get the local date for this group | ||||
|     let localDateNow clock group = | ||||
|         (localTimeNow clock group).Date | ||||
|      | ||||
| 
 | ||||
| 
 | ||||
| /// Support functions for prayer requests | ||||
| /// Functions to support prayer requests | ||||
| 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? | ||||
|     let isExpired (asOf : LocalDate) group req = | ||||
|         match req.Expiration, req.RequestType with | ||||
| @ -958,21 +486,65 @@ module PrayerRequest = | ||||
|                 >= 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>] | ||||
| type SmallGroupInfo = | ||||
|     {   /// The ID of the small group | ||||
|         Id : string | ||||
| type User = | ||||
|     {   /// The ID of this user | ||||
|         Id : UserId | ||||
|          | ||||
|         /// The name of the small group | ||||
|         Name : string | ||||
|         /// The first name of this user | ||||
|         FirstName : string | ||||
|          | ||||
|         /// The name of the church to which the small group belongs | ||||
|         ChurchName : string | ||||
|         /// The last name of this user | ||||
|         LastName : string | ||||
|          | ||||
|         /// The ID of the time zone for the small group | ||||
|         TimeZoneId : TimeZoneId | ||||
|         /// The e-mail address of the user | ||||
|         Email : string | ||||
|          | ||||
|         /// Whether the small group has a publicly-available request list | ||||
|         IsPublic : bool | ||||
|         /// Whether this user is a PrayerTracker system administrator | ||||
|         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 | ||||
|         } | ||||
|  | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -7,18 +7,12 @@ | ||||
|   <ItemGroup> | ||||
|     <Compile Include="Entities.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> | ||||
|     <PackageReference Include="FSharp.EFCore.OptionConverter" Version="1.0.0" /> | ||||
|     <PackageReference Include="Giraffe" Version="6.0.0" /> | ||||
|     <PackageReference Include="Microsoft.FSharpLu" Version="0.11.7" /> | ||||
|     <PackageReference Include="NodaTime" Version="3.1.0" /> | ||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" /> | ||||
|     <PackageReference Include="NodaTime" Version="3.1.1" /> | ||||
|     <PackageReference Update="FSharp.Core" Version="6.0.5" /> | ||||
|     <PackageReference Include="Npgsql.FSharp" Version="5.3.0" /> | ||||
|     <PackageReference Include="Npgsql.NodaTime" Version="6.0.6" /> | ||||
|  | ||||
| @ -118,8 +118,6 @@ let listPreferencesTests = | ||||
|             Expect.isFalse mt.IsPublic "The isPublic flag should not have been set" | ||||
|             Expect.equal (TimeZoneId.toString mt.TimeZoneId) "America/Denver" | ||||
|                 "The default time zone should have been America/Denver" | ||||
|             Expect.equal (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.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.Email "" "The member e-mail address should have been blank" | ||||
|             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.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.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" { | ||||
|             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.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.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 [ | ||||
|             "LocalTimeNow adjusts the time ahead of UTC", | ||||
| @ -309,7 +297,6 @@ let smallGroupTests = | ||||
|                     { SmallGroup.empty with | ||||
|                         Preferences = { ListPreferences.empty with TimeZoneId = TimeZoneId "Europe/Berlin" } | ||||
|                     } | ||||
|                 let tz = DateTimeZoneProviders.Tzdb["Europe/Berlin"] | ||||
|                 Expect.isGreaterThan (SmallGroup.localTimeNow clock grp) (now.InUtc().LocalDateTime) | ||||
|                     "UTC to Europe/Berlin should have added hours" | ||||
|             "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>] | ||||
| let userTests = | ||||
|     testList "User" [ | ||||
| @ -359,9 +334,6 @@ let userTests = | ||||
|             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.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" { | ||||
|             let user = { User.empty with FirstName = "Unit"; LastName = "Test" } | ||||
| @ -376,7 +348,5 @@ let userSmallGroupTests = | ||||
|             let mt = UserSmallGroup.empty | ||||
|             Expect.equal mt.UserId.Value Guid.Empty "The user ID should have been an empty GUID" | ||||
|             Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID" | ||||
|             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" | ||||
|         } | ||||
|     ] | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <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 Update="FSharp.Core" Version="6.0.5" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| @ -543,9 +543,9 @@ let requestListTests = | ||||
|                 let curReqHtml = | ||||
|                     [ "<ul>" | ||||
|                       """<li style="list-style-type:circle;font-family:Century Gothic,Tahoma,Luxi Sans,sans-serif;font-size:12pt;padding-bottom:.25em;">""" | ||||
|                       "<strong>Zeb</strong> — zyx</li>" | ||||
|                       "<strong>Zeb</strong> – zyx</li>" | ||||
|                       """<li style="list-style-type:disc;font-family:Century Gothic,Tahoma,Luxi Sans,sans-serif;font-size:12pt;padding-bottom:.25em;">""" | ||||
|                       "<strong>Aaron</strong> — abc</li></ul>" | ||||
|                       "<strong>Aaron</strong> – abc</li></ul>" | ||||
|                     ] | ||||
|                     |> String.concat "" | ||||
|                 Expect.stringContains html curReqHtml """Expected HTML for "Current Requests" requests not found""" | ||||
| @ -582,7 +582,7 @@ let requestListTests = | ||||
|                     |> String.concat "" | ||||
|                 Expect.stringContains html lstHeading "Expected HTML for the list heading not found" | ||||
|                 // spot check; without header test tests this exhaustively | ||||
|                 Expect.stringContains html "<strong>Zeb</strong> — zyx</li>" "Expected requests not found" | ||||
|                 Expect.stringContains html "<strong>Zeb</strong> – zyx</li>" "Expected requests not found" | ||||
|             "AsHtml succeeds with short as-of date", | ||||
|             fun reqList -> | ||||
|                 let htmlList = | ||||
| @ -595,7 +595,7 @@ let requestListTests = | ||||
|                 let html     = htmlList.AsHtml _s | ||||
|                 let expected = | ||||
|                     htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("d", null) | ||||
|                     |> sprintf """<strong>Zeb</strong> — zyx<i style="font-size:9.60pt">  (as of %s)</i>""" | ||||
|                     |> sprintf """<strong>Zeb</strong> – zyx<i style="font-size:9.60pt">  (as of %s)</i>""" | ||||
|                 // spot check; if one request has it, they all should | ||||
|                 Expect.stringContains html expected "Expected short as-of date not found"     | ||||
|             "AsHtml succeeds with long as-of date", | ||||
| @ -610,7 +610,7 @@ let requestListTests = | ||||
|                 let html     = htmlList.AsHtml _s | ||||
|                 let expected = | ||||
|                     htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("D", null) | ||||
|                     |> sprintf """<strong>Zeb</strong> — zyx<i style="font-size:9.60pt">  (as of %s)</i>""" | ||||
|                     |> sprintf """<strong>Zeb</strong> – zyx<i style="font-size:9.60pt">  (as of %s)</i>""" | ||||
|                 // spot check; if one request has it, they all should | ||||
|                 Expect.stringContains html expected "Expected long as-of date not found"     | ||||
|             "AsText succeeds with no as-of date", | ||||
|  | ||||
| @ -14,13 +14,12 @@ type RequestStartMiddleware (next : RequestDelegate) = | ||||
| open System | ||||
| open Microsoft.AspNetCore.Builder | ||||
| open Microsoft.AspNetCore.Hosting | ||||
| open Microsoft.Extensions.Configuration | ||||
| 
 | ||||
| /// Module to hold configuration for the web app | ||||
| [<RequireQualifiedAccess>] | ||||
| module Configure = | ||||
|    | ||||
|     open Microsoft.Extensions.Configuration | ||||
| 
 | ||||
|     /// Set up the configuration for the app | ||||
|     let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) = | ||||
|         cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath) | ||||
| @ -39,7 +38,6 @@ module Configure = | ||||
|     open System.IO | ||||
|     open Microsoft.AspNetCore.Authentication.Cookies | ||||
|     open Microsoft.AspNetCore.Localization | ||||
|     open Microsoft.EntityFrameworkCore | ||||
|     open Microsoft.Extensions.DependencyInjection | ||||
|     open NeoSmart.Caching.Sqlite | ||||
|     open NodaTime | ||||
| @ -69,15 +67,6 @@ module Configure = | ||||
|         let _ = svc.AddAntiforgery () | ||||
|         let _ = svc.AddRouting () | ||||
|         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 | ||||
| @ -194,10 +183,6 @@ module Configure = | ||||
|             let _ = app.UseDeveloperExceptionPage () | ||||
|             () | ||||
|         else | ||||
|             try | ||||
|                 use scope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope () | ||||
|                 scope.ServiceProvider.GetService<AppDbContext>().Database.Migrate () | ||||
|             with _ -> () // om nom nom | ||||
|             let _ = app.UseGiraffeErrorHandler errorHandler | ||||
|             () | ||||
|          | ||||
| @ -219,34 +204,55 @@ module Configure = | ||||
| module App = | ||||
|      | ||||
|     open System.Text | ||||
|     open Microsoft.EntityFrameworkCore | ||||
|     open Microsoft.Extensions.DependencyInjection | ||||
|     open Npgsql | ||||
|     open Npgsql.FSharp | ||||
|     open PrayerTracker.Entities | ||||
|      | ||||
|     let migratePasswords (app : IWebHost) = | ||||
|         task { | ||||
|             use db = app.Services.GetService<AppDbContext>() | ||||
|             let! v1Users = db.Users.FromSqlRaw("SELECT * FROM pt.pt_user WHERE salt IS NULL").ToListAsync () | ||||
|             for user in v1Users do | ||||
|             let config = app.Services.GetService<IConfiguration> () | ||||
|             use conn   = new NpgsqlConnection (config.GetConnectionString "PrayerTracker") | ||||
|             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 = | ||||
|                     [|  254uy  | ||||
|                         yield! (Encoding.UTF8.GetBytes user.PasswordHash) | ||||
|                         yield! (Encoding.UTF8.GetBytes oldHash) | ||||
|                     |] | ||||
|                     |> Convert.ToBase64String | ||||
|                 db.UpdateEntry { user with PasswordHash = pw } | ||||
|             let! v1Count = db.SaveChangesAsync () | ||||
|             printfn $"Updated {v1Count} users with version 1 password" | ||||
|                 let! _ = | ||||
|                     conn | ||||
|                     |> 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 = | ||||
|                 db.Users.FromSqlRaw("SELECT * FROM pt.pt_user WHERE salt IS NOT NULL").ToListAsync () | ||||
|             for user in v2Users do | ||||
|                 conn | ||||
|                 |> 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 = | ||||
|                     [|  255uy | ||||
|                         yield! (user.Salt.Value.ToByteArray ()) | ||||
|                         yield! (Encoding.UTF8.GetBytes user.PasswordHash) | ||||
|                         yield! (salt.ToByteArray ()) | ||||
|                         yield! (Encoding.UTF8.GetBytes oldHash) | ||||
|                     |] | ||||
|                     |> Convert.ToBase64String | ||||
|                 db.UpdateEntry { user with PasswordHash = pw } | ||||
|             let! v2Count = db.SaveChangesAsync () | ||||
|             printfn $"Updated {v2Count} users with version 2 password" | ||||
|                 let! _ = | ||||
|                     conn | ||||
|                     |> 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 | ||||
|      | ||||
|     open System.IO | ||||
|  | ||||
| @ -39,8 +39,7 @@ type ISession with | ||||
|       with get () = this.GetObject<User> Key.Session.currentUser |> Option.fromObject | ||||
|        and set (v : User option) = | ||||
|           match v with | ||||
|           | Some user -> | ||||
|               this.SetObject Key.Session.currentUser { user with PasswordHash = ""; SmallGroups = ResizeArray() } | ||||
|           | Some user -> this.SetObject Key.Session.currentUser { user with PasswordHash = "" } | ||||
|           | None -> this.Remove Key.Session.currentUser | ||||
|      | ||||
|     /// Current messages for the session | ||||
| @ -74,7 +73,6 @@ open System.Threading.Tasks | ||||
| open Giraffe | ||||
| open Microsoft.Extensions.Configuration | ||||
| open Npgsql | ||||
| open PrayerTracker | ||||
| 
 | ||||
| /// Extensions on the ASP.NET Core HTTP context | ||||
| type HttpContext with | ||||
| @ -87,9 +85,6 @@ type HttpContext with | ||||
|         return conn | ||||
|     }) | ||||
|      | ||||
|     /// The EF Core database context (via DI) | ||||
|     member this.Db = this.GetService<AppDbContext> () | ||||
|      | ||||
|     /// The PostgreSQL connection (configured via DI) | ||||
|     member this.Conn = backgroundTask { | ||||
|         return! this.LazyConn.Force () | ||||
|  | ||||
| @ -126,7 +126,7 @@ let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task | ||||
|     | Ok req -> | ||||
|         let  s    = Views.I18N.localizer.Force () | ||||
|         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 ()] | ||||
|         return! redirectTo false "/prayer-requests" next ctx | ||||
|     | Result.Error e -> return! e | ||||
| @ -177,16 +177,16 @@ let lists : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx | ||||
| ///  - OR - | ||||
| /// GET /prayer-requests?search=[search-query] | ||||
| let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { | ||||
|     // TODO: stopped here | ||||
|     let group   = ctx.Session.CurrentGroup.Value | ||||
|     let pageNbr = | ||||
|         match ctx.GetQueryStringValue "page" with | ||||
|         | Ok pg -> match Int32.TryParse pg with true, p -> p | false, _ -> 1 | ||||
|         | Result.Error _ -> 1 | ||||
|     let! model = backgroundTask { | ||||
|         let! conn = ctx.Conn | ||||
|         match ctx.GetQueryStringValue "search" with | ||||
|         | Ok search -> | ||||
|             let! reqs = ctx.Db.SearchRequestsForSmallGroup group search pageNbr | ||||
|             let! reqs = PrayerRequests.searchForGroup group search pageNbr conn | ||||
|             return | ||||
|                 { MaintainRequests.empty with | ||||
|                     Requests   = reqs | ||||
| @ -194,7 +194,14 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx | ||||
|                     PageNbr    = Some pageNbr | ||||
|                 } | ||||
|         | 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 | ||||
|                 { MaintainRequests.empty with | ||||
|                     Requests   = reqs | ||||
| @ -221,9 +228,9 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas | ||||
|     let requestId = PrayerRequestId reqId | ||||
|     match! findRequest ctx requestId with | ||||
|     | Ok req -> | ||||
|         let s  = Views.I18N.localizer.Force () | ||||
|         ctx.Db.UpdateEntry { req with Expiration = Automatic; UpdatedDate = ctx.Now } | ||||
|         let! _ = ctx.Db.SaveChangesAsync () | ||||
|         let  s    = Views.I18N.localizer.Force () | ||||
|         let! conn = ctx.Conn | ||||
|         do! PrayerRequests.updateExpiration { req with Expiration = Automatic; UpdatedDate = ctx.Now } true conn | ||||
|         addInfo ctx s["Successfully {0} prayer request", s["Restored"].Value.ToLower ()] | ||||
|         return! redirectTo false "/prayer-requests" next ctx | ||||
|     | Result.Error e -> return! e | ||||
| @ -235,41 +242,42 @@ open System.Threading.Tasks | ||||
| let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { | ||||
|     match! ctx.TryBindFormAsync<EditRequest> () with | ||||
|     | Ok model -> | ||||
|         let! req = | ||||
|           if model.IsNew then | ||||
|               Task.FromResult (Some { PrayerRequest.empty with Id = (Guid.NewGuid >> PrayerRequestId) () }) | ||||
|           else ctx.Db.TryRequestById (idFromShort PrayerRequestId model.RequestId) | ||||
|         let  group = ctx.Session.CurrentGroup.Value | ||||
|         let! conn  = ctx.Conn | ||||
|         let! req   = | ||||
|             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 | ||||
|         | Some pr -> | ||||
|             let upd8 = | ||||
|         | Some pr when pr.SmallGroupId = group.Id -> | ||||
|             let now  = SmallGroup.localDateNow ctx.Clock group | ||||
|             let updated = | ||||
|                 { pr with | ||||
|                     RequestType = PrayerRequestType.fromCode model.RequestType | ||||
|                     Requestor   = match model.Requestor with Some x when x.Trim () = "" -> None | x -> x | ||||
|                     Text        = ckEditorToText model.Text | ||||
|                     Expiration  = Expiration.fromCode model.Expiration | ||||
|                 } | ||||
|             let group = ctx.Session.CurrentGroup.Value | ||||
|             let now   = SmallGroup.localDateNow ctx.Clock group | ||||
|             match model.IsNew with | ||||
|             | true -> | ||||
|                 let dt = | ||||
|                     (defaultArg (parseListDate model.EnteredDate) now) | ||||
|                         .AtStartOfDayInZone(SmallGroup.timeZone group) | ||||
|                         .ToInstant() | ||||
|                 { upd8 with | ||||
|                     SmallGroupId = group.Id | ||||
|                     UserId       = ctx.User.UserId.Value | ||||
|                     EnteredDate  = dt | ||||
|                     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" | ||||
|                 |> function | ||||
|                 | it when model.IsNew -> | ||||
|                     let dt = | ||||
|                         (defaultArg (parseListDate model.EnteredDate) now) | ||||
|                             .AtStartOfDayInZone(SmallGroup.timeZone group) | ||||
|                             .ToInstant() | ||||
|                     { it with EnteredDate = dt; UpdatedDate = dt } | ||||
|                 | it when defaultArg model.SkipDateUpdate false -> it | ||||
|                 | it -> { it with UpdatedDate = ctx.Now } | ||||
|             do! PrayerRequests.save updated conn | ||||
|             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 ()] | ||||
|             return! redirectTo false "/prayer-requests" next ctx | ||||
|         | Some _ | ||||
|         | None -> return! fourOhFour ctx | ||||
|     | Result.Error e -> return! bindError e next ctx | ||||
| } | ||||
|  | ||||
| @ -28,9 +28,7 @@ | ||||
|     <PackageReference Include="Giraffe.Htmx" Version="1.8.0" /> | ||||
|     <PackageReference Include="NeoSmart.Caching.Sqlite" Version="6.0.1" /> | ||||
|     <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 Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="6.0.6" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user