From ef4f82cf9d54e65cabe16cbad6522090fa622d33 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 16 Mar 2019 18:58:17 -0500 Subject: [PATCH] Search / inactive request list paginate Also: - Fixed a verbiage error with the confirmation prompts - Split the I18N for the maintain requests page into its own localized view --- src/PrayerTracker.Data/DataAccess.fs | 11 +- src/PrayerTracker.Tests/Data/EntitiesTests.fs | 4 +- src/PrayerTracker.Tests/UI/UtilsTests.fs | 15 ++ src/PrayerTracker.Tests/UI/ViewModelsTests.fs | 13 ++ src/PrayerTracker.UI/Church.fs | 2 +- src/PrayerTracker.UI/PrayerRequest.fs | 92 +++++++--- src/PrayerTracker.UI/PrayerTracker.UI.fsproj | 3 + src/PrayerTracker.UI/Resources/Common.es.resx | 35 +--- .../Resources/Views/Requests/Maintain.es.resx | 159 ++++++++++++++++++ src/PrayerTracker.UI/SmallGroup.fs | 8 +- src/PrayerTracker.UI/User.fs | 2 +- src/PrayerTracker.UI/Utils.fs | 14 ++ src/PrayerTracker.UI/ViewModels.fs | 25 +++ src/PrayerTracker/PrayerRequest.fs | 26 ++- src/PrayerTracker/SmallGroup.fs | 2 +- 15 files changed, 341 insertions(+), 70 deletions(-) create mode 100644 src/PrayerTracker.UI/Resources/Views/Requests/Maintain.es.resx diff --git a/src/PrayerTracker.Data/DataAccess.fs b/src/PrayerTracker.Data/DataAccess.fs index 79bfe1d..1567533 100644 --- a/src/PrayerTracker.Data/DataAccess.fs +++ b/src/PrayerTracker.Data/DataAccess.fs @@ -90,7 +90,7 @@ type AppDbContext with } /// Get all (or active) requests for a small group as of now or the specified date - member this.AllRequestsForSmallGroup (grp : SmallGroup) clock listDate activeOnly : PrayerRequest seq = + member this.AllRequestsForSmallGroup (grp : SmallGroup) clock listDate activeOnly pageNbr : PrayerRequest seq = let theDate = match listDate with Some dt -> dt | _ -> grp.localDateNow clock upcast ( this.PrayerRequests.AsNoTracking().Where(fun pr -> pr.smallGroupId = grp.smallGroupId) @@ -104,8 +104,13 @@ type AppDbContext with || RequestType.Expecting = pr.requestType) && not pr.isManuallyExpired) | query -> query - |> reqSort grp.preferences.requestSort) - + |> reqSort grp.preferences.requestSort + |> function + | query -> + match activeOnly with + | true -> query.Skip 0 + | false -> query.Skip((pageNbr - 1) * grp.preferences.pageSize).Take grp.preferences.pageSize) + /// Count prayer requests for the given small group Id member this.CountRequestsBySmallGroup gId = this.PrayerRequests.CountAsync (fun pr -> pr.smallGroupId = gId) diff --git a/src/PrayerTracker.Tests/Data/EntitiesTests.fs b/src/PrayerTracker.Tests/Data/EntitiesTests.fs index 17ed70a..64bec37 100644 --- a/src/PrayerTracker.Tests/Data/EntitiesTests.fs +++ b/src/PrayerTracker.Tests/Data/EntitiesTests.fs @@ -1,10 +1,9 @@ module PrayerTracker.Entities.EntitiesTests open Expecto -open System -open System.Linq open NodaTime.Testing open NodaTime +open System [] let churchTests = @@ -45,6 +44,7 @@ let listPreferencesTests = Expect.isFalse mt.isPublic "The isPublic flag should not have been set" Expect.equal mt.timeZoneId "America/Denver" "The default time zone should have been America/Denver" Expect.equal mt.timeZone.timeZoneId "" "The default preferences should have included an empty time zone" + Expect.equal mt.pageSize 100 "The default page size should have been 100" } ] diff --git a/src/PrayerTracker.Tests/UI/UtilsTests.fs b/src/PrayerTracker.Tests/UI/UtilsTests.fs index dbf2384..7fc94c4 100644 --- a/src/PrayerTracker.Tests/UI/UtilsTests.fs +++ b/src/PrayerTracker.Tests/UI/UtilsTests.fs @@ -68,6 +68,21 @@ let htmlToPlainTextTests = } ] +[] +let makeUrlTests = + testList "makeUrl" [ + test "returns the URL when there are no parameters" { + Expect.equal (makeUrl "/test" []) "/test" "The URL should not have had any query string parameters added" + } + test "returns the URL with one query string parameter" { + Expect.equal (makeUrl "/test" [ "unit", "true" ]) "/test?unit=true" "The URL was not constructed properly" + } + test "returns the URL with multiple encoded query string parameters" { + let url = makeUrl "/test" [ "space", "a space"; "turkey", "=" ] + Expect.equal url "/test?space=a+space&turkey=%3D" "The URL was not constructed properly" + } + ] + [] let sndAsStringTests = testList "sndAsString" [ diff --git a/src/PrayerTracker.Tests/UI/ViewModelsTests.fs b/src/PrayerTracker.Tests/UI/ViewModelsTests.fs index 8255041..ba58dbe 100644 --- a/src/PrayerTracker.Tests/UI/ViewModelsTests.fs +++ b/src/PrayerTracker.Tests/UI/ViewModelsTests.fs @@ -438,6 +438,19 @@ let groupLogOnTests = } ] +[] +let maintainRequestsTests = + testList "MaintainRequests" [ + test "empty is as expected" { + let mt = MaintainRequests.empty + Expect.isEmpty mt.requests "The requests for the model should have been empty" + Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one" + Expect.isNone mt.onlyActive "The only active flag should have been None" + Expect.isNone mt.searchTerm "The search term should have been None" + Expect.isNone mt.pageNbr "The page number should have been None" + } + ] + [] let requestListTests = testList "RequestList" [ diff --git a/src/PrayerTracker.UI/Church.fs b/src/PrayerTracker.UI/Church.fs index 9deb102..341d473 100644 --- a/src/PrayerTracker.UI/Church.fs +++ b/src/PrayerTracker.UI/Church.fs @@ -75,7 +75,7 @@ let maintain (churches : Church list) (stats : Map) ctx vi |> List.map (fun ch -> let chId = flatGuid ch.churchId let delAction = sprintf "/church/%s/delete" chId - let delPrompt = s.["Are you want to delete this {0}? This action cannot be undone.", + let delPrompt = s.["Are you sure you want to delete this {0}? This action cannot be undone.", sprintf "%s (%s)" (s.["Church"].Value.ToLower ()) ch.name] tr [] [ td [] [ diff --git a/src/PrayerTracker.UI/PrayerRequest.fs b/src/PrayerTracker.UI/PrayerRequest.fs index c0d1850..adc9956 100644 --- a/src/PrayerTracker.UI/PrayerRequest.fs +++ b/src/PrayerTracker.UI/PrayerRequest.fs @@ -160,39 +160,50 @@ let lists (grps : SmallGroup list) vi = /// View for the prayer request maintenance page -let maintain (reqs : PrayerRequest seq) (grp : SmallGroup) onlyActive (ctx : HttpContext) vi = +let maintain m (ctx : HttpContext) vi = let s = I18N.localizer.Force () - let now = grp.localDateNow (ctx.GetService ()) + let l = I18N.forView "Requests/Maintain" + use sw = new StringWriter () + let raw = rawLocText sw + let now = m.smallGroup.localDateNow (ctx.GetService ()) let typs = ReferenceList.requestTypeList s |> Map.ofList let updReq (req : PrayerRequest) = - match req.updateRequired now grp.preferences.daysToExpire grp.preferences.longTermUpdateWeeks with + match req.updateRequired now m.smallGroup.preferences.daysToExpire m.smallGroup.preferences.longTermUpdateWeeks with | true -> "pt-request-update" | false -> "" |> _class let reqExp (req : PrayerRequest) = - _class (match req.isExpired now grp.preferences.daysToExpire with true -> "pt-request-expired" | false -> "") + _class (match req.isExpired now m.smallGroup.preferences.daysToExpire with true -> "pt-request-expired" | false -> "") /// Iterate the sequence once, before we render, so we can get the count of it at the top of the table let requests = - reqs + m.requests |> Seq.map (fun req -> let reqId = flatGuid req.prayerRequestId let reqText = Utils.htmlToPlainText req.text let delAction = sprintf "/prayer-request/%s/delete" reqId - let delPrompt = s.["Are you want to delete this prayer request? This action cannot be undone.\\n(If the prayer request has been answered, or an event has passed, consider inactivating it instead.)"].Value + let delPrompt = + [ s.["Are you sure you want to delete this {0}? This action cannot be undone.", + s.["Prayer Request"].Value.ToLower() ] + .Value + "\\n" + l.["(If the prayer request has been answered, or an event has passed, consider inactivating it instead.)"] + .Value + ] + |> String.concat "" tr [] [ td [] [ - yield a [ _href (sprintf "/prayer-request/%s/edit" reqId); _title s.["Edit This Prayer Request"].Value ] + yield a [ _href (sprintf "/prayer-request/%s/edit" reqId); _title l.["Edit This Prayer Request"].Value ] [ icon "edit" ] - match req.isExpired now grp.preferences.daysToExpire with + match req.isExpired now m.smallGroup.preferences.daysToExpire with | true -> yield a [ _href (sprintf "/prayer-request/%s/restore" reqId) - _title s.["Restore This Inactive Request"].Value ] + _title l.["Restore This Inactive Request"].Value ] [ icon "visibility" ] | false -> yield a [ _href (sprintf "/prayer-request/%s/expire" reqId) - _title s.["Expire This Request Immediately"].Value ] + _title l.["Expire This Request Immediately"].Value ] [ icon "visibility_off" ] - yield a [ _href delAction; _title s.["Delete This Request"].Value; + yield a [ _href delAction; _title l.["Delete This Request"].Value; _onclick (sprintf "return PT.confirmDelete('%s','%s')" delAction delPrompt) ] [ icon "delete_forever" ] ] @@ -210,15 +221,25 @@ let maintain (reqs : PrayerRequest seq) (grp : SmallGroup) onlyActive (ctx : Htt ]) |> List.ofSeq [ yield div [ _class "pt-center-text" ] [ - br [] - a [ _href (sprintf "/prayer-request/%s/edit" emptyGuid); _title s.["Add a New Request"].Value ] + yield br [] + yield a [ _href (sprintf "/prayer-request/%s/edit" emptyGuid); _title s.["Add a New Request"].Value ] [ icon "add_circle"; rawText "  "; locStr s.["Add a New Request"] ] - rawText "       " - a [ _href "/prayer-requests/view"; _title s.["View Prayer Request List"].Value ] + yield rawText "       " + yield a [ _href "/prayer-requests/view"; _title s.["View Prayer Request List"].Value ] [ icon "list"; rawText "  "; locStr s.["View Prayer Request List"] ] + match m.searchTerm with + | Some _ -> + yield rawText "       " + yield a [ _href "/prayer-requests"; _title l.["Clear Search Criteria"].Value ] + [ icon "highlight_off"; rawText "  "; raw l.["Clear Search Criteria"] ] + | None -> () ] yield form [ _action "/prayer-requests"; _method "get"; _class "pt-center-text pt-search-form" ] [ - input [ _type "text"; _name "search"; _placeholder s.["Search requests..."].Value ] + input [ _type "text" + _name "search" + _placeholder l.["Search requests..."].Value + _value (defaultArg m.searchTerm "") + ] space submit [] "search" s.["Search"] ] @@ -241,20 +262,41 @@ let maintain (reqs : PrayerRequest seq) (grp : SmallGroup) onlyActive (ctx : Htt ] yield div [ _class "pt-center-text" ] [ yield br [] - match onlyActive with - | true -> - yield locStr s.["Inactive requests are currently not shown"] + match m.onlyActive with + | Some true -> + yield raw l.["Inactive requests are currently not shown"] yield br [] - yield a [ _href "/prayer-requests/inactive" ] [ locStr s.["Show Inactive Requests"] ] - | false -> - yield locStr s.["Inactive requests are currently shown"] - yield br [] - yield a [ _href "/prayer-requests" ] [ locStr s.["Do Not Show Inactive Requests"] ] + yield a [ _href "/prayer-requests/inactive" ] [ raw l.["Show Inactive Requests"] ] + | _ -> + match Option.isSome m.onlyActive with + | true -> + yield raw l.["Inactive requests are currently shown"] + yield br [] + yield a [ _href "/prayer-requests" ] [ raw l.["Do Not Show Inactive Requests"] ] + yield br [] + yield br [] + | false -> () + let srch = [ match m.searchTerm with Some s -> yield "search", s | None -> () ] + let url = match m.onlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/prayer-requests%s" + let pg = defaultArg m.pageNbr 1 + match pg with + | 1 -> () + | _ -> + // button (_type "submit" :: attrs) [ icon ico; rawText "  "; locStr text ] + let withPage = match pg with 2 -> srch | _ -> ("page", string (pg - 1)) :: srch + yield a [ _href (makeUrl url withPage) ] + [ icon "keyboard_arrow_left"; space; raw l.["Previous Page"] ] + yield rawText "     " + match requests.Length = m.smallGroup.preferences.pageSize with + | true -> + yield a [ _href (makeUrl url (("page", string (pg + 1)) :: srch)) ] + [ raw l.["Next Page"]; space; icon "keyboard_arrow_right" ] + | false -> () ] yield form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] ] |> Layout.Content.wide - |> Layout.standard vi "Maintain Requests" + |> Layout.standard vi (match m.searchTerm with Some _ -> "Search Results" | None -> "Maintain Requests") /// View for the printable prayer request list diff --git a/src/PrayerTracker.UI/PrayerTracker.UI.fsproj b/src/PrayerTracker.UI/PrayerTracker.UI.fsproj index aa959ca..5bf0a24 100644 --- a/src/PrayerTracker.UI/PrayerTracker.UI.fsproj +++ b/src/PrayerTracker.UI/PrayerTracker.UI.fsproj @@ -55,6 +55,9 @@ ResXFileCodeGenerator + + ResXFileCodeGenerator + ResXFileCodeGenerator diff --git a/src/PrayerTracker.UI/Resources/Common.es.resx b/src/PrayerTracker.UI/Resources/Common.es.resx index 978b6cf..f00ca4c 100644 --- a/src/PrayerTracker.UI/Resources/Common.es.resx +++ b/src/PrayerTracker.UI/Resources/Common.es.resx @@ -144,8 +144,8 @@ Verde Azulado Brillante - - ¿Está desea eliminar este {0}? Esta acción no se puede deshacer. + + ¿Seguro que desea eliminar este {0}? Esta acción no se puede deshacer. PDF Adjunto @@ -612,9 +612,6 @@ Eliminar Este Grupo - - No Muestran las Peticiones Inactivos - Correo Electrónico @@ -633,12 +630,6 @@ Las Preferencias del Grupo - - Peticiones inactivas no se muestra actualmente - - - Peticiones inactivas se muestra actualmente - Mantener los Grupos @@ -696,9 +687,6 @@ Enviar anuncio a - - Muestran las Peticiones Inactivos - Ordenar por Fecha de Última Actualización @@ -729,15 +717,6 @@ Peticiones Activas - - Eliminar esta petición - - - Editar esta petición de oración - - - Expirar esta petición de oración de inmediato - Mantener las Peticiones de Oración @@ -747,9 +726,6 @@ Acciones Rápidas - - Restaurar esta petición inactiva - Guardar @@ -822,7 +798,10 @@ Buscar - - Busca las peticiones... + + Petición de Oración + + + Resultados de la Búsqueda \ No newline at end of file diff --git a/src/PrayerTracker.UI/Resources/Views/Requests/Maintain.es.resx b/src/PrayerTracker.UI/Resources/Views/Requests/Maintain.es.resx new file mode 100644 index 0000000..ee5d48c --- /dev/null +++ b/src/PrayerTracker.UI/Resources/Views/Requests/Maintain.es.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + (Si la solicitud de oración ha sido respondida o si un evento ha pasado, considere desactivarla.) + + + Borrar los Criterios de Búsqueda + + + Eliminar esta petición + + + No Muestran las Peticiones Inactivos + + + Editar esta petición de oración + + + Expirar esta petición de oración de inmediato + + + Peticiones inactivas no se muestra actualmente + + + Peticiones inactivas se muestra actualmente + + + Siguiente Página + + + Página Anterior + + + Restaurar esta petición inactiva + + + Busca las peticiones... + + + Muestran las Peticiones Inactivos + + \ No newline at end of file diff --git a/src/PrayerTracker.UI/SmallGroup.fs b/src/PrayerTracker.UI/SmallGroup.fs index 61a8e20..007ea6e 100644 --- a/src/PrayerTracker.UI/SmallGroup.fs +++ b/src/PrayerTracker.UI/SmallGroup.fs @@ -192,7 +192,7 @@ let maintain (grps : SmallGroup list) ctx vi = |> List.map (fun g -> let grpId = flatGuid g.smallGroupId let delAction = sprintf "/small-group/%s/delete" grpId - let delPrompt = s.["Are you want to delete this {0}? This action cannot be undone.", + let delPrompt = s.["Are you sure you want to delete this {0}? This action cannot be undone.", sprintf "%s (%s)" (s.["Small Group"].Value.ToLower ()) g.name].Value tr [] [ td [] [ @@ -246,8 +246,10 @@ let members (mbrs : Member list) (emailTyps : Map) ctx |> List.map (fun mbr -> let mbrId = flatGuid mbr.memberId let delAction = sprintf "/small-group/member/%s/delete" mbrId - let delPrompt = s.["Are you want to delete this {0} ({1})? This action cannot be undone.", - s.["group member"], mbr.memberName].Value + let delPrompt = + s.["Are you sure you want to delete this {0}? This action cannot be undone.", s.["group member"]] + .Value + .Replace("?", sprintf " (%s)?" mbr.memberName) tr [] [ td [] [ a [ _href (sprintf "/small-group/member/%s/edit" mbrId); _title s.["Edit This Group Member"].Value ] diff --git a/src/PrayerTracker.UI/User.fs b/src/PrayerTracker.UI/User.fs index e2119ae..b6527f8 100644 --- a/src/PrayerTracker.UI/User.fs +++ b/src/PrayerTracker.UI/User.fs @@ -190,7 +190,7 @@ let maintain (users : User list) ctx vi = |> List.map (fun user -> let userId = flatGuid user.userId let delAction = sprintf "/user/%s/delete" userId - let delPrompt = s.["Are you want to delete this {0}? This action cannot be undone.", + let delPrompt = s.["Are you sure you want to delete this {0}? This action cannot be undone.", (sprintf "%s (%s)" (s.["User"].Value.ToLower()) user.fullName)].Value tr [] [ td [] [ diff --git a/src/PrayerTracker.UI/Utils.fs b/src/PrayerTracker.UI/Utils.fs index cc1621e..dda074e 100644 --- a/src/PrayerTracker.UI/Utils.fs +++ b/src/PrayerTracker.UI/Utils.fs @@ -127,6 +127,20 @@ let htmlToPlainText html = /// Get the second portion of a tuple as a string let sndAsString x = (snd >> string) x + +/// Make a URL with query string parameters +let makeUrl (url : string) (qs : (string * string) list) = + let queryString = + qs + |> List.fold + (fun (acc : StringBuilder) (key, value) -> + acc.Append(key).Append("=").Append(WebUtility.UrlEncode value).Append "&") + (StringBuilder ()) + match queryString.Length with + | 0 -> url + | _ -> queryString.Insert(0, "?").Insert(0, url).Remove(queryString.Length - 1, 1).ToString () + + /// "Magic string" repository [] module Key = diff --git a/src/PrayerTracker.UI/ViewModels.fs b/src/PrayerTracker.UI/ViewModels.fs index 0f162eb..023b093 100644 --- a/src/PrayerTracker.UI/ViewModels.fs +++ b/src/PrayerTracker.UI/ViewModels.fs @@ -473,7 +473,32 @@ with } +/// Items needed to display the request maintenance page +[] +type MaintainRequests = + { /// The requests to be displayed + requests : PrayerRequest seq + /// The small group to which the requests belong + smallGroup : SmallGroup + /// Whether only active requests are included + onlyActive : bool option + /// The search term for the requests + searchTerm : string option + /// The page number of the results + pageNbr : int option + } +with + static member empty = + { requests = Seq.empty + smallGroup = SmallGroup.empty + onlyActive = None + searchTerm = None + pageNbr = None + } + + /// Items needed to display the small group overview page +[] type Overview = { /// The total number of active requests totalActiveReqs : int diff --git a/src/PrayerTracker/PrayerRequest.fs b/src/PrayerTracker/PrayerRequest.fs index 19d49aa..6f1d75b 100644 --- a/src/PrayerTracker/PrayerRequest.fs +++ b/src/PrayerTracker/PrayerRequest.fs @@ -31,7 +31,7 @@ let private generateRequestList ctx date = match date with | Some d -> d | None -> grp.localDateNow clock - let reqs = ctx.dbContext().AllRequestsForSmallGroup grp clock (Some listDate) true + let reqs = ctx.dbContext().AllRequestsForSmallGroup grp clock (Some listDate) true 0 { requests = reqs |> List.ofSeq date = listDate listGroup = grp @@ -155,7 +155,7 @@ let list groupId : HttpHandler = match grp with | Some g when g.preferences.isPublic -> let clock = ctx.GetService () - let reqs = db.AllRequestsForSmallGroup g clock None true + let reqs = db.AllRequestsForSmallGroup g clock None true 0 return! viewInfo ctx startTicks |> Views.PrayerRequest.list @@ -199,13 +199,27 @@ let maintain onlyActive : HttpHandler = let db = ctx.dbContext () let grp = currentGroup ctx task { - let reqs = + let pageNbr = + match ctx.GetQueryStringValue "page" with + | Ok pg -> match Int32.TryParse pg with true, p -> p | false, _ -> 1 + | Error _ -> 1 + let m = match ctx.GetQueryStringValue "search" with - | Ok srch -> db.SearchRequestsForSmallGroup grp srch 1 - | Error _ -> db.AllRequestsForSmallGroup grp (ctx.GetService ()) None onlyActive + | Ok srch -> + { MaintainRequests.empty with + requests = db.SearchRequestsForSmallGroup grp srch pageNbr + searchTerm = Some srch + pageNbr = Some pageNbr + } + | Error _ -> + { MaintainRequests.empty with + requests = db.AllRequestsForSmallGroup grp (ctx.GetService ()) None onlyActive pageNbr + onlyActive = Some onlyActive + pageNbr = match onlyActive with true -> None | false -> Some pageNbr + } return! { viewInfo ctx startTicks with helpLink = Some Help.maintainRequests } - |> Views.PrayerRequest.maintain reqs grp onlyActive ctx + |> Views.PrayerRequest.maintain { m with smallGroup = grp } ctx |> renderHtml next ctx } diff --git a/src/PrayerTracker/SmallGroup.fs b/src/PrayerTracker/SmallGroup.fs index 89d687d..00cfff2 100644 --- a/src/PrayerTracker/SmallGroup.fs +++ b/src/PrayerTracker/SmallGroup.fs @@ -208,7 +208,7 @@ let overview : HttpHandler = let db = ctx.dbContext () let clock = ctx.GetService () task { - let reqs = db.AllRequestsForSmallGroup (currentGroup ctx) clock None true |> List.ofSeq + let reqs = db.AllRequestsForSmallGroup (currentGroup ctx) clock None true 0 |> List.ofSeq let! reqCount = db.CountRequestsBySmallGroup (currentGroup ctx).smallGroupId let! mbrCount = db.CountMembersForSmallGroup (currentGroup ctx).smallGroupId let m =