From 42976da1bde384ba2478df95c76a00037982bfa4 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 13 Aug 2022 22:35:48 -0400 Subject: [PATCH] WIP on SQL migration - Restructure to eliminate time zone table --- src/PrayerTracker.Data/Access.fs | 183 +++++++++++++++++++++++- src/PrayerTracker.Data/Entities.fs | 5 +- src/PrayerTracker.UI/CommonFunctions.fs | 29 ++-- src/PrayerTracker.UI/PrayerRequest.fs | 11 +- src/PrayerTracker.UI/SmallGroup.fs | 7 +- src/PrayerTracker.UI/Utils.fs | 7 + src/PrayerTracker/PrayerRequest.fs | 45 ++++-- src/PrayerTracker/SmallGroup.fs | 101 ++++++------- 8 files changed, 297 insertions(+), 91 deletions(-) diff --git a/src/PrayerTracker.Data/Access.fs b/src/PrayerTracker.Data/Access.fs index 723e391..e554df1 100644 --- a/src/PrayerTracker.Data/Access.fs +++ b/src/PrayerTracker.Data/Access.fs @@ -86,6 +86,7 @@ module private Helpers = Name = row.string "group_name" ChurchName = row.string "church_name" TimeZoneId = TimeZoneId (row.string "time_zone_id") + IsPublic = row.bool "is_public" } /// Map a row to a Small Group list item @@ -207,6 +208,30 @@ module Members = |> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ] |> Sql.executeAsync mapToMember + /// Save a small group member + let save (mbr : Member) conn = backgroundTask { + let! _ = + conn + |> Sql.existingConnection + |> Sql.query """ + INSERT INTO pt.member ( + id, small_group_id, member_name, email, email_format + ) VALUES ( + @id, @groupId, @name, @email, @format + ) ON CONFLICT (id) DO UPDATE + SET member_name = EXCLUDED.member_name, + email = EXCLUDED.email, + email_format = EXCLUDED.email_format""" + |> Sql.parameters + [ "@id", Sql.uuid mbr.Id.Value + "@groupId", Sql.uuid mbr.SmallGroupId.Value + "@name", Sql.string mbr.Name + "@email", Sql.string mbr.Email + "@format", Sql.stringOrNone (mbr.Format |> Option.map EmailFormat.toCode) ] + |> Sql.executeNonQueryAsync + return () + } + /// Retrieve a small group member by its ID let tryById (memberId : MemberId) conn = backgroundTask { let! mbr = @@ -270,6 +295,17 @@ module PrayerRequests = |> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ] |> Sql.executeRowAsync (fun row -> row.int "req_count") + /// Delete a prayer request by its ID + let deleteById (reqId : PrayerRequestId) conn = backgroundTask { + let! _ = + conn + |> Sql.existingConnection + |> Sql.query "DELETE FROM pt.prayer_request WHERE id = @id" + |> Sql.parameters [ "@id", Sql.uuid reqId.Value ] + |> Sql.executeNonQueryAsync + return () + } + /// Get all (or active) requests for a small group as of now or the specified date let forGroup (opts : PrayerRequestOptions) conn = let theDate = defaultArg opts.ListDate (SmallGroup.localDateNow opts.Clock opts.SmallGroup) @@ -301,6 +337,65 @@ module PrayerRequests = {paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}""" |> Sql.parameters (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) |> Sql.executeAsync mapToPrayerRequest + + /// Save a prayer request + let save (req : PrayerRequest) conn = backgroundTask { + let! _ = + conn + |> Sql.existingConnection + |> Sql.query """ + INSERT into pt.prayer_request ( + id, request_type, user_id, small_group_id, entered_date, updated_date, requestor, request_text, + notify_chaplain, expiration + ) VALUES ( + @id, @type, @userId, @groupId, @entered, @updated, @requestor, @text, + @notifyChaplain, @expiration + ) ON CONFLICT (id) DO UPDATE + SET request_type = EXCLUDED.request_type, + updated_date = EXCLUDED.updated_date, + requestor = EXCLUDED.requestor, + request_text = EXCLUDED.request_text, + notify_chaplain = EXCLUDED.notify_chaplain, + expiration = EXCLUDED.expiration""" + |> Sql.parameters + [ "@id", Sql.uuid req.Id.Value + "@type", Sql.string (PrayerRequestType.toCode req.RequestType) + "@userId", Sql.uuid req.UserId.Value + "@groupId", Sql.uuid req.SmallGroupId.Value + "@entered", Sql.parameter (NpgsqlParameter ("@entered", req.EnteredDate)) + "@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate)) + "@requestor", Sql.stringOrNone req.Requestor + "@text", Sql.string req.Text + "@notifyChaplain", Sql.bool req.NotifyChaplain + "@expiration", Sql.string (Expiration.toCode req.Expiration) + ] + |> Sql.executeNonQueryAsync + return () + } + + /// Retrieve a prayer request by its ID + let tryById (reqId : PrayerRequestId) conn = backgroundTask { + let! req = + conn + |> Sql.existingConnection + |> Sql.query "SELECT * FROM pt.prayer_request WHERE id = @id" + |> Sql.parameters [ "@id", Sql.uuid reqId.Value ] + |> Sql.executeAsync mapToPrayerRequest + return List.tryHead req + } + + /// Update the expiration for the given prayer request + let updateExpiration (req : PrayerRequest) conn = backgroundTask { + let! _ = + conn + |> Sql.existingConnection + |> Sql.query "UPDATE pt.prayer_request SET expiration = @expiration WHERE id = @id" + |> Sql.parameters + [ "@expiration", Sql.string (Expiration.toCode req.Expiration) + "@id", Sql.uuid req.Id.Value ] + |> Sql.executeNonQueryAsync + return () + } /// Functions to retrieve small group information @@ -356,7 +451,7 @@ module SmallGroups = conn |> Sql.existingConnection |> Sql.query """ - SELECT g.group_name, g.id, c.church_name + SELECT g.group_name, g.id, c.church_name, lp.is_public FROM pt.small_group g INNER JOIN pt.church c ON c.id = g.church_id INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id @@ -364,6 +459,20 @@ module SmallGroups = ORDER BY c.church_name, g.group_name""" |> Sql.executeAsync mapToSmallGroupItem + /// Get a list of small group IDs and descriptions for groups that are public or have a group password + let listPublicAndProtected conn = + conn + |> Sql.existingConnection + |> Sql.query """ + SELECT g.group_name, g.id, c.church_name, lp.is_public + FROM pt.small_group g + INNER JOIN pt.church c ON c.id = g.church_id + INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id + WHERE lp.is_public = TRUE + OR COALESCE(lp.group_password, '') <> '' + ORDER BY c.church_name, g.group_name""" + |> Sql.executeAsync mapToSmallGroupInfo + /// Log on for a small group (includes list preferences) let logOn (groupId : SmallGroupId) password conn = backgroundTask { let! group = @@ -380,6 +489,78 @@ module SmallGroups = return List.tryHead group } + /// Save a small group + let save (group : SmallGroup) isNew conn = backgroundTask { + let! _ = + conn + |> Sql.existingConnection + |> Sql.executeTransactionAsync [ + """ INSERT INTO pt.small_group ( + id, church_id, group_name + ) VALUES ( + @id, @churchId, @name + ) ON CONFLICT (id) DO UPDATE + SET church_id = EXCLUDED.church_id, + group_name = EXCLUDED.group_name""", + [ [ "@id", Sql.uuid group.Id.Value + "@churchId", Sql.uuid group.ChurchId.Value + "@name", Sql.string group.Name ] ] + if isNew then + "INSERT INTO pt.list_preference (small_group_id) VALUES (@id)", + [ [ "@id", Sql.uuid group.Id.Value ] ] + ] + return () + } + + /// Save a small group's list preferences + let savePreferences (pref : ListPreferences) conn = backgroundTask { + let! _ = + conn + |> Sql.existingConnection + |> Sql.query """ + UPDATE pt.list_preference + SET days_to_keep_new = @daysToKeepNew, + days_to_expire = @daysToExpire, + long_term_update_weeks = @longTermUpdateWeeks, + email_from_name = @emailFromName, + email_from_address = @emailFromAddress, + fonts = @fonts, + heading_color = @headingColor, + line_color = @lineColor, + heading_font_size = @headingFontSize, + text_font_size = @textFontSize, + request_sort = @requestSort, + group_password = @groupPassword, + default_email_type = @defaultEmailType, + is_public = @isPublic, + time_zone_id = @timeZoneId, + page_size = @pageSize, + as_of_date_display = @asOfDateDisplay + WHERE small_group_id = @groupId""" + |> Sql.parameters + [ "@groupId", Sql.uuid pref.SmallGroupId.Value + "@daysToKeepNew", Sql.int pref.DaysToKeepNew + "@daysToExpire", Sql.int pref.DaysToExpire + "@longTermUpdateWeeks", Sql.int pref.LongTermUpdateWeeks + "@emailFromName", Sql.string pref.EmailFromName + "@emailFromAddress", Sql.string pref.EmailFromAddress + "@fonts", Sql.string pref.Fonts + "@headingColor", Sql.string pref.HeadingColor + "@lineColor", Sql.string pref.LineColor + "@headingFontSize", Sql.int pref.HeadingFontSize + "@textFontSize", Sql.int pref.TextFontSize + "@requestSort", Sql.string (RequestSort.toCode pref.RequestSort) + "@groupPassword", Sql.string pref.GroupPassword + "@defaultEmailType", Sql.string (EmailFormat.toCode pref.DefaultEmailType) + "@isPublic", Sql.bool pref.IsPublic + "@timeZoneId", Sql.string (TimeZoneId.toString pref.TimeZoneId) + "@pageSize", Sql.int pref.PageSize + "@asOfDateDisplay", Sql.string (AsOfDateDisplay.toCode pref.AsOfDateDisplay) + ] + |> Sql.executeNonQueryAsync + return () + } + /// Get a small group by its ID let tryById (groupId : SmallGroupId) conn = backgroundTask { let! group = diff --git a/src/PrayerTracker.Data/Entities.fs b/src/PrayerTracker.Data/Entities.fs index 37c16e6..2a1ecf7 100644 --- a/src/PrayerTracker.Data/Entities.fs +++ b/src/PrayerTracker.Data/Entities.fs @@ -958,7 +958,7 @@ module PrayerRequest = >= req.UpdatedDate.InZone(SmallGroup.timeZone group).Date -/// Information needed to display the small group maintenance page +/// Information needed to display the public/protected request list and small group maintenance pages [] type SmallGroupInfo = { /// The ID of the small group @@ -972,4 +972,7 @@ type SmallGroupInfo = /// The ID of the time zone for the small group TimeZoneId : TimeZoneId + + /// Whether the small group has a publicly-available request list + IsPublic : bool } diff --git a/src/PrayerTracker.UI/CommonFunctions.fs b/src/PrayerTracker.UI/CommonFunctions.fs index 003965a..8dabb0c 100644 --- a/src/PrayerTracker.UI/CommonFunctions.fs +++ b/src/PrayerTracker.UI/CommonFunctions.fs @@ -156,25 +156,28 @@ let renderHtmlString = renderHtmlNode >> HtmlString /// Utility methods to help with time zones (and localization of their names) module TimeZones = - open System.Collections.Generic open PrayerTracker.Entities /// Cross-reference between time zone Ids and their English names - let private xref = - [ "America/Chicago", "Central" - "America/Denver", "Mountain" - "America/Los_Angeles", "Pacific" - "America/New_York", "Eastern" - "America/Phoenix", "Mountain (Arizona)" - "Europe/Berlin", "Central European" - ] - |> Map.ofList + let private xref = [ + TimeZoneId "America/Chicago", "Central" + TimeZoneId "America/Denver", "Mountain" + TimeZoneId "America/Los_Angeles", "Pacific" + TimeZoneId "America/New_York", "Eastern" + TimeZoneId "America/Phoenix", "Mountain (Arizona)" + TimeZoneId "Europe/Berlin", "Central European" + ] /// Get the name of a time zone, given its Id let name timeZoneId (s : IStringLocalizer) = - let tzId = TimeZoneId.toString timeZoneId - try s[xref[tzId]] - with :? KeyNotFoundException -> LocalizedString (tzId, tzId) + match xref |> List.tryFind (fun it -> fst it = timeZoneId) with + | Some tz -> s[snd tz] + | None -> + let tzId = TimeZoneId.toString timeZoneId + LocalizedString (tzId, tzId) + + /// All known time zones in their defined order + let all = xref |> List.map fst open Giraffe.ViewEngine.Htmx diff --git a/src/PrayerTracker.UI/PrayerRequest.fs b/src/PrayerTracker.UI/PrayerRequest.fs index 1823ead..a2a7f7d 100644 --- a/src/PrayerTracker.UI/PrayerRequest.fs +++ b/src/PrayerTracker.UI/PrayerRequest.fs @@ -106,7 +106,7 @@ let list (model : RequestList) viewInfo = /// View for the prayer request lists page -let lists (groups : SmallGroup list) viewInfo = +let lists (groups : SmallGroupInfo list) viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "Requests/Lists" use sw = new StringWriter () @@ -126,17 +126,16 @@ let lists (groups : SmallGroup list) viewInfo = tableHeadings s [ "Actions"; "Church"; "Group" ] groups |> List.map (fun grp -> - let grpId = shortGuid grp.Id.Value tr [] [ - if grp.Preferences.IsPublic then - a [ _href $"/prayer-requests/{grpId}/list"; _title s["View"].Value ] [ icon "list" ] + if grp.IsPublic then + a [ _href $"/prayer-requests/{grp.Id}/list"; _title s["View"].Value ] [ icon "list" ] else - a [ _href $"/small-group/log-on/{grpId}"; _title s["Log On"].Value ] [ + a [ _href $"/small-group/log-on/{grp.Id}"; _title s["Log On"].Value ] [ icon "verified_user" ] |> List.singleton |> td [] - td [] [ str grp.Church.Name ] + td [] [ str grp.ChurchName ] td [] [ str grp.Name ] ]) |> tbody [] diff --git a/src/PrayerTracker.UI/SmallGroup.fs b/src/PrayerTracker.UI/SmallGroup.fs index ec78ffb..e231561 100644 --- a/src/PrayerTracker.UI/SmallGroup.fs +++ b/src/PrayerTracker.UI/SmallGroup.fs @@ -351,7 +351,7 @@ let overview model viewInfo = open System.IO /// View for the small group preferences page -let preferences (model : EditPreferences) (tzs : TimeZone list) ctx viewInfo = +let preferences (model : EditPreferences) ctx viewInfo = let s = I18N.localizer.Force () let l = I18N.forView "SmallGroup/Preferences" use sw = new StringWriter () @@ -518,9 +518,8 @@ let preferences (model : EditPreferences) (tzs : TimeZone list) ctx viewInfo = seq { "", selectDefault s["Select"].Value yield! - tzs - |> List.map (fun tz -> - TimeZoneId.toString tz.Id, (TimeZones.name tz.Id s).Value) + TimeZones.all + |> List.map (fun tz -> TimeZoneId.toString tz, (TimeZones.name tz s).Value) } |> selectList (nameof model.TimeZone) model.TimeZone [ _required ] ] diff --git a/src/PrayerTracker.UI/Utils.fs b/src/PrayerTracker.UI/Utils.fs index c904b4e..941846d 100644 --- a/src/PrayerTracker.UI/Utils.fs +++ b/src/PrayerTracker.UI/Utils.fs @@ -32,6 +32,13 @@ module String = match haystack.IndexOf needle with | -1 -> haystack | idx -> String.concat "" [ haystack[0..idx - 1]; replacement; haystack[idx + needle.Length..] ] + + /// Convert a string to an option, with null, blank, and whitespace becoming None + let noneIfBlank (str : string) = + match str with + | null -> None + | it when it.Trim () = "" -> None + | it -> Some it open System.Text.RegularExpressions diff --git a/src/PrayerTracker/PrayerRequest.fs b/src/PrayerTracker/PrayerRequest.fs index 6f477c2..4dd8ce3 100644 --- a/src/PrayerTracker/PrayerRequest.fs +++ b/src/PrayerTracker/PrayerRequest.fs @@ -3,12 +3,14 @@ open Giraffe open Microsoft.AspNetCore.Http open PrayerTracker +open PrayerTracker.Data open PrayerTracker.Entities open PrayerTracker.ViewModels /// Retrieve a prayer request, and ensure that it belongs to the current class let private findRequest (ctx : HttpContext) reqId = task { - match! ctx.Db.TryRequestById reqId with + let! conn = ctx.Conn + match! PrayerRequests.tryById reqId conn with | Some req when req.SmallGroupId = ctx.Session.CurrentGroup.Value.Id -> return Ok req | Some _ -> let s = Views.I18N.localizer.Force () @@ -21,7 +23,15 @@ let private findRequest (ctx : HttpContext) reqId = task { let private generateRequestList (ctx : HttpContext) date = task { let group = ctx.Session.CurrentGroup.Value let listDate = match date with Some d -> d | None -> SmallGroup.localDateNow ctx.Clock group - let! reqs = ctx.Db.AllRequestsForSmallGroup group ctx.Clock (Some listDate) true 0 + let! conn = ctx.Conn + let! reqs = + PrayerRequests.forGroup + { SmallGroup = group + Clock = ctx.Clock + ListDate = Some listDate + ActiveOnly = true + PageNumber = 0 + } conn return { Requests = reqs Date = listDate @@ -78,7 +88,8 @@ let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let listDate = parseListDate (Some date) let group = ctx.Session.CurrentGroup.Value let! list = generateRequestList ctx listDate - let! recipients = ctx.Db.AllMembersForSmallGroup group.Id + let! conn = ctx.Conn + let! recipients = Members.forGroup group.Id conn use! client = Email.getConnection () do! Email.sendEmails { Client = client @@ -100,9 +111,9 @@ let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun let requestId = PrayerRequestId reqId match! findRequest ctx requestId with | Ok req -> - let s = Views.I18N.localizer.Force () - ctx.Db.PrayerRequests.Remove req |> ignore - let! _ = ctx.Db.SaveChangesAsync () + let s = Views.I18N.localizer.Force () + let! conn = ctx.Conn + do! PrayerRequests.deleteById req.Id conn addInfo ctx s["The prayer request was deleted successfully"] return! redirectTo false "/prayer-requests" next ctx | Result.Error e -> return! e @@ -113,9 +124,9 @@ let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task let requestId = PrayerRequestId reqId match! findRequest ctx requestId with | Ok req -> - let s = Views.I18N.localizer.Force () - ctx.Db.UpdateEntry { req with Expiration = Forced } - let! _ = ctx.Db.SaveChangesAsync () + let s = Views.I18N.localizer.Force () + let! conn = ctx.Conn + do! PrayerRequests.updateExpiration { req with Expiration = Forced } conn addInfo ctx s["Successfully {0} prayer request", s["Expired"].Value.ToLower ()] return! redirectTo false "/prayer-requests" next ctx | Result.Error e -> return! e @@ -123,9 +134,17 @@ let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task /// GET /prayer-requests/[group-id]/list let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { - match! ctx.Db.TryGroupById groupId with + let! conn = ctx.Conn + match! SmallGroups.tryByIdWithPreferences groupId conn with | Some group when group.Preferences.IsPublic -> - let! reqs = ctx.Db.AllRequestsForSmallGroup group ctx.Clock None true 0 + let! reqs = + PrayerRequests.forGroup + { SmallGroup = group + Clock = ctx.Clock + ListDate = None + ActiveOnly = true + PageNumber = 0 + } conn return! viewInfo ctx |> Views.PrayerRequest.list @@ -146,7 +165,8 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne /// GET /prayer-requests/lists let lists : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { - let! groups = ctx.Db.PublicAndProtectedGroups () + let! conn = ctx.Conn + let! groups = SmallGroups.listPublicAndProtected conn return! viewInfo ctx |> Views.PrayerRequest.lists groups @@ -157,6 +177,7 @@ 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 diff --git a/src/PrayerTracker/SmallGroup.fs b/src/PrayerTracker/SmallGroup.fs index 643ad78..ae259e7 100644 --- a/src/PrayerTracker/SmallGroup.fs +++ b/src/PrayerTracker/SmallGroup.fs @@ -174,7 +174,8 @@ let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { |> Seq.ofList |> Seq.map (fun req -> req.RequestType) |> Seq.distinct - |> Seq.map (fun reqType -> reqType, reqs |> List.filter (fun r -> r.RequestType = reqType) |> List.length) + |> Seq.map (fun reqType -> + reqType, reqs |> List.filter (fun r -> r.RequestType = reqType) |> List.length) |> Map.ofSeq) Admins = admins } @@ -186,12 +187,9 @@ let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { /// GET /small-group/preferences let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { - // TODO: stopped here - let group = ctx.Session.CurrentGroup.Value - let! tzs = ctx.Db.AllTimeZones () return! { viewInfo ctx with HelpLink = Some Help.groupPreferences } - |> Views.SmallGroup.preferences (EditPreferences.fromPreferences group.Preferences) tzs ctx + |> Views.SmallGroup.preferences (EditPreferences.fromPreferences ctx.Session.CurrentGroup.Value.Preferences) ctx |> renderHtml next ctx } @@ -201,19 +199,14 @@ open System.Threading.Tasks let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { match! ctx.TryBindFormAsync () with | Ok model -> - let s = Views.I18N.localizer.Force () - let! group = + let s = Views.I18N.localizer.Force () + let! conn = ctx.Conn + let! tryGroup = if model.IsNew then Task.FromResult (Some { SmallGroup.empty with Id = (Guid.NewGuid >> SmallGroupId) () }) - else ctx.Db.TryGroupById (idFromShort SmallGroupId model.SmallGroupId) - match group with - | Some grp -> - model.populateGroup grp - |> function - | grp when model.IsNew -> - ctx.Db.AddEntry grp - ctx.Db.AddEntry { grp.Preferences with SmallGroupId = grp.Id } - | grp -> ctx.Db.UpdateEntry grp - let! _ = ctx.Db.SaveChangesAsync () + else SmallGroups.tryById (idFromShort SmallGroupId model.SmallGroupId) conn + match tryGroup with + | Some group -> + do! SmallGroups.save (model.populateGroup group) model.IsNew conn let act = s[if model.IsNew then "Added" else "Updated"].Value.ToLower () addHtmlInfo ctx s["Successfully {0} group “{1}”", act, model.Name] return! redirectTo false "/small-groups" next ctx @@ -225,21 +218,21 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { match! ctx.TryBindFormAsync () with | Ok model -> - let group = ctx.Session.CurrentGroup.Value - let! mMbr = + let group = ctx.Session.CurrentGroup.Value + let! conn = ctx.Conn + let! tryMbr = if model.IsNew then Task.FromResult (Some { Member.empty with Id = (Guid.NewGuid >> MemberId) (); SmallGroupId = group.Id }) - else ctx.Db.TryMemberById (idFromShort MemberId model.MemberId) - match mMbr with + else Members.tryById (idFromShort MemberId model.MemberId) conn + match tryMbr with | Some mbr when mbr.SmallGroupId = group.Id -> - { mbr with - Name = model.Name - Email = model.Email - Format = match model.Format with "" | null -> None | _ -> Some (EmailFormat.fromCode model.Format) - } - |> if model.IsNew then ctx.Db.AddEntry else ctx.Db.UpdateEntry - let! _ = ctx.Db.SaveChangesAsync () - let s = Views.I18N.localizer.Force () + do! Members.save + { mbr with + Name = model.Name + Email = model.Email + Format = String.noneIfBlank model.Format |> Option.map EmailFormat.fromCode + } conn + let s = Views.I18N.localizer.Force () let act = s[if model.IsNew then "Added" else "Updated"].Value.ToLower () addInfo ctx s["Successfully {0} group member", act] return! redirectTo false "/small-group/members" next ctx @@ -255,14 +248,14 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> // Since the class is stored in the session, we'll use an intermediate instance to persist it; once that works, // we can repopulate the session instance. That way, if the update fails, the page should still show the // database values, not the then out-of-sync session ones. - let group = ctx.Session.CurrentGroup.Value - match! ctx.Db.TryGroupById group.Id with + let group = ctx.Session.CurrentGroup.Value + let! conn = ctx.Conn + match! SmallGroups.tryByIdWithPreferences group.Id conn with | Some grp -> - let prefs = model.PopulatePreferences grp.Preferences - ctx.Db.UpdateEntry prefs - let! _ = ctx.Db.SaveChangesAsync () + let pref = model.PopulatePreferences grp.Preferences + do! SmallGroups.savePreferences pref conn // Refresh session instance - ctx.Session.CurrentGroup <- Some { grp with Preferences = prefs } + ctx.Session.CurrentGroup <- Some { grp with Preferences = pref } let s = Views.I18N.localizer.Force () addInfo ctx s["Group preferences updated successfully"] return! redirectTo false "/small-group/preferences" next ctx @@ -289,10 +282,13 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> |> renderHtmlNode let plainText = (htmlToPlainText >> wordWrap 74) htmlText // Send the e-mails - let! recipients = - match model.SendToClass with - | "N" when usr.IsAdmin -> ctx.Db.AllUsersAsMembers () - | _ -> ctx.Db.AllMembersForSmallGroup group.Id + let! conn = ctx.Conn + let! recipients = task { + if model.SendToClass = "N" && usr.IsAdmin then + let! users = Users.all conn + return users |> List.map (fun u -> { Member.empty with Name = u.Name; Email = u.Email }) + else return! Members.forGroup group.Id conn + } use! client = Email.getConnection () do! Email.sendEmails { Client = client @@ -311,23 +307,20 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> | _, Some x when not x -> () | _, _ -> let zone = SmallGroup.timeZone group - { PrayerRequest.empty with - Id = (Guid.NewGuid >> PrayerRequestId) () - SmallGroupId = group.Id - UserId = usr.Id - RequestType = (Option.get >> PrayerRequestType.fromCode) model.RequestType - Text = requestText - EnteredDate = now.Date.AtStartOfDayInZone(zone).ToInstant() - UpdatedDate = now.InZoneLeniently(zone).ToInstant() - } - |> ctx.Db.AddEntry - let! _ = ctx.Db.SaveChangesAsync () - () + do! PrayerRequests.save + { PrayerRequest.empty with + Id = (Guid.NewGuid >> PrayerRequestId) () + SmallGroupId = group.Id + UserId = usr.Id + RequestType = (Option.get >> PrayerRequestType.fromCode) model.RequestType + Text = requestText + EnteredDate = now.Date.AtStartOfDayInZone(zone).ToInstant() + UpdatedDate = now.InZoneLeniently(zone).ToInstant() + } conn // Tell 'em what they've won, Johnny! let toWhom = - match model.SendToClass with - | "N" -> s["{0} users", s["PrayerTracker"]].Value - | _ -> s["Group Members"].Value.ToLower () + if model.SendToClass = "N" then s["{0} users", s["PrayerTracker"]].Value + else s["Group Members"].Value.ToLower () let andAdded = match model.AddToRequestList with Some x when x -> "and added it to the request list" | _ -> "" addInfo ctx s["Successfully sent announcement to all {0} {1}", toWhom, s[andAdded]] return!