diff --git a/src/PrayerTracker.Data/Access.fs b/src/PrayerTracker.Data/Access.fs index 8c38286..fe3f28b 100644 --- a/src/PrayerTracker.Data/Access.fs +++ b/src/PrayerTracker.Data/Access.fs @@ -12,7 +12,7 @@ module private Helpers = /// Map a row to a Church instance let mapToChurch (row : RowReader) = { Id = ChurchId (row.uuid "id") - Name = row.string "name" + Name = row.string "church_name" City = row.string "city" State = row.string "state" HasVpsInterface = row.bool "has_vps_interface" @@ -21,27 +21,37 @@ module private Helpers = /// Map a row to a ListPreferences instance let mapToListPreferences (row : RowReader) = - { SmallGroupId = SmallGroupId (row.uuid "small_group_id") - DaysToKeepNew = row.int "days_to_keep_new" - DaysToExpire = row.int "days_to_expire" - LongTermUpdateWeeks = row.int "long_term_update_weeks" - EmailFromName = row.string "email_from_name" - EmailFromAddress = row.string "email_from_address" - Fonts = row.string "fonts" - HeadingColor = row.string "heading_color" - LineColor = row.string "line_color" - HeadingFontSize = row.int "heading_font_size" - TextFontSize = row.int "text_font_size" - RequestSort = RequestSort.fromCode (row.string "request_sort") - GroupPassword = row.string "group_password" - DefaultEmailType = EmailFormat.fromCode (row.string "default_email_type") - IsPublic = row.bool "is_public" - TimeZoneId = TimeZoneId (row.string "time_zone_id") - PageSize = row.int "page_size" + { SmallGroupId = SmallGroupId (row.uuid "small_group_id") + DaysToKeepNew = row.int "days_to_keep_new" + DaysToExpire = row.int "days_to_expire" + LongTermUpdateWeeks = row.int "long_term_update_weeks" + EmailFromName = row.string "email_from_name" + EmailFromAddress = row.string "email_from_address" + Fonts = row.string "fonts" + HeadingColor = row.string "heading_color" + LineColor = row.string "line_color" + HeadingFontSize = row.int "heading_font_size" + TextFontSize = row.int "text_font_size" + GroupPassword = row.string "group_password" + IsPublic = row.bool "is_public" + PageSize = row.int "page_size" + TimeZoneId = TimeZoneId (row.string "time_zone_id") + 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 + let mapToMember (row : RowReader) = + { Id = MemberId (row.uuid "id") + SmallGroupId = SmallGroupId (row.uuid "small_group_id") + 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 Small Group instance let mapToSmallGroup (row : RowReader) = { Id = SmallGroupId (row.uuid "id") @@ -54,6 +64,10 @@ module private Helpers = Users = ResizeArray () } + /// Map a row to a Small Group list item + let mapToSmallGroupItem (row : RowReader) = + Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}""" + /// Map a row to a Small Group instance with populated list preferences let mapToSmallGroupWithPreferences (row : RowReader) = { mapToSmallGroup row with @@ -74,7 +88,60 @@ module private Helpers = } +/// Functions to manipulate churches module Churches = + + /// Get a list of all churches + let all conn = + conn + |> Sql.existingConnection + |> Sql.query "SELECT * FROM pt.church ORDER BY church_name" + |> Sql.executeAsync mapToChurch + + /// Delete a church by its ID + let deleteById (churchId : ChurchId) conn = backgroundTask { + let idParam = [ [ "@churchId", Sql.uuid churchId.Value ] ] + let where = "WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)" + let! _ = + conn + |> Sql.existingConnection + |> Sql.executeTransactionAsync + [ $"DELETE FROM pt.prayer_request {where}", idParam + $"DELETE FROM pt.user_small_group {where}", idParam + $"DELETE FROM pt.list_preference {where}", idParam + "DELETE FROM pt.small_group WHERE church_id = @churchId", idParam + "DELETE FROM pt.church WHERE id = @churchId", idParam ] + return () + } + + /// Save a church's information + let save (church : Church) conn = backgroundTask { + let! _ = + conn + |> Sql.existingConnection + |> Sql.query """ + INSERT INTO pt.church ( + id, church_name, city, state, has_vps_interface, interface_address + ) VALUES ( + @id, @name, @city, @state, @hasVpsInterface, @interfaceAddress + ) ON CONFLICT (id) DO UPDATE + SET church_name = EXCLUDED.church_name, + city = EXCLUDED.city, + state = EXCLUDED.state, + has_vps_interface = EXCLUDED.has_vps_interface, + interface_address = EXCLUDED.interface_address""" + |> Sql.parameters + [ "@id", Sql.uuid church.Id.Value + "@name", Sql.string church.Name + "@city", Sql.string church.City + "@state", Sql.string church.State + "@hasVpsInterface", Sql.bool church.HasVpsInterface + "@interfaceAddress", Sql.stringOrNone church.InterfaceAddress ] + |> Sql.executeNonQueryAsync + return () + } + + /// Find a church by its ID let tryById (churchId : ChurchId) conn = backgroundTask { let! church = conn @@ -86,9 +153,80 @@ module Churches = } +/// Functions to manipulate small group members +module Members = + + /// Delete a small group member by its ID + let deleteById (memberId : MemberId) conn = backgroundTask { + let! _ = + conn + |> Sql.existingConnection + |> Sql.query "DELETE FROM pt.member WHERE id = @id" + |> Sql.parameters [ "@id", Sql.uuid memberId.Value ] + |> Sql.executeNonQueryAsync + return () + } + + /// Retrieve a small group member by its ID + let tryById (memberId : MemberId) conn = backgroundTask { + let! mbr = + conn + |> Sql.existingConnection + |> Sql.query "SELECT * FROM pt.member WHERE id = @id" + |> Sql.parameters [ "@id", Sql.uuid memberId.Value ] + |> Sql.executeAsync mapToMember + return List.tryHead mbr + } + + +/// Functions to manipulate prayer requests +module PrayerRequests = + + /// Count the number of prayer requests for a church + let countByChurch (churchId : ChurchId) conn = + conn + |> Sql.existingConnection + |> Sql.query """ + SELECT COUNT(id) AS req_count + FROM pt.prayer_request + WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)""" + |> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ] + |> Sql.executeRowAsync (fun row -> row.int "req_count") + + /// Count the number of prayer requests for a small group + let countByGroup (groupId : SmallGroupId) conn = + conn + |> Sql.existingConnection + |> Sql.query "SELECT COUNT(id) AS req_count FROM pt.prayer_request WHERE small_group_id = @groupId" + |> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ] + |> Sql.executeRowAsync (fun row -> row.int "req_count") + + /// Functions to retrieve small group information module SmallGroups = + /// Count the number of small groups for a church + let countByChurch (churchId : ChurchId) conn = + conn + |> Sql.existingConnection + |> Sql.query "SELECT COUNT(id) AS group_count FROM pt.small_group WHERE church_id = @churchId" + |> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ] + |> Sql.executeRowAsync (fun row -> row.int "group_count") + + /// Delete a small group by its ID + let deleteById (groupId : SmallGroupId) conn = backgroundTask { + let idParam = [ [ "@groupId", Sql.uuid groupId.Value ] ] + let! _ = + conn + |> Sql.existingConnection + |> Sql.executeTransactionAsync + [ "DELETE FROM pt.prayer_request WHERE small_group_id = @groupId", idParam + "DELETE FROM pt.user_small_group WHERE small_group_id = @groupId", idParam + "DELETE FROM pt.list_preference WHERE small_group_id = @groupId", idParam + "DELETE FROM pt.small_group WHERE id = @groupId", idParam ] + return () + } + /// Get a list of small group IDs along with a description that includes the church name let listAll conn = conn @@ -98,9 +236,33 @@ module SmallGroups = FROM pt.small_group g INNER JOIN pt.church c ON c.id = g.church_id ORDER BY c.church_name, g.group_name""" - |> Sql.executeAsync (fun row -> - Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}""") + |> Sql.executeAsync mapToSmallGroupItem + /// Get a list of small group IDs and descriptions for groups with a group password + let listProtected conn = + conn + |> Sql.existingConnection + |> Sql.query """ + SELECT g.group_name, g.id, c.church_name + FROM pt.small_group g + INNER JOIN pt.church c ON c.id = g.church_id + INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id + WHERE COALESCE(lp.group_password, '') <> '' + ORDER BY c.church_name, g.group_name""" + |> Sql.executeAsync mapToSmallGroupItem + + /// Get a small group by its ID + let tryById (groupId : SmallGroupId) conn = backgroundTask { + let! group = + conn + |> Sql.existingConnection + |> Sql.query "SELECT * FROM pt.small_group WHERE id = @id" + |> Sql.parameters [ "@id", Sql.uuid groupId.Value ] + |> Sql.executeAsync mapToSmallGroup + return List.tryHead group + } + + /// Get a small group by its ID with its list preferences populated let tryByIdWithPreferences (groupId : SmallGroupId) conn = backgroundTask { let! group = conn @@ -108,7 +270,7 @@ module SmallGroups = |> Sql.query """ SELECT sg.*, lp.* FROM pt.small_group sg - INNER JOIN list_preference lp ON lp.small_group_id = sg.id + INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id WHERE sg.id = @id""" |> Sql.parameters [ "@id", Sql.uuid groupId.Value ] |> Sql.executeAsync mapToSmallGroupWithPreferences @@ -126,6 +288,30 @@ module Users = |> Sql.query "SELECT * FROM pt.pt_user ORDER BY last_name, first_name" |> Sql.executeAsync mapToUser + /// Count the number of users for a church + let countByChurch (churchId : ChurchId) conn = + conn + |> Sql.existingConnection + |> Sql.query """ + SELECT COUNT(u.id) AS user_count + FROM pt.pt_user u + WHERE EXISTS ( + SELECT 1 + FROM pt.user_small_group usg + INNER JOIN pt.small_group sg ON sg.id = usg.small_group_id + WHERE usg.user_id = u.id + AND sg.church_id = @churchId)""" + |> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ] + |> Sql.executeRowAsync (fun row -> row.int "user_count") + + /// Count the number of users for a small group + let countByGroup (groupId : SmallGroupId) conn = + conn + |> Sql.existingConnection + |> Sql.query "SELECT COUNT(user_id) AS user_count FROM pt.user_small_group WHERE small_group_id = @groupId" + |> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ] + |> Sql.executeRowAsync (fun row -> row.int "user_count") + /// Delete a user by its database ID let deleteById (userId : UserId) conn = backgroundTask { let! _ = @@ -137,6 +323,27 @@ module Users = return () } + /// Get the IDs of the small groups for which the given user is authorized + let groupIdsByUserId (userId : UserId) conn = + conn + |> Sql.existingConnection + |> Sql.query "SELECT small_group_id FROM pt.user_small_group WHERE user_id = @id" + |> Sql.parameters [ "@id", Sql.uuid userId.Value ] + |> Sql.executeAsync (fun row -> SmallGroupId (row.uuid "small_group_id")) + + /// Get a list of users authorized to administer the given small group + let listByGroupId (groupId : SmallGroupId) conn = + conn + |> Sql.existingConnection + |> Sql.query """ + SELECT u.* + FROM pt.pt_user u + INNER JOIN pt.user_small_group usg ON usg.user_id = u.id + WHERE usg.small_group_id = @groupId + ORDER BY u.last_name, u.first_name""" + |> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ] + |> Sql.executeAsync mapToUser + /// Save a user's information let save user conn = backgroundTask { let! _ = @@ -212,3 +419,27 @@ module Users = |> Sql.executeNonQueryAsync return () } + + /// Update a user's authorized small groups + let updateSmallGroups (userId : UserId) groupIds conn = backgroundTask { + let! existingGroupIds = groupIdsByUserId userId conn + let toAdd = + groupIds |> List.filter (fun it -> existingGroupIds |> List.exists (fun grpId -> grpId = it) |> not) + let toDelete = + existingGroupIds |> List.filter (fun it -> groupIds |> List.exists (fun grpId -> grpId = it) |> not) + let queries = seq { + if not (List.isEmpty toAdd) then + "INSERT INTO pt.user_small_group VALUES (@userId, @smallGroupId)", + toAdd |> List.map (fun it -> [ "@userId", Sql.uuid userId.Value; "@smallGroupId", Sql.uuid it.Value ]) + if not (List.isEmpty toDelete) then + "DELETE FROM pt.user_small_group WHERE user_id = @userId AND small_group_id = @smallGroupId", + toDelete + |> List.map (fun it -> [ "@userId", Sql.uuid userId.Value; "@smallGroupId", Sql.uuid it.Value ]) + } + if not (Seq.isEmpty queries) then + let! _ = + conn + |> Sql.existingConnection + |> Sql.executeTransactionAsync (List.ofSeq queries) + () + } diff --git a/src/PrayerTracker.UI/Resources/Common.es.resx b/src/PrayerTracker.UI/Resources/Common.es.resx index 5eb1ca7..a131275 100644 --- a/src/PrayerTracker.UI/Resources/Common.es.resx +++ b/src/PrayerTracker.UI/Resources/Common.es.resx @@ -828,4 +828,7 @@ Ultima vez Visto + + Administradores + \ No newline at end of file diff --git a/src/PrayerTracker.UI/SmallGroup.fs b/src/PrayerTracker.UI/SmallGroup.fs index 0a13040..4207432 100644 --- a/src/PrayerTracker.UI/SmallGroup.fs +++ b/src/PrayerTracker.UI/SmallGroup.fs @@ -141,7 +141,7 @@ let editMember (model : EditMember) (types : (string * LocalizedString) seq) ctx /// View for the small group log on page -let logOn (groups : SmallGroup list) grpId ctx viewInfo = +let logOn (groups : (string * string) list) grpId ctx viewInfo = let s = I18N.localizer.Force () let model = { SmallGroupId = emptyGuid; Password = ""; RememberMe = None } let vi = AppViewInfo.withOnLoadScript "PT.smallGroup.logOn.onPageLoad" viewInfo @@ -154,9 +154,7 @@ let logOn (groups : SmallGroup list) grpId ctx viewInfo = if groups.Length = 0 then "", s["There are no classes with passwords defined"].Value else "", selectDefault s["Select Group"].Value - yield! - groups - |> List.map (fun grp -> shortGuid grp.Id.Value, $"{grp.Church.Name} | {grp.Name}") + yield! groups } |> selectList (nameof model.SmallGroupId) grpId [ _required ] ] @@ -336,6 +334,13 @@ let overview model viewInfo = strong [] [ str (model.TotalMembers.ToString "N0"); space; locStr s["Members"] ] hr [] a [ _href "/small-group/members" ] [ icon "email"; linkSpacer; locStr s["Maintain Group Members"] ] + hr [] + strong [] [ str ((List.length model.Admins).ToString "N0"); space; locStr s["Administrators"] ] + for admin in model.Admins do + hr [] + str admin.Name + br [] + small [] [ a [ _href $"mailto:{admin.Email}" ] [ str admin.Email ] ] ] ] ] diff --git a/src/PrayerTracker.UI/ViewModels.fs b/src/PrayerTracker.UI/ViewModels.fs index a1118e5..0cf709a 100644 --- a/src/PrayerTracker.UI/ViewModels.fs +++ b/src/PrayerTracker.UI/ViewModels.fs @@ -688,6 +688,9 @@ type Overview = /// A count of all members TotalMembers : int + + /// The users authorized to administer this group + Admins : User list } diff --git a/src/PrayerTracker/Church.fs b/src/PrayerTracker/Church.fs index 16f14d7..e0b22e1 100644 --- a/src/PrayerTracker/Church.fs +++ b/src/PrayerTracker/Church.fs @@ -1,28 +1,29 @@ module PrayerTracker.Handlers.Church +open System.Threading.Tasks open Giraffe open PrayerTracker +open PrayerTracker.Data open PrayerTracker.Entities open PrayerTracker.ViewModels /// Find statistics for the given church -let private findStats (db : AppDbContext) churchId = task { - let! grps = db.CountGroupsByChurch churchId - let! reqs = db.CountRequestsByChurch churchId - let! usrs = db.CountUsersByChurch churchId - return shortGuid churchId.Value, { SmallGroups = grps; PrayerRequests = reqs; Users = usrs } +let private findStats churchId conn = task { + let! groups = SmallGroups.countByChurch churchId conn + let! requests = PrayerRequests.countByChurch churchId conn + let! users = Users.countByChurch churchId conn + return shortGuid churchId.Value, { SmallGroups = groups; PrayerRequests = requests; Users = users } } /// POST /church/[church-id]/delete let delete chId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { let churchId = ChurchId chId use! conn = ctx.Conn - match! Data.Churches.tryById churchId conn with + match! Churches.tryById churchId conn with | Some church -> - let! _, stats = findStats ctx.Db churchId - ctx.Db.RemoveEntry church - let! _ = ctx.Db.SaveChangesAsync () - let s = Views.I18N.localizer.Force () + let! _, stats = findStats churchId conn + do! Churches.deleteById churchId conn + let s = Views.I18N.localizer.Force () addInfo ctx s["The church {0} and its {1} small groups (with {2} prayer request(s)) were deleted successfully; revoked access from {3} user(s)", church.Name, stats.SmallGroups, stats.PrayerRequests, stats.Users] @@ -41,7 +42,7 @@ let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> ta |> renderHtml next ctx else use! conn = ctx.Conn - match! Data.Churches.tryById (ChurchId churchId) conn with + match! Churches.tryById (ChurchId churchId) conn with | Some church -> return! viewInfo ctx @@ -52,17 +53,15 @@ let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> ta /// GET /churches let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { - let await = Async.AwaitTask >> Async.RunSynchronously - let! churches = ctx.Db.AllChurches () - let stats = churches |> List.map (fun c -> await (findStats ctx.Db c.Id)) + let! conn = ctx.Conn + let! churches = Churches.all conn + let stats = churches |> List.map (fun c -> findStats c.Id conn |> Async.AwaitTask |> Async.RunSynchronously) return! viewInfo ctx |> Views.Church.maintain churches (stats |> Map.ofList) ctx |> renderHtml next ctx } -open System.Threading.Tasks - /// POST /church/save let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { match! ctx.TryBindFormAsync () with @@ -70,14 +69,12 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c let! conn = ctx.Conn let! church = if model.IsNew then Task.FromResult (Some { Church.empty with Id = (Guid.NewGuid >> ChurchId) () }) - else Data.Churches.tryById (idFromShort ChurchId model.ChurchId) conn + else Churches.tryById (idFromShort ChurchId model.ChurchId) conn match church with | Some ch -> - model.PopulateChurch ch - |> (if model.IsNew then ctx.Db.AddEntry else ctx.Db.UpdateEntry) - let! _ = ctx.Db.SaveChangesAsync () - let s = Views.I18N.localizer.Force () - let act = s[if model.IsNew then "Added" else "Updated"].Value.ToLower () + do! Churches.save (model.PopulateChurch ch) conn + let s = Views.I18N.localizer.Force () + let act = s[if model.IsNew then "Added" else "Updated"].Value.ToLower () addInfo ctx s["Successfully {0} church “{1}”", act, model.Name] return! redirectTo false "/churches" next ctx | None -> return! fourOhFour ctx diff --git a/src/PrayerTracker/Extensions.fs b/src/PrayerTracker/Extensions.fs index 668e042..1cf8409 100644 --- a/src/PrayerTracker/Extensions.fs +++ b/src/PrayerTracker/Extensions.fs @@ -6,6 +6,7 @@ open Microsoft.FSharpLu open Newtonsoft.Json open NodaTime open NodaTime.Serialization.JsonNet +open PrayerTracker.Data open PrayerTracker.Entities open PrayerTracker.ViewModels @@ -107,7 +108,8 @@ type HttpContext with | None -> match this.User.SmallGroupId with | Some groupId -> - match! this.Db.TryGroupById groupId with + let! conn = this.Conn + match! SmallGroups.tryByIdWithPreferences groupId conn with | Some group -> this.Session.CurrentGroup <- Some group return Some group @@ -122,11 +124,11 @@ type HttpContext with | None -> match this.User.UserId with | Some userId -> - match! this.Db.TryUserById userId with + let! conn = this.Conn + match! Users.tryById userId conn with | Some user -> // Set last seen for user - this.Db.UpdateEntry { user with LastSeen = Some this.Now } - let! _ = this.Db.SaveChangesAsync () + do! Users.updateLastSeen userId this.Now conn this.Session.CurrentUser <- Some user return Some user | None -> return None diff --git a/src/PrayerTracker/SmallGroup.fs b/src/PrayerTracker/SmallGroup.fs index d2d8803..a72b257 100644 --- a/src/PrayerTracker/SmallGroup.fs +++ b/src/PrayerTracker/SmallGroup.fs @@ -3,6 +3,7 @@ open System open Giraffe open PrayerTracker +open PrayerTracker.Data open PrayerTracker.Entities open PrayerTracker.ViewModels @@ -14,14 +15,14 @@ let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx -> /// POST /small-group/[group-id]/delete let delete grpId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { - let s = Views.I18N.localizer.Force () - let groupId = SmallGroupId grpId - match! ctx.Db.TryGroupById groupId with + let s = Views.I18N.localizer.Force () + let groupId = SmallGroupId grpId + let! conn = ctx.Conn + match! SmallGroups.tryById groupId conn with | Some grp -> - let! reqs = ctx.Db.CountRequestsBySmallGroup groupId - let! users = ctx.Db.CountUsersBySmallGroup groupId - ctx.Db.RemoveEntry grp - let! _ = ctx.Db.SaveChangesAsync () + let! reqs = PrayerRequests.countByGroup groupId conn + let! users = Users.countByGroup groupId conn + do! SmallGroups.deleteById groupId conn addInfo ctx s["The group {0} and its {1} prayer request(s) were deleted successfully; revoked access from {2} user(s)", grp.Name, reqs, users] @@ -31,13 +32,13 @@ let delete grpId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fu /// POST /small-group/member/[member-id]/delete let deleteMember mbrId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { - let s = Views.I18N.localizer.Force () - let group = ctx.Session.CurrentGroup.Value - let memberId = MemberId mbrId - match! ctx.Db.TryMemberById memberId with + let s = Views.I18N.localizer.Force () + let group = ctx.Session.CurrentGroup.Value + let memberId = MemberId mbrId + let! conn = ctx.Conn + match! Members.tryById memberId conn with | Some mbr when mbr.SmallGroupId = group.Id -> - ctx.Db.RemoveEntry mbr - let! _ = ctx.Db.SaveChangesAsync () + do! Members.deleteById memberId conn addHtmlInfo ctx s["The group member “{0}” was deleted successfully", mbr.Name] return! redirectTo false "/small-group/members" next ctx | Some _ @@ -46,7 +47,8 @@ let deleteMember mbrId : HttpHandler = requireAccess [ User ] >=> validateCsrf > /// GET /small-group/[group-id]/edit let edit grpId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { - let! churches = ctx.Db.AllChurches () + let! conn = ctx.Conn + let! churches = Churches.all conn let groupId = SmallGroupId grpId if groupId.Value = Guid.Empty then return! @@ -54,7 +56,7 @@ let edit grpId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task |> Views.SmallGroup.edit EditSmallGroup.empty churches ctx |> renderHtml next ctx else - match! ctx.Db.TryGroupById groupId with + match! SmallGroups.tryById groupId conn with | Some grp -> return! viewInfo ctx @@ -75,7 +77,8 @@ let editMember mbrId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> |> Views.SmallGroup.editMember EditMember.empty types ctx |> renderHtml next ctx else - match! ctx.Db.TryMemberById memberId with + let! conn = ctx.Conn + match! Members.tryById memberId conn with | Some mbr when mbr.SmallGroupId = group.Id -> return! viewInfo ctx @@ -87,7 +90,8 @@ let editMember mbrId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> /// GET /small-group/log-on/[group-id?] let logOn grpId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { - let! groups = ctx.Db.ProtectedGroups () + let! conn = ctx.Conn + let! groups = SmallGroups.listProtected conn let groupId = match grpId with Some gid -> shortGuid gid | None -> "" return! { viewInfo ctx with HelpLink = Some Help.logOn } @@ -147,21 +151,24 @@ let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { /// GET /small-group let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let group = ctx.Session.CurrentGroup.Value + let! conn = ctx.Conn let! reqs = ctx.Db.AllRequestsForSmallGroup group ctx.Clock None true 0 let! reqCount = ctx.Db.CountRequestsBySmallGroup group.Id let! mbrCount = ctx.Db.CountMembersForSmallGroup group.Id + let! admins = Users.listByGroupId group.Id conn let model = - { TotalActiveReqs = List.length reqs - AllReqs = reqCount - TotalMembers = mbrCount - ActiveReqsByType = - (reqs + { TotalActiveReqs = List.length reqs + AllReqs = reqCount + TotalMembers = mbrCount + ActiveReqsByType = ( + reqs |> 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) |> Map.ofSeq) - } + Admins = admins + } return! viewInfo ctx |> Views.SmallGroup.overview model diff --git a/src/PrayerTracker/User.fs b/src/PrayerTracker/User.fs index 56b46e8..9bb8435 100644 --- a/src/PrayerTracker/User.fs +++ b/src/PrayerTracker/User.fs @@ -263,25 +263,11 @@ let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun addError ctx s["You must select at least one group to assign"] return! redirectTo false $"/user/{model.UserId}/small-groups" next ctx | _ -> - match! ctx.Db.TryUserByIdWithGroups (idFromShort UserId model.UserId) with - | Some user -> - let groups = - model.SmallGroups.Split ',' - |> Array.map (idFromShort SmallGroupId) - |> List.ofArray - user.SmallGroups - |> Seq.filter (fun x -> not (groups |> List.exists (fun y -> y = x.SmallGroupId))) - |> ctx.Db.UserGroupXref.RemoveRange - groups - |> Seq.ofList - |> Seq.filter (fun x -> not (user.SmallGroups |> Seq.exists (fun y -> y.SmallGroupId = x))) - |> Seq.map (fun x -> { UserSmallGroup.empty with UserId = user.Id; SmallGroupId = x }) - |> List.ofSeq - |> List.iter ctx.Db.AddEntry - let! _ = ctx.Db.SaveChangesAsync () - addInfo ctx s["Successfully updated group permissions for {0}", model.UserName] - return! redirectTo false "/users" next ctx - | _ -> return! fourOhFour ctx + let! conn = ctx.Conn + do! Users.updateSmallGroups (idFromShort UserId model.UserId) + (model.SmallGroups.Split ',' |> Array.map (idFromShort SmallGroupId) |> List.ofArray) conn + addInfo ctx s["Successfully updated group permissions for {0}", model.UserName] + return! redirectTo false "/users" next ctx | Result.Error e -> return! bindError e next ctx } @@ -289,10 +275,11 @@ let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun let smallGroups usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let! conn = ctx.Conn let userId = UserId usrId - match! ctx.Db.TryUserByIdWithGroups userId with + match! Users.tryById userId conn with | Some user -> let! groups = SmallGroups.listAll conn - let curGroups = user.SmallGroups |> Seq.map (fun g -> shortGuid g.SmallGroupId.Value) |> List.ofSeq + let! groupIds = Users.groupIdsByUserId userId conn + let curGroups = groupIds |> List.map (fun g -> shortGuid g.Value) return! viewInfo ctx |> Views.User.assignGroups (AssignGroups.fromUser user) groups curGroups ctx