diff --git a/src/Data/Access.fs b/src/Data/Access.fs index 363be2e..ddb7896 100644 --- a/src/Data/Access.fs +++ b/src/Data/Access.fs @@ -68,6 +68,10 @@ module Json = opts +module private Helpers = + let instant (it: Instant) = + it.ToString() + open BitBadger.Documents open BitBadger.Documents.Sqlite @@ -120,19 +124,19 @@ module SmallGroups = /// Query to retrieve data for a small group info instance let private infoQuery = - $"SELECT g.data->>'id' AS id, g.data->>'groupName' AS groupName, c.data->>'churchName' AS churchName, + $"SELECT g.data->>'id' AS id, g.data->>'name' AS groupName, c.data->>'name' AS churchName, g.data->'preferences'->>'timeZoneId' AS timeZoneId, g.data->'preferences'->>'isPublic' AS isPublic FROM {Table.Group} g INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'" /// Query to retrieve data for a small group select list item let private itemQuery = - $"SELECT g.data->>'groupName' AS groupName, g.data->>'id' AS id, c.data->>'churchName' AS churchName + $"SELECT g.data->>'name' AS groupName, g.data->>'id' AS id, c.data->>'name' AS churchName FROM {Table.Group} g INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'" /// The ORDER BY clause for select list item queries - let private itemOrderBy = "ORDER BY c.data->>'churchName', g.data->>'groupName'" + let private itemOrderBy = "ORDER BY c.data->>'name', g.data->>'name'" /// Map a row to a Small Group list item let private toSmallGroupItem (rdr: SqliteDataReader) = @@ -142,13 +146,13 @@ module SmallGroups = /// Get the group IDs for the given church let internal groupIdsByChurch (churchId: ChurchId) = backgroundTask { - let! groups = Find.byFields Table.Group All [ Field.Equal "churchId" churchId ] + let! groups = Find.byFields Table.Group All [ Field.Equal "churchId" (string churchId) ] return groups |> List.map _.Id } /// Count the number of small groups for a church let countByChurch (churchId: ChurchId) = - Count.byFields Table.Group All [ Field.Equal "churchId" churchId ] + Count.byFields Table.Group All [ Field.Equal "churchId" (string churchId) ] /// Delete a small group by its ID let deleteById (groupId: SmallGroupId) = @@ -156,20 +160,21 @@ module SmallGroups = use conn = Configuration.dbConn () use! txn = conn.BeginTransactionAsync() - let! users = Find.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] + let! users = + Find.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ (string groupId) ] ] for user in users do do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |} - do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" groupId ] - do! conn.deleteById Table.Group groupId + do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" (string groupId) ] + do! conn.deleteById Table.Group (string groupId) do! txn.CommitAsync() } /// Get information for all small groups let infoForAll () = - Custom.list $"{infoQuery} ORDER BY g.data->>'groupName'" [] SmallGroupInfo.FromReader + Custom.list $"{infoQuery} ORDER BY g.data->>'name'" [] SmallGroupInfo.FromReader /// Get a list of small group IDs along with a description that includes the church name let listAll () = @@ -197,14 +202,14 @@ module SmallGroups = Find.firstByFields Table.Group All - [ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ] + [ Field.Equal "id" (string groupId); Field.Equal "preferences.groupPassword" password ] /// Save a small group let save group = save Table.Group group /// Save a small group's list preferences let savePreferences (groupId: SmallGroupId) (pref: ListPreferences) = - Patch.byId Table.Group groupId {| Preferences = pref |} + Patch.byId Table.Group (string groupId) {| Preferences = pref |} /// Get a small group by its ID (including list preferences) let tryById groupId = @@ -223,17 +228,18 @@ module Churches = use conn = Configuration.dbConn () use! txn = conn.BeginTransactionAsync() - let! groupIds = SmallGroups.groupIdsByChurch churchId + let! groupIds = SmallGroups.groupIdsByChurch churchId + let gIdStrings = groupIds |> List.map string - do! Delete.byFields Table.Request All [ Field.In "smallGroupId" groupIds ] + do! Delete.byFields Table.Request All [ Field.In "smallGroupId" gIdStrings ] - let! users = Find.byFields Table.User All [ Field.InArray "smallGroups" Table.User groupIds ] + let! users = Find.byFields Table.User All [ Field.InArray "smallGroups" Table.User gIdStrings ] for user in users do - do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except groupIds |} + do! Patch.byId Table.User (string user.Id) {| SmallGroups = user.SmallGroups |> List.except groupIds |} - do! Delete.byFields Table.Group All [ Field.Equal "churchId" churchId ] - do! Delete.byId Table.Church churchId + do! Delete.byFields Table.Group All [ Field.Equal "churchId" (string churchId) ] + do! Delete.byId Table.Church (string churchId) do! txn.CommitAsync() } @@ -250,17 +256,17 @@ module Members = /// Count members for the given small group let countByGroup (groupId: SmallGroupId) = - Count.byFields Table.Member All [ Field.Equal "smallGroupId" groupId ] + Count.byFields Table.Member All [ Field.Equal "smallGroupId" (string groupId) ] /// Delete a small group member by its ID - let deleteById (memberId: MemberId) = Delete.byId Table.Member memberId + let deleteById (memberId: MemberId) = Delete.byId Table.Member (string memberId) /// Retrieve all members for a given small group let forGroup (groupId: SmallGroupId) = Find.byFieldsOrdered Table.Member All - [ Field.Equal "smallGroupId" groupId ] + [ Field.Equal "smallGroupId" (string groupId) ] [ Field.Named "memberName" ] /// Save a small group member @@ -312,15 +318,15 @@ module PrayerRequests = let countByChurch churchId = backgroundTask { let! groupIds = SmallGroups.groupIdsByChurch churchId - return! Count.byFields Table.Request All [ Field.In "smallGroupId" groupIds ] + return! Count.byFields Table.Request All [ Field.In "smallGroupId" (List.map string groupIds) ] } /// Count the number of prayer requests for a small group let countByGroup (groupId: SmallGroupId) = - Count.byFields Table.Request All [ Field.Equal "smallGroupId" groupId ] + Count.byFields Table.Request All [ Field.Equal "smallGroupId" (string groupId) ] /// Delete a prayer request by its ID - let deleteById (reqId: PrayerRequestId) = Delete.byId Table.Request reqId + let deleteById (reqId: PrayerRequestId) = Delete.byId Table.Request (string reqId) /// Get all (or active) requests for a small group as of now or the specified date let forGroup (opts: PrayerRequestOptions) = @@ -332,11 +338,11 @@ module PrayerRequests = (theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone) - Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire) .ToInstant() - $"""AND ( data->>'updatedDate' > :updatedDate - OR data->>'expiration' = :expManual - OR data->>'requestType' IN (:typLongTerm, :typExpecting)) + $"""AND ( date(data->>'updatedDate') > date(:updatedDate) + OR data->>'expiration' = :expManual + OR data->>'requestType' IN (:typLongTerm, :typExpecting)) AND data->>'expiration' <> :expForced""", - [ SqliteParameter(":updatedDate", expDate) + [ SqliteParameter(":updatedDate", string expDate) SqliteParameter(":expManual", string Manual) SqliteParameter(":typLongTerm", string LongTermRequest) SqliteParameter(":typExpecting", string Expecting) @@ -346,8 +352,9 @@ module PrayerRequests = Custom.list $"SELECT data FROM {Table.Request} - WHERE data->>'smallGroupId = :groupId {sql} - ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort} + WHERE data->>'smallGroupId' = :groupId + {sql} + {orderBy opts.SmallGroup.Preferences.RequestSort} {paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}" (SqliteParameter(":groupId", string opts.SmallGroup.Id) :: parameters) fromData @@ -357,18 +364,20 @@ module PrayerRequests = /// Search prayer requests for the given term let searchForGroup group searchTerm pageNbr = + let pct = "%" Custom.list - $"SELECT data FROM {Table.Request} - WHERE data->>'smallGroupId' = :groupId - AND data->>'requestText' LIKE :search - UNION - SELECT data FROM {Table.Request} - WHERE data->>'smallGroupId' = :groupId - AND COALESCE(data->>'requestor', '') LIKE :search - ORDER BY {orderBy group.Preferences.RequestSort} + $"WITH results AS ( + SELECT data FROM {Table.Request} + WHERE data->>'smallGroupId' = :groupId + AND data->>'text' LIKE :search + UNION + SELECT data FROM {Table.Request} + WHERE data->>'smallGroupId' = :groupId + AND COALESCE(data->>'requestor', '') LIKE :search) + SELECT data FROM results + {orderBy group.Preferences.RequestSort} {paginate pageNbr group.Preferences.PageSize}" - [ SqliteParameter(":groupId", string group.Id) - SqliteParameter(":search", $"%%%s{searchTerm}%%") ] + [ SqliteParameter(":groupId", string group.Id); SqliteParameter(":search", $"{pct}%s{searchTerm}{pct}") ] fromData /// Retrieve a prayer request by its ID @@ -380,11 +389,11 @@ module PrayerRequests = if withTime then Patch.byId Table.Request - req.Id + (string req.Id) {| UpdatedDate = req.UpdatedDate Expiration = req.Expiration |} else - Patch.byId Table.Request req.Id {| Expiration = req.Expiration |} + Patch.byId Table.Request (string req.Id) {| Expiration = req.Expiration |} /// Functions to manipulate users @@ -398,22 +407,22 @@ module Users = let countByChurch churchId = backgroundTask { let! groupIds = SmallGroups.groupIdsByChurch churchId - return! Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User groupIds ] + return! Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User (List.map string groupIds) ] } /// Count the number of users for a small group let countByGroup (groupId: SmallGroupId) = - Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] + Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ (string groupId) ] ] /// Delete a user by its database ID - let deleteById (userId: UserId) = Delete.byId Table.User userId + let deleteById (userId: UserId) = Delete.byId Table.User (string userId) /// Get a list of users authorized to administer the given small group let listByGroupId (groupId: SmallGroupId) = Find.byFieldsOrdered Table.User All - [ Field.InArray "smallGroups" Table.User [ groupId ] ] + [ Field.InArray "smallGroups" Table.User [ (string groupId) ] ] [ Field.Named "lastName"; Field.Named "firstName" ] /// Save a user's information @@ -425,7 +434,7 @@ module Users = Table.User All [ Field.Equal "email" email - Field.InArray "smallGroups" Table.User [ groupId ] ] + Field.InArray "smallGroups" Table.User [ (string groupId) ] ] /// Find a user by their database ID let tryById userId = @@ -433,12 +442,12 @@ module Users = /// Update a user's last seen date/time let updateLastSeen (userId: UserId) (now: Instant) = - Patch.byId Table.User userId {| LastSeen = now |} + Patch.byId Table.User (string userId) {| LastSeen = now |} /// Update a user's password hash let updatePassword (user: User) = - Patch.byId Table.User user.Id {| PasswordHash = user.PasswordHash |} + Patch.byId Table.User (string user.Id) {| PasswordHash = user.PasswordHash |} /// Update a user's authorized small groups let updateSmallGroups (userId: UserId) (groupIds: SmallGroupId list) = - Patch.byId Table.User userId {| SmallGroups = groupIds |} + Patch.byId Table.User (string userId) {| SmallGroups = groupIds |} diff --git a/src/PrayerTracker/App.fs b/src/PrayerTracker/App.fs index 47b7738..f47f8c4 100644 --- a/src/PrayerTracker/App.fs +++ b/src/PrayerTracker/App.fs @@ -87,6 +87,7 @@ module Configure = let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SessionDB")) "./data/session.db" let _ = svc.AddSqliteCache(fun o -> o.CachePath <- cachePath) let _ = svc.AddSession() + let _ = svc.AddLogging() let _ = svc.AddAntiforgery() let _ = svc.AddRouting() let _ = svc.AddSingleton SystemClock.Instance @@ -219,8 +220,8 @@ module Configure = open Microsoft.Extensions.Options /// Configure the application - let app (app: IApplicationBuilder) = - let env = app.ApplicationServices.GetRequiredService() + let app (app: WebApplication) = + let env = app.Services.GetRequiredService() if env.IsDevelopment() then app.UseDeveloperExceptionPage() @@ -232,24 +233,20 @@ module Configure = let _ = app.UseCanonicalDomains() let _ = app.UseStatusCodePagesWithReExecute "/error/{0}" let _ = app.UseStaticFiles() - - let _ = - app.UseCookiePolicy(CookiePolicyOptions(MinimumSameSitePolicy = SameSiteMode.Strict)) - + let _ = app.UseCookiePolicy(CookiePolicyOptions(MinimumSameSitePolicy = SameSiteMode.Strict)) let _ = app.UseMiddleware() let _ = app.UseRouting() let _ = app.UseSession() - - let _ = - app.UseRequestLocalization(app.ApplicationServices.GetService>().Value) - + let _ = app.UseRequestLocalization(app.Services.GetService>().Value) let _ = app.UseAuthentication() let _ = app.UseAuthorization() let _ = app.UseEndpoints(fun e -> e.MapGiraffeEndpoints routes) - app.ApplicationServices.GetRequiredService() + app.Services.GetRequiredService() |> Views.I18N.setUpFactories +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Logging /// The web application module App = @@ -258,22 +255,32 @@ module App = [] let main args = - let contentRoot = Directory.GetCurrentDirectory() - let app = - WebHostBuilder() - .UseContentRoot(contentRoot) + let contentRoot = Directory.GetCurrentDirectory() + let builder = + WebApplication.CreateBuilder( + WebApplicationOptions( + Args = args, + ApplicationName = "PrayerTracker", + ContentRootPath = contentRoot, + WebRootPath = Path.Combine(contentRoot, "wwwroot"))) + let _ = + builder.WebHost .ConfigureAppConfiguration(Configure.configuration) - .UseKestrel(Configure.kestrel) - .UseWebRoot(Path.Combine(contentRoot, "wwwroot")) + .ConfigureKestrel(Configure.kestrel) .ConfigureServices(Configure.services) .ConfigureLogging(Configure.logging) - .Configure(System.Action Configure.app) - .Build() - if args.Length > 0 then - printfn $"Unrecognized option {args[0]}" - else - app.Run() + use app = builder.Build() + + Configure.app app + + let fac = app.Services.GetRequiredService() + let log = fac.CreateLogger "PrayerTracker" + log.LogInformation "Application Started" + + app.Run() + + log.LogInformation "Application Shutting Down" 0 diff --git a/src/PrayerTracker/CommonFunctions.fs b/src/PrayerTracker/CommonFunctions.fs index c0ad350..70d3ae9 100644 --- a/src/PrayerTracker/CommonFunctions.fs +++ b/src/PrayerTracker/CommonFunctions.fs @@ -13,11 +13,11 @@ let toSelectList<'T> valFunc textFunc withDefault emptyText (items: 'T seq) = SelectListItem($"""— %A{s[emptyText]} —""", "") | _ -> () yield! items |> Seq.map (fun x -> SelectListItem(textFunc x, valFunc x)) ] - + /// Create a select list from an enumeration let toSelectListWithEmpty<'T> valFunc textFunc emptyText (items: 'T seq) = toSelectList valFunc textFunc true emptyText items - + /// Create a select list from an enumeration let toSelectListWithDefault<'T> valFunc textFunc (items: 'T seq) = toSelectList valFunc textFunc true "Select" items @@ -117,7 +117,7 @@ let addInfo ctx msg = /// Add an informational HTML message to the session let addHtmlInfo ctx msg = addUserMessage ctx { UserMessage.info with Text = htmlString msg } - + /// Add a warning message to the session let addWarning ctx msg = addUserMessage ctx { UserMessage.warning with Text = htmlLocString msg } diff --git a/src/UI/CommonFunctions.fs b/src/UI/CommonFunctions.fs index cbb62b3..0295d5d 100644 --- a/src/UI/CommonFunctions.fs +++ b/src/UI/CommonFunctions.fs @@ -182,13 +182,6 @@ let toHtmlIds it = let renderHtmlNode = RenderView.AsString.htmlNode -open Giraffe.Fixi - -/// Create a page link that will make the request with fixi -let pageLink href attrs content = - a (List.append [ _href href; _fxGet; _fxAction href; _fxTarget "#pt-body" ] attrs) content - - open Microsoft.AspNetCore.Html /// Render an HTML node, then return the value as an HTML string @@ -224,6 +217,10 @@ module TimeZones = open Giraffe.ViewEngine.Htmx +/// Create a page link that will make the request with fixi +let pageLink href attrs content = + a (List.append [ _href href; _hxGet href ] attrs) content + /// Known htmx targets module Target = diff --git a/src/UI/Layout.fs b/src/UI/Layout.fs index 2e5e64e..7cf56eb 100644 --- a/src/UI/Layout.fs +++ b/src/UI/Layout.fs @@ -12,7 +12,7 @@ let langCode () = if CultureInfo.CurrentCulture.Name.StartsWith "es" then "es" e /// Navigation items module Navigation = - + /// Top navigation bar let top m = let s = I18N.localizer.Force() @@ -103,7 +103,7 @@ module Navigation = section [ _class "pt-title-bar-center"; _ariaLabel "Empty center space in top menu" ] [] section [ _class "pt-title-bar-right"; _roleToolBar; _ariaLabel "Right side of top menu" ] [ ul [] rightLinks ] ] - + /// Identity bar (below top nav) let identity m = let s = I18N.localizer.Force() @@ -111,7 +111,7 @@ module Navigation = div [] [ span [ _title s["Language"].Value ] [ icon "record_voice_over"; space ] match langCode () with - | "es" -> + | "es" -> strong [] [ locStr s["Spanish"] ] rawText "     " pageLink "/language/en" [] [ locStr s["Change to English"] ] @@ -142,14 +142,14 @@ module Navigation = /// Content layouts module Content = - + /// Content layout that tops at 60rem let standard = div [ _class "pt-content" ] /// Content layout that uses the full width of the browser window let wide = div [ _class "pt-content pt-full-width" ] - + /// Separator for parts of the title let private titleSep = rawText " « " @@ -274,14 +274,14 @@ let private partialHead pgTitle = let private pageLayout viewInfo pgTitle content = body [] [ Navigation.top viewInfo - div [ _id "pt-body" ] (contentSection viewInfo pgTitle content) + div [ _id "pt-body"; Target.content ] (contentSection viewInfo pgTitle content) match viewInfo.Layout with | FullPage -> script [ _src "/js/ckeditor/ckeditor.js" ] [] - script [ _src "/_/fixi-0.5.7.js" ] [] + Htmx.Script.minified script [ _src "/_/app.js" ] [] | _ -> () ] - + /// The standard layout(s) for PrayerTracker let standard viewInfo pageTitle content = let s = I18N.localizer.Force() @@ -328,7 +328,7 @@ let help pageTitle isHome content = div [] [ locStr s["Language"]; rawText ": " match langCode () with - | "es" -> + | "es" -> locStr s["Spanish"]; rawText " • " a [ _href "/language/en" ] [ locStr s["Change to English"] ] | _ -> @@ -348,4 +348,3 @@ let help pageTitle isHome content = p [ _class "pt-center-text" ] [ a [ _href "/help"; _title s["Help Index"].Value ] [ rawText "« "; locStr s["Back to Help Index"] ] ] ] ] ] ] ] - \ No newline at end of file diff --git a/src/UI/PrayerTracker.UI.fsproj b/src/UI/PrayerTracker.UI.fsproj index ff2cf99..3ca48e8 100644 --- a/src/UI/PrayerTracker.UI.fsproj +++ b/src/UI/PrayerTracker.UI.fsproj @@ -15,7 +15,6 @@ -