Version 8 #43

Merged
danieljsummers merged 37 commits from version-8 into main 2022-08-19 19:08:31 +00:00
8 changed files with 330 additions and 95 deletions
Showing only changes of commit e621cd6a1f - Show all commits

View File

@ -12,7 +12,7 @@ module private Helpers =
/// Map a row to a Church instance /// Map a row to a Church instance
let mapToChurch (row : RowReader) = let mapToChurch (row : RowReader) =
{ Id = ChurchId (row.uuid "id") { Id = ChurchId (row.uuid "id")
Name = row.string "name" Name = row.string "church_name"
City = row.string "city" City = row.string "city"
State = row.string "state" State = row.string "state"
HasVpsInterface = row.bool "has_vps_interface" HasVpsInterface = row.bool "has_vps_interface"
@ -21,27 +21,37 @@ module private Helpers =
/// Map a row to a ListPreferences instance /// Map a row to a ListPreferences instance
let mapToListPreferences (row : RowReader) = let mapToListPreferences (row : RowReader) =
{ SmallGroupId = SmallGroupId (row.uuid "small_group_id") { SmallGroupId = SmallGroupId (row.uuid "small_group_id")
DaysToKeepNew = row.int "days_to_keep_new" DaysToKeepNew = row.int "days_to_keep_new"
DaysToExpire = row.int "days_to_expire" DaysToExpire = row.int "days_to_expire"
LongTermUpdateWeeks = row.int "long_term_update_weeks" LongTermUpdateWeeks = row.int "long_term_update_weeks"
EmailFromName = row.string "email_from_name" EmailFromName = row.string "email_from_name"
EmailFromAddress = row.string "email_from_address" EmailFromAddress = row.string "email_from_address"
Fonts = row.string "fonts" Fonts = row.string "fonts"
HeadingColor = row.string "heading_color" HeadingColor = row.string "heading_color"
LineColor = row.string "line_color" LineColor = row.string "line_color"
HeadingFontSize = row.int "heading_font_size" HeadingFontSize = row.int "heading_font_size"
TextFontSize = row.int "text_font_size" TextFontSize = row.int "text_font_size"
RequestSort = RequestSort.fromCode (row.string "request_sort") GroupPassword = row.string "group_password"
GroupPassword = row.string "group_password" IsPublic = row.bool "is_public"
DefaultEmailType = EmailFormat.fromCode (row.string "default_email_type") PageSize = row.int "page_size"
IsPublic = row.bool "is_public" TimeZoneId = TimeZoneId (row.string "time_zone_id")
TimeZoneId = TimeZoneId (row.string "time_zone_id") RequestSort = RequestSort.fromCode (row.string "request_sort")
PageSize = row.int "page_size" DefaultEmailType = EmailFormat.fromCode (row.string "default_email_type")
AsOfDateDisplay = AsOfDateDisplay.fromCode (row.string "as_of_date_display") AsOfDateDisplay = AsOfDateDisplay.fromCode (row.string "as_of_date_display")
TimeZone = TimeZone.empty 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 /// Map a row to a Small Group instance
let mapToSmallGroup (row : RowReader) = let mapToSmallGroup (row : RowReader) =
{ Id = SmallGroupId (row.uuid "id") { Id = SmallGroupId (row.uuid "id")
@ -54,6 +64,10 @@ module private Helpers =
Users = ResizeArray () 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 /// Map a row to a Small Group instance with populated list preferences
let mapToSmallGroupWithPreferences (row : RowReader) = let mapToSmallGroupWithPreferences (row : RowReader) =
{ mapToSmallGroup row with { mapToSmallGroup row with
@ -74,7 +88,60 @@ module private Helpers =
} }
/// Functions to manipulate churches
module 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 tryById (churchId : ChurchId) conn = backgroundTask {
let! church = let! church =
conn 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 /// Functions to retrieve small group information
module SmallGroups = 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 /// Get a list of small group IDs along with a description that includes the church name
let listAll conn = let listAll conn =
conn conn
@ -98,9 +236,33 @@ module SmallGroups =
FROM pt.small_group g FROM pt.small_group g
INNER JOIN pt.church c ON c.id = g.church_id INNER JOIN pt.church c ON c.id = g.church_id
ORDER BY c.church_name, g.group_name""" ORDER BY c.church_name, g.group_name"""
|> Sql.executeAsync (fun row -> |> Sql.executeAsync mapToSmallGroupItem
Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}""")
/// 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 tryByIdWithPreferences (groupId : SmallGroupId) conn = backgroundTask {
let! group = let! group =
conn conn
@ -108,7 +270,7 @@ module SmallGroups =
|> Sql.query """ |> Sql.query """
SELECT sg.*, lp.* SELECT sg.*, lp.*
FROM pt.small_group sg 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""" WHERE sg.id = @id"""
|> Sql.parameters [ "@id", Sql.uuid groupId.Value ] |> Sql.parameters [ "@id", Sql.uuid groupId.Value ]
|> Sql.executeAsync mapToSmallGroupWithPreferences |> Sql.executeAsync mapToSmallGroupWithPreferences
@ -126,6 +288,30 @@ module Users =
|> Sql.query "SELECT * FROM pt.pt_user ORDER BY last_name, first_name" |> Sql.query "SELECT * FROM pt.pt_user ORDER BY last_name, first_name"
|> Sql.executeAsync mapToUser |> 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 /// Delete a user by its database ID
let deleteById (userId : UserId) conn = backgroundTask { let deleteById (userId : UserId) conn = backgroundTask {
let! _ = let! _ =
@ -137,6 +323,27 @@ module Users =
return () 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 /// Save a user's information
let save user conn = backgroundTask { let save user conn = backgroundTask {
let! _ = let! _ =
@ -212,3 +419,27 @@ module Users =
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
return () 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)
()
}

View File

@ -828,4 +828,7 @@
<data name="Last Seen" xml:space="preserve"> <data name="Last Seen" xml:space="preserve">
<value>Ultima vez Visto</value> <value>Ultima vez Visto</value>
</data> </data>
<data name="Administrators" xml:space="preserve">
<value>Administradores</value>
</data>
</root> </root>

View File

@ -141,7 +141,7 @@ let editMember (model : EditMember) (types : (string * LocalizedString) seq) ctx
/// View for the small group log on page /// 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 s = I18N.localizer.Force ()
let model = { SmallGroupId = emptyGuid; Password = ""; RememberMe = None } let model = { SmallGroupId = emptyGuid; Password = ""; RememberMe = None }
let vi = AppViewInfo.withOnLoadScript "PT.smallGroup.logOn.onPageLoad" viewInfo 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 if groups.Length = 0 then "", s["There are no classes with passwords defined"].Value
else else
"", selectDefault s["Select Group"].Value "", selectDefault s["Select Group"].Value
yield! yield! groups
groups
|> List.map (fun grp -> shortGuid grp.Id.Value, $"{grp.Church.Name} | {grp.Name}")
} }
|> selectList (nameof model.SmallGroupId) grpId [ _required ] |> selectList (nameof model.SmallGroupId) grpId [ _required ]
] ]
@ -336,6 +334,13 @@ let overview model viewInfo =
strong [] [ str (model.TotalMembers.ToString "N0"); space; locStr s["Members"] ] strong [] [ str (model.TotalMembers.ToString "N0"); space; locStr s["Members"] ]
hr [] hr []
a [ _href "/small-group/members" ] [ icon "email"; linkSpacer; locStr s["Maintain Group Members"] ] 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 ] ]
] ]
] ]
] ]

View File

@ -688,6 +688,9 @@ type Overview =
/// A count of all members /// A count of all members
TotalMembers : int TotalMembers : int
/// The users authorized to administer this group
Admins : User list
} }

View File

@ -1,28 +1,29 @@
module PrayerTracker.Handlers.Church module PrayerTracker.Handlers.Church
open System.Threading.Tasks
open Giraffe open Giraffe
open PrayerTracker open PrayerTracker
open PrayerTracker.Data
open PrayerTracker.Entities open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
/// Find statistics for the given church /// Find statistics for the given church
let private findStats (db : AppDbContext) churchId = task { let private findStats churchId conn = task {
let! grps = db.CountGroupsByChurch churchId let! groups = SmallGroups.countByChurch churchId conn
let! reqs = db.CountRequestsByChurch churchId let! requests = PrayerRequests.countByChurch churchId conn
let! usrs = db.CountUsersByChurch churchId let! users = Users.countByChurch churchId conn
return shortGuid churchId.Value, { SmallGroups = grps; PrayerRequests = reqs; Users = usrs } return shortGuid churchId.Value, { SmallGroups = groups; PrayerRequests = requests; Users = users }
} }
/// POST /church/[church-id]/delete /// POST /church/[church-id]/delete
let delete chId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { let delete chId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
let churchId = ChurchId chId let churchId = ChurchId chId
use! conn = ctx.Conn use! conn = ctx.Conn
match! Data.Churches.tryById churchId conn with match! Churches.tryById churchId conn with
| Some church -> | Some church ->
let! _, stats = findStats ctx.Db churchId let! _, stats = findStats churchId conn
ctx.Db.RemoveEntry church do! Churches.deleteById churchId conn
let! _ = ctx.Db.SaveChangesAsync () let s = Views.I18N.localizer.Force ()
let s = Views.I18N.localizer.Force ()
addInfo ctx 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)", 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] 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 |> renderHtml next ctx
else else
use! conn = ctx.Conn use! conn = ctx.Conn
match! Data.Churches.tryById (ChurchId churchId) conn with match! Churches.tryById (ChurchId churchId) conn with
| Some church -> | Some church ->
return! return!
viewInfo ctx viewInfo ctx
@ -52,17 +53,15 @@ let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> ta
/// GET /churches /// GET /churches
let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
let await = Async.AwaitTask >> Async.RunSynchronously let! conn = ctx.Conn
let! churches = ctx.Db.AllChurches () let! churches = Churches.all conn
let stats = churches |> List.map (fun c -> await (findStats ctx.Db c.Id)) let stats = churches |> List.map (fun c -> findStats c.Id conn |> Async.AwaitTask |> Async.RunSynchronously)
return! return!
viewInfo ctx viewInfo ctx
|> Views.Church.maintain churches (stats |> Map.ofList) ctx |> Views.Church.maintain churches (stats |> Map.ofList) ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
open System.Threading.Tasks
/// POST /church/save /// POST /church/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditChurch> () with match! ctx.TryBindFormAsync<EditChurch> () with
@ -70,14 +69,12 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c
let! conn = ctx.Conn let! conn = ctx.Conn
let! church = let! church =
if model.IsNew then Task.FromResult (Some { Church.empty with Id = (Guid.NewGuid >> ChurchId) () }) 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 match church with
| Some ch -> | Some ch ->
model.PopulateChurch ch do! Churches.save (model.PopulateChurch ch) conn
|> (if model.IsNew then ctx.Db.AddEntry else ctx.Db.UpdateEntry) let s = Views.I18N.localizer.Force ()
let! _ = ctx.Db.SaveChangesAsync () let act = s[if model.IsNew then "Added" else "Updated"].Value.ToLower ()
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] addInfo ctx s["Successfully {0} church “{1}”", act, model.Name]
return! redirectTo false "/churches" next ctx return! redirectTo false "/churches" next ctx
| None -> return! fourOhFour ctx | None -> return! fourOhFour ctx

View File

@ -6,6 +6,7 @@ open Microsoft.FSharpLu
open Newtonsoft.Json open Newtonsoft.Json
open NodaTime open NodaTime
open NodaTime.Serialization.JsonNet open NodaTime.Serialization.JsonNet
open PrayerTracker.Data
open PrayerTracker.Entities open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
@ -107,7 +108,8 @@ type HttpContext with
| None -> | None ->
match this.User.SmallGroupId with match this.User.SmallGroupId with
| Some groupId -> | Some groupId ->
match! this.Db.TryGroupById groupId with let! conn = this.Conn
match! SmallGroups.tryByIdWithPreferences groupId conn with
| Some group -> | Some group ->
this.Session.CurrentGroup <- Some group this.Session.CurrentGroup <- Some group
return Some group return Some group
@ -122,11 +124,11 @@ type HttpContext with
| None -> | None ->
match this.User.UserId with match this.User.UserId with
| Some userId -> | Some userId ->
match! this.Db.TryUserById userId with let! conn = this.Conn
match! Users.tryById userId conn with
| Some user -> | Some user ->
// Set last seen for user // Set last seen for user
this.Db.UpdateEntry { user with LastSeen = Some this.Now } do! Users.updateLastSeen userId this.Now conn
let! _ = this.Db.SaveChangesAsync ()
this.Session.CurrentUser <- Some user this.Session.CurrentUser <- Some user
return Some user return Some user
| None -> return None | None -> return None

View File

@ -3,6 +3,7 @@
open System open System
open Giraffe open Giraffe
open PrayerTracker open PrayerTracker
open PrayerTracker.Data
open PrayerTracker.Entities open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
@ -14,14 +15,14 @@ let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
/// POST /small-group/[group-id]/delete /// POST /small-group/[group-id]/delete
let delete grpId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { let delete grpId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
let groupId = SmallGroupId grpId let groupId = SmallGroupId grpId
match! ctx.Db.TryGroupById groupId with let! conn = ctx.Conn
match! SmallGroups.tryById groupId conn with
| Some grp -> | Some grp ->
let! reqs = ctx.Db.CountRequestsBySmallGroup groupId let! reqs = PrayerRequests.countByGroup groupId conn
let! users = ctx.Db.CountUsersBySmallGroup groupId let! users = Users.countByGroup groupId conn
ctx.Db.RemoveEntry grp do! SmallGroups.deleteById groupId conn
let! _ = ctx.Db.SaveChangesAsync ()
addInfo ctx addInfo ctx
s["The group {0} and its {1} prayer request(s) were deleted successfully; revoked access from {2} user(s)", s["The group {0} and its {1} prayer request(s) were deleted successfully; revoked access from {2} user(s)",
grp.Name, reqs, users] grp.Name, reqs, users]
@ -31,13 +32,13 @@ let delete grpId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fu
/// POST /small-group/member/[member-id]/delete /// POST /small-group/member/[member-id]/delete
let deleteMember mbrId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { let deleteMember mbrId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let memberId = MemberId mbrId let memberId = MemberId mbrId
match! ctx.Db.TryMemberById memberId with let! conn = ctx.Conn
match! Members.tryById memberId conn with
| Some mbr when mbr.SmallGroupId = group.Id -> | Some mbr when mbr.SmallGroupId = group.Id ->
ctx.Db.RemoveEntry mbr do! Members.deleteById memberId conn
let! _ = ctx.Db.SaveChangesAsync ()
addHtmlInfo ctx s["The group member &ldquo;{0}&rdquo; was deleted successfully", mbr.Name] addHtmlInfo ctx s["The group member &ldquo;{0}&rdquo; was deleted successfully", mbr.Name]
return! redirectTo false "/small-group/members" next ctx return! redirectTo false "/small-group/members" next ctx
| Some _ | Some _
@ -46,7 +47,8 @@ let deleteMember mbrId : HttpHandler = requireAccess [ User ] >=> validateCsrf >
/// GET /small-group/[group-id]/edit /// GET /small-group/[group-id]/edit
let edit grpId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { 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 let groupId = SmallGroupId grpId
if groupId.Value = Guid.Empty then if groupId.Value = Guid.Empty then
return! return!
@ -54,7 +56,7 @@ let edit grpId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task
|> Views.SmallGroup.edit EditSmallGroup.empty churches ctx |> Views.SmallGroup.edit EditSmallGroup.empty churches ctx
|> renderHtml next ctx |> renderHtml next ctx
else else
match! ctx.Db.TryGroupById groupId with match! SmallGroups.tryById groupId conn with
| Some grp -> | Some grp ->
return! return!
viewInfo ctx viewInfo ctx
@ -75,7 +77,8 @@ let editMember mbrId : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
|> Views.SmallGroup.editMember EditMember.empty types ctx |> Views.SmallGroup.editMember EditMember.empty types ctx
|> renderHtml next ctx |> renderHtml next ctx
else else
match! ctx.Db.TryMemberById memberId with let! conn = ctx.Conn
match! Members.tryById memberId conn with
| Some mbr when mbr.SmallGroupId = group.Id -> | Some mbr when mbr.SmallGroupId = group.Id ->
return! return!
viewInfo ctx viewInfo ctx
@ -87,7 +90,8 @@ let editMember mbrId : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
/// GET /small-group/log-on/[group-id?] /// GET /small-group/log-on/[group-id?]
let logOn grpId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { 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 -> "" let groupId = match grpId with Some gid -> shortGuid gid | None -> ""
return! return!
{ viewInfo ctx with HelpLink = Some Help.logOn } { viewInfo ctx with HelpLink = Some Help.logOn }
@ -147,21 +151,24 @@ let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
/// GET /small-group /// GET /small-group
let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let! conn = ctx.Conn
let! reqs = ctx.Db.AllRequestsForSmallGroup group ctx.Clock None true 0 let! reqs = ctx.Db.AllRequestsForSmallGroup group ctx.Clock None true 0
let! reqCount = ctx.Db.CountRequestsBySmallGroup group.Id let! reqCount = ctx.Db.CountRequestsBySmallGroup group.Id
let! mbrCount = ctx.Db.CountMembersForSmallGroup group.Id let! mbrCount = ctx.Db.CountMembersForSmallGroup group.Id
let! admins = Users.listByGroupId group.Id conn
let model = let model =
{ TotalActiveReqs = List.length reqs { TotalActiveReqs = List.length reqs
AllReqs = reqCount AllReqs = reqCount
TotalMembers = mbrCount TotalMembers = mbrCount
ActiveReqsByType = ActiveReqsByType = (
(reqs reqs
|> Seq.ofList |> Seq.ofList
|> Seq.map (fun req -> req.RequestType) |> Seq.map (fun req -> req.RequestType)
|> Seq.distinct |> 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) |> Map.ofSeq)
} Admins = admins
}
return! return!
viewInfo ctx viewInfo ctx
|> Views.SmallGroup.overview model |> Views.SmallGroup.overview model

View File

@ -263,25 +263,11 @@ let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun
addError ctx s["You must select at least one group to assign"] addError ctx s["You must select at least one group to assign"]
return! redirectTo false $"/user/{model.UserId}/small-groups" next ctx return! redirectTo false $"/user/{model.UserId}/small-groups" next ctx
| _ -> | _ ->
match! ctx.Db.TryUserByIdWithGroups (idFromShort UserId model.UserId) with let! conn = ctx.Conn
| Some user -> do! Users.updateSmallGroups (idFromShort UserId model.UserId)
let groups = (model.SmallGroups.Split ',' |> Array.map (idFromShort SmallGroupId) |> List.ofArray) conn
model.SmallGroups.Split ',' addInfo ctx s["Successfully updated group permissions for {0}", model.UserName]
|> Array.map (idFromShort SmallGroupId) return! redirectTo false "/users" next ctx
|> 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
| Result.Error e -> return! bindError e 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 smallGroups usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
let! conn = ctx.Conn let! conn = ctx.Conn
let userId = UserId usrId let userId = UserId usrId
match! ctx.Db.TryUserByIdWithGroups userId with match! Users.tryById userId conn with
| Some user -> | Some user ->
let! groups = SmallGroups.listAll conn 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! return!
viewInfo ctx viewInfo ctx
|> Views.User.assignGroups (AssignGroups.fromUser user) groups curGroups ctx |> Views.User.assignGroups (AssignGroups.fromUser user) groups curGroups ctx