6 Commits
v8 ... v8.1

Author SHA1 Message Date
dd5f32e320 v8.1 (#47)
- Update to .NET 7
- Update database access to use data source vs. connection
- Allow e-mail server address to be configurable
- Support Docker hosting
2023-07-04 20:03:43 -04:00
370fbb0c3e Fix time zone in tests 2023-05-25 19:45:28 -04:00
14608de5be Update doc dependencies 2023-05-23 21:37:06 -04:00
ec6eea21c8 Merge pull request #45 from bit-badger/dependabot/nuget/src/PrayerTracker.UI/Newtonsoft.Json-13.0.2
Bump Newtonsoft.Json from 13.0.1 to 13.0.2 in /src/PrayerTracker.UI
2023-05-23 21:24:16 -04:00
dependabot[bot]
3b0d304595 Bump Newtonsoft.Json from 13.0.1 to 13.0.2 in /src/PrayerTracker.UI
Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 13.0.1 to 13.0.2.
- [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases)
- [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/13.0.1...13.0.2)

---
updated-dependencies:
- dependency-name: Newtonsoft.Json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-08 14:53:23 +00:00
50a91cd725 Remove v8 migration code
(and prior)
2022-08-19 15:37:48 -04:00
27 changed files with 644 additions and 983 deletions

1
.gitignore vendored
View File

@@ -335,3 +335,4 @@ docs/_site
**/*.db* **/*.db*
.ionide .ionide
.vscode

View File

@@ -26,7 +26,7 @@ Target.create "Test" (fun _ ->
let testPath = $"{projPath}.Tests" let testPath = $"{projPath}.Tests"
DotNet.build (fun opts -> { opts with NoLogo = true }) $"{testPath}/PrayerTracker.Tests.fsproj" DotNet.build (fun opts -> { opts with NoLogo = true }) $"{testPath}/PrayerTracker.Tests.fsproj"
Expecto.run Expecto.run
(fun opts -> { opts with WorkingDirectory = $"{testPath}/bin/Release/net6.0" }) (fun opts -> { opts with WorkingDirectory = $"{testPath}/bin/Release/net7.0" })
[ "PrayerTracker.Tests.dll" ]) [ "PrayerTracker.Tests.dll" ])
Target.create "Publish" (fun _ -> Target.create "Publish" (fun _ ->

View File

@@ -1,97 +1,98 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
activesupport (3.2.22.5) activesupport (7.0.4.3)
i18n (~> 0.6, >= 0.6.4) concurrent-ruby (~> 1.0, >= 1.0.2)
multi_json (~> 1.0) i18n (>= 1.6, < 2)
addressable (2.8.0) minitest (>= 5.1)
public_suffix (>= 2.0.2, < 5.0) tzinfo (~> 2.0)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
coffee-script (2.4.1) coffee-script (2.4.1)
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.11.1) coffee-script-source (1.11.1)
colorator (1.1.0) colorator (1.1.0)
commonmarker (0.17.13) commonmarker (0.23.9)
ruby-enum (~> 0.5) concurrent-ruby (1.2.2)
concurrent-ruby (1.1.7) dnsruby (1.70.0)
dnsruby (1.61.5) simpleidn (~> 0.2.1)
simpleidn (~> 0.1) em-websocket (0.5.3)
em-websocket (0.5.2)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0)
ethon (0.12.0) ethon (0.16.0)
ffi (>= 1.3.0) ffi (>= 1.15.0)
eventmachine (1.2.7) eventmachine (1.2.7)
eventmachine (1.2.7-x64-mingw32) execjs (2.8.1)
execjs (2.7.0) faraday (2.7.5)
faraday (1.1.0) faraday-net_http (>= 2.0, < 3.1)
multipart-post (>= 1.2, < 3) ruby2_keywords (>= 0.0.4)
ruby2_keywords faraday-net_http (3.0.2)
ffi (1.13.1) ffi (1.15.5)
ffi (1.13.1-x64-mingw32)
forwardable-extended (2.6.0) forwardable-extended (2.6.0)
gemoji (3.0.1) gemoji (3.0.1)
github-pages (209) github-pages (228)
github-pages-health-check (= 1.16.1) github-pages-health-check (= 1.17.9)
jekyll (= 3.9.0) jekyll (= 3.9.3)
jekyll-avatar (= 0.7.0) jekyll-avatar (= 0.7.0)
jekyll-coffeescript (= 1.1.1) jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.6) jekyll-commonmark-ghpages (= 0.4.0)
jekyll-default-layout (= 0.1.4) jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.15.1) jekyll-feed (= 0.15.1)
jekyll-gist (= 1.5.0) jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.13.0) jekyll-github-metadata (= 2.13.0)
jekyll-include-cache (= 0.2.1)
jekyll-mentions (= 1.6.0) jekyll-mentions (= 1.6.0)
jekyll-optional-front-matter (= 0.3.2) jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0) jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.3.0) jekyll-readme-index (= 0.3.0)
jekyll-redirect-from (= 0.16.0) jekyll-redirect-from (= 0.16.0)
jekyll-relative-links (= 0.6.1) jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.2) jekyll-remote-theme (= 0.4.3)
jekyll-sass-converter (= 1.5.2) jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.6.1) jekyll-seo-tag (= 2.8.0)
jekyll-sitemap (= 1.4.0) jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0) jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.1.1) jekyll-theme-architect (= 0.2.0)
jekyll-theme-cayman (= 0.1.1) jekyll-theme-cayman (= 0.2.0)
jekyll-theme-dinky (= 0.1.1) jekyll-theme-dinky (= 0.2.0)
jekyll-theme-hacker (= 0.1.2) jekyll-theme-hacker (= 0.2.0)
jekyll-theme-leap-day (= 0.1.1) jekyll-theme-leap-day (= 0.2.0)
jekyll-theme-merlot (= 0.1.1) jekyll-theme-merlot (= 0.2.0)
jekyll-theme-midnight (= 0.1.1) jekyll-theme-midnight (= 0.2.0)
jekyll-theme-minimal (= 0.1.1) jekyll-theme-minimal (= 0.2.0)
jekyll-theme-modernist (= 0.1.1) jekyll-theme-modernist (= 0.2.0)
jekyll-theme-primer (= 0.5.4) jekyll-theme-primer (= 0.6.0)
jekyll-theme-slate (= 0.1.1) jekyll-theme-slate (= 0.2.0)
jekyll-theme-tactile (= 0.1.1) jekyll-theme-tactile (= 0.2.0)
jekyll-theme-time-machine (= 0.1.1) jekyll-theme-time-machine (= 0.2.0)
jekyll-titles-from-headings (= 0.5.3) jekyll-titles-from-headings (= 0.5.3)
jemoji (= 0.12.0) jemoji (= 0.12.0)
kramdown (= 2.3.0) kramdown (= 2.3.2)
kramdown-parser-gfm (= 1.1.0) kramdown-parser-gfm (= 1.1.0)
liquid (= 4.0.3) liquid (= 4.0.4)
mercenary (~> 0.3) mercenary (~> 0.3)
minima (= 2.5.1) minima (= 2.5.1)
nokogiri (>= 1.10.4, < 2.0) nokogiri (>= 1.13.6, < 2.0)
rouge (= 3.23.0) rouge (= 3.26.0)
terminal-table (~> 1.4) terminal-table (~> 1.4)
github-pages-health-check (1.16.1) github-pages-health-check (1.17.9)
addressable (~> 2.3) addressable (~> 2.3)
dnsruby (~> 1.60) dnsruby (~> 1.60)
octokit (~> 4.0) octokit (~> 4.0)
public_suffix (~> 3.0) public_suffix (>= 3.0, < 5.0)
typhoeus (~> 1.3) typhoeus (~> 1.3)
html-pipeline (2.14.0) html-pipeline (2.14.3)
activesupport (>= 2) activesupport (>= 2)
nokogiri (>= 1.4) nokogiri (>= 1.4)
http_parser.rb (0.6.0) http_parser.rb (0.8.0)
i18n (0.9.5) i18n (1.13.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jekyll (3.9.0) jekyll (3.9.3)
addressable (~> 2.4) addressable (~> 2.4)
colorator (~> 1.0) colorator (~> 1.0)
em-websocket (~> 0.5) em-websocket (~> 0.5)
i18n (~> 0.7) i18n (>= 0.7, < 2)
jekyll-sass-converter (~> 1.0) jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0) jekyll-watch (~> 2.0)
kramdown (>= 1.17, < 3) kramdown (>= 1.17, < 3)
@@ -105,13 +106,13 @@ GEM
jekyll-coffeescript (1.1.1) jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2) coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1) coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.3.1) jekyll-commonmark (1.4.0)
commonmarker (~> 0.14) commonmarker (~> 0.22)
jekyll (>= 3.7, < 5.0) jekyll-commonmark-ghpages (0.4.0)
jekyll-commonmark-ghpages (0.1.6) commonmarker (~> 0.23.7)
commonmarker (~> 0.17.6) jekyll (~> 3.9.0)
jekyll-commonmark (~> 1.2) jekyll-commonmark (~> 1.4.0)
rouge (>= 2.0, < 4.0) rouge (>= 2.0, < 5.0)
jekyll-default-layout (0.1.4) jekyll-default-layout (0.1.4)
jekyll (~> 3.0) jekyll (~> 3.0)
jekyll-feed (0.15.1) jekyll-feed (0.15.1)
@@ -121,6 +122,8 @@ GEM
jekyll-github-metadata (2.13.0) jekyll-github-metadata (2.13.0)
jekyll (>= 3.4, < 5.0) jekyll (>= 3.4, < 5.0)
octokit (~> 4.0, != 4.4.0) octokit (~> 4.0, != 4.4.0)
jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0)
jekyll-mentions (1.6.0) jekyll-mentions (1.6.0)
html-pipeline (~> 2.3) html-pipeline (~> 2.3)
jekyll (>= 3.7, < 5.0) jekyll (>= 3.7, < 5.0)
@@ -133,57 +136,57 @@ GEM
jekyll (>= 3.3, < 5.0) jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1) jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0) jekyll (>= 3.3, < 5.0)
jekyll-remote-theme (0.4.2) jekyll-remote-theme (0.4.3)
addressable (~> 2.0) addressable (~> 2.0)
jekyll (>= 3.5, < 5.0) jekyll (>= 3.5, < 5.0)
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
rubyzip (>= 1.3.0, < 3.0) rubyzip (>= 1.3.0, < 3.0)
jekyll-sass-converter (1.5.2) jekyll-sass-converter (1.5.2)
sass (~> 3.4) sass (~> 3.4)
jekyll-seo-tag (2.6.1) jekyll-seo-tag (2.8.0)
jekyll (>= 3.3, < 5.0) jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0) jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0) jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0) jekyll-swiss (1.0.0)
jekyll-theme-architect (0.1.1) jekyll-theme-architect (0.2.0)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.1.2)
jekyll (> 3.5, < 5.0) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.1.1) jekyll-theme-cayman (0.2.0)
jekyll (~> 3.5) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.1.1) jekyll-theme-dinky (0.2.0)
jekyll (~> 3.5) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.1.1) jekyll-theme-hacker (0.2.0)
jekyll (~> 3.5) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.1.1) jekyll-theme-leap-day (0.2.0)
jekyll (~> 3.5) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.1.1) jekyll-theme-merlot (0.2.0)
jekyll (~> 3.5) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.4) jekyll-theme-midnight (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.6.0)
jekyll (> 3.5, < 5.0) jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9) jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1) jekyll-theme-slate (0.2.0)
jekyll (~> 3.5) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.1.1) jekyll-theme-tactile (0.2.0)
jekyll (~> 3.5) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.1.1) jekyll-theme-time-machine (0.2.0)
jekyll (~> 3.5) jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.3) jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0) jekyll (>= 3.3, < 5.0)
@@ -193,75 +196,64 @@ GEM
gemoji (~> 3.0) gemoji (~> 3.0)
html-pipeline (~> 2.2) html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0) jekyll (>= 3.0, < 5.0)
kramdown (2.3.0) kramdown (2.3.2)
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
liquid (4.0.3) liquid (4.0.4)
listen (3.3.1) listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6) mercenary (0.3.6)
mini_portile2 (2.8.0)
minima (2.5.1) minima (2.5.1)
jekyll (>= 3.5, < 5.0) jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9) jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
multi_json (1.15.0) minitest (5.18.0)
multipart-post (2.1.1) nokogiri (1.15.1-x86_64-linux)
nokogiri (1.13.6)
mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.6-x64-mingw32) octokit (4.25.1)
racc (~> 1.4) faraday (>= 1, < 3)
octokit (4.19.0) sawyer (~> 0.9)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2) pathutil (0.16.2)
forwardable-extended (~> 2.6) forwardable-extended (~> 2.6)
public_suffix (3.1.1) public_suffix (4.0.7)
racc (1.6.0) racc (1.6.2)
rb-fsevent (0.10.4) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
rexml (3.2.5) rexml (3.2.5)
rouge (3.23.0) rouge (3.26.0)
ruby-enum (0.8.0) ruby2_keywords (0.0.5)
i18n rubyzip (2.3.2)
ruby2_keywords (0.0.2)
rubyzip (2.3.0)
safe_yaml (1.0.5) safe_yaml (1.0.5)
sass (3.7.4) sass (3.7.4)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.2) sawyer (0.9.2)
addressable (>= 2.3.5) addressable (>= 2.3.5)
faraday (> 0.8, < 2.0) faraday (>= 0.17.3, < 3)
simpleidn (0.1.1) simpleidn (0.2.1)
unf (~> 0.1.4) unf (~> 0.1.4)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
typhoeus (1.4.0) typhoeus (1.4.0)
ethon (>= 0.9.0) ethon (>= 0.9.0)
tzinfo (2.0.3) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2020.4)
tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.7) unf_ext (0.0.8.2)
unf_ext (0.0.7.7-x64-mingw32) unicode-display_width (1.8.0)
unicode-display_width (1.7.0)
PLATFORMS PLATFORMS
ruby x86_64-linux
x64-mingw32
DEPENDENCIES DEPENDENCIES
github-pages github-pages
tzinfo-data tzinfo-data
BUNDLED WITH BUNDLED WITH
2.1.4 2.3.15

2
src/.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
**/bin/*
**/obj/*

View File

@@ -1,11 +1,11 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<AssemblyVersion>8.0.0.0</AssemblyVersion> <AssemblyVersion>8.1.0.0</AssemblyVersion>
<FileVersion>8.0.0.0</FileVersion> <FileVersion>8.1.0.0</FileVersion>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<Version>8.0.0</Version> <Version>8.1.0</Version>
<DebugType>Embedded</DebugType> <DebugType>Embedded</DebugType>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

25
src/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build
WORKDIR /pt
COPY ./PrayerTracker.sln ./
COPY ./Directory.Build.props ./
COPY ./PrayerTracker/PrayerTracker.fsproj ./PrayerTracker/
COPY ./PrayerTracker.Data/PrayerTracker.Data.fsproj ./PrayerTracker.Data/
COPY ./PrayerTracker.Tests/PrayerTracker.Tests.fsproj ./PrayerTracker.Tests/
COPY ./PrayerTracker.UI/PrayerTracker.UI.fsproj ./PrayerTracker.UI/
RUN dotnet restore
COPY . ./
WORKDIR /pt/PrayerTracker.Tests
RUN dotnet run
WORKDIR /pt/PrayerTracker
RUN dotnet publish -c Release -r linux-x64
FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine as final
WORKDIR /app
RUN apk add --no-cache icu-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
COPY --from=build /pt/PrayerTracker/bin/Release/net7.0/linux-x64/publish/ ./
EXPOSE 80
CMD [ "dotnet", "/app/PrayerTracker.dll" ]

View File

@@ -103,36 +103,35 @@ module private Helpers =
} }
open BitBadger.Npgsql.FSharp.Documents
/// Functions to manipulate churches /// Functions to manipulate churches
module Churches = module Churches =
/// Get a list of all churches /// Get a list of all churches
let all conn = let all () =
Sql.existingConnection conn Custom.list "SELECT * FROM pt.church ORDER BY church_name" [] mapToChurch
|> Sql.query "SELECT * FROM pt.church ORDER BY church_name"
|> Sql.executeAsync mapToChurch
/// Delete a church by its ID /// Delete a church by its ID
let deleteById (churchId : ChurchId) conn = backgroundTask { let deleteById (churchId : ChurchId) = backgroundTask {
let idParam = [ [ "@churchId", Sql.uuid churchId.Value ] ] 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 where = "WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)"
let! _ = let! _ =
Sql.existingConnection conn Configuration.dataSource ()
|> Sql.fromDataSource
|> Sql.executeTransactionAsync |> Sql.executeTransactionAsync
[ $"DELETE FROM pt.prayer_request {where}", idParam [ $"DELETE FROM pt.prayer_request {where}", idParam
$"DELETE FROM pt.user_small_group {where}", idParam $"DELETE FROM pt.user_small_group {where}", idParam
$"DELETE FROM pt.list_preference {where}", idParam $"DELETE FROM pt.list_preference {where}", idParam
"DELETE FROM pt.small_group WHERE church_id = @churchId", idParam "DELETE FROM pt.small_group WHERE church_id = @churchId", idParam
"DELETE FROM pt.church WHERE id = @churchId", idParam ] "DELETE FROM pt.church WHERE id = @churchId", idParam ]
return () ()
} }
/// Save a church's information /// Save a church's information
let save (church : Church) conn = backgroundTask { let save (church : Church) =
let! _ = Custom.nonQuery
Sql.existingConnection conn "INSERT INTO pt.church (
|> Sql.query """
INSERT INTO pt.church (
id, church_name, city, state, has_vps_interface, interface_address id, church_name, city, state, has_vps_interface, interface_address
) VALUES ( ) VALUES (
@id, @name, @city, @state, @hasVpsInterface, @interfaceAddress @id, @name, @city, @state, @hasVpsInterface, @interfaceAddress
@@ -141,88 +140,56 @@ module Churches =
city = EXCLUDED.city, city = EXCLUDED.city,
state = EXCLUDED.state, state = EXCLUDED.state,
has_vps_interface = EXCLUDED.has_vps_interface, has_vps_interface = EXCLUDED.has_vps_interface,
interface_address = EXCLUDED.interface_address""" interface_address = EXCLUDED.interface_address"
|> Sql.parameters
[ "@id", Sql.uuid church.Id.Value [ "@id", Sql.uuid church.Id.Value
"@name", Sql.string church.Name "@name", Sql.string church.Name
"@city", Sql.string church.City "@city", Sql.string church.City
"@state", Sql.string church.State "@state", Sql.string church.State
"@hasVpsInterface", Sql.bool church.HasVpsInterface "@hasVpsInterface", Sql.bool church.HasVpsInterface
"@interfaceAddress", Sql.stringOrNone church.InterfaceAddress ] "@interfaceAddress", Sql.stringOrNone church.InterfaceAddress ]
|> Sql.executeNonQueryAsync
return ()
}
/// Find a church by its ID /// Find a church by its ID
let tryById (churchId : ChurchId) conn = backgroundTask { let tryById (churchId : ChurchId) =
let! church = Custom.single "SELECT * FROM pt.church WHERE id = @id" [ "@id", Sql.uuid churchId.Value ] mapToChurch
Sql.existingConnection conn
|> Sql.query "SELECT * FROM pt.church WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid churchId.Value ]
|> Sql.executeAsync mapToChurch
return List.tryHead church
}
/// Functions to manipulate small group members /// Functions to manipulate small group members
module Members = module Members =
/// Count members for the given small group /// Count members for the given small group
let countByGroup (groupId : SmallGroupId) conn = let countByGroup (groupId : SmallGroupId) =
Sql.existingConnection conn Custom.scalar "SELECT COUNT(id) AS mbr_count FROM pt.member WHERE small_group_id = @groupId"
|> Sql.query "SELECT COUNT(id) AS mbr_count FROM pt.member WHERE small_group_id = @groupId" [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "mbr_count")
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ]
|> Sql.executeRowAsync (fun row -> row.int "mbr_count")
/// Delete a small group member by its ID /// Delete a small group member by its ID
let deleteById (memberId : MemberId) conn = backgroundTask { let deleteById (memberId : MemberId) =
let! _ = Custom.nonQuery "DELETE FROM pt.member WHERE id = @id" [ "@id", Sql.uuid memberId.Value ]
Sql.existingConnection conn
|> Sql.query "DELETE FROM pt.member WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid memberId.Value ]
|> Sql.executeNonQueryAsync
return ()
}
/// Retrieve all members for a given small group /// Retrieve all members for a given small group
let forGroup (groupId : SmallGroupId) conn = let forGroup (groupId : SmallGroupId) =
Sql.existingConnection conn Custom.list "SELECT * FROM pt.member WHERE small_group_id = @groupId ORDER BY member_name"
|> Sql.query "SELECT * FROM pt.member WHERE small_group_id = @groupId ORDER BY member_name" [ "@groupId", Sql.uuid groupId.Value ] mapToMember
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ]
|> Sql.executeAsync mapToMember
/// Save a small group member /// Save a small group member
let save (mbr : Member) conn = backgroundTask { let save (mbr : Member) =
let! _ = Custom.nonQuery
Sql.existingConnection conn "INSERT INTO pt.member (
|> Sql.query """
INSERT INTO pt.member (
id, small_group_id, member_name, email, email_format id, small_group_id, member_name, email, email_format
) VALUES ( ) VALUES (
@id, @groupId, @name, @email, @format @id, @groupId, @name, @email, @format
) ON CONFLICT (id) DO UPDATE ) ON CONFLICT (id) DO UPDATE
SET member_name = EXCLUDED.member_name, SET member_name = EXCLUDED.member_name,
email = EXCLUDED.email, email = EXCLUDED.email,
email_format = EXCLUDED.email_format""" email_format = EXCLUDED.email_format"
|> Sql.parameters
[ "@id", Sql.uuid mbr.Id.Value [ "@id", Sql.uuid mbr.Id.Value
"@groupId", Sql.uuid mbr.SmallGroupId.Value "@groupId", Sql.uuid mbr.SmallGroupId.Value
"@name", Sql.string mbr.Name "@name", Sql.string mbr.Name
"@email", Sql.string mbr.Email "@email", Sql.string mbr.Email
"@format", Sql.stringOrNone (mbr.Format |> Option.map EmailFormat.toCode) ] "@format", Sql.stringOrNone (mbr.Format |> Option.map EmailFormat.toCode) ]
|> Sql.executeNonQueryAsync
return ()
}
/// Retrieve a small group member by its ID /// Retrieve a small group member by its ID
let tryById (memberId : MemberId) conn = backgroundTask { let tryById (memberId : MemberId) =
let! mbr = Custom.single "SELECT * FROM pt.member WHERE id = @id" [ "@id", Sql.uuid memberId.Value ] mapToMember
Sql.existingConnection conn
|> Sql.query "SELECT * FROM pt.member WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid memberId.Value ]
|> Sql.executeAsync mapToMember
return List.tryHead mbr
}
/// Options to retrieve a list of requests /// Options to retrieve a list of requests
@@ -258,34 +225,24 @@ module PrayerRequests =
if pageNbr > 0 then $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" else "" if pageNbr > 0 then $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" else ""
/// Count the number of prayer requests for a church /// Count the number of prayer requests for a church
let countByChurch (churchId : ChurchId) conn = let countByChurch (churchId : ChurchId) =
Sql.existingConnection conn Custom.scalar
|> Sql.query """ "SELECT COUNT(id) AS req_count
SELECT COUNT(id) AS req_count
FROM pt.prayer_request FROM pt.prayer_request
WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)""" WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)"
|> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ] [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "req_count")
|> Sql.executeRowAsync (fun row -> row.int "req_count")
/// Count the number of prayer requests for a small group /// Count the number of prayer requests for a small group
let countByGroup (groupId : SmallGroupId) conn = let countByGroup (groupId : SmallGroupId) =
Sql.existingConnection conn Custom.scalar "SELECT COUNT(id) AS req_count FROM pt.prayer_request WHERE small_group_id = @groupId"
|> Sql.query "SELECT COUNT(id) AS req_count FROM pt.prayer_request WHERE small_group_id = @groupId" [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "req_count")
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ]
|> Sql.executeRowAsync (fun row -> row.int "req_count")
/// Delete a prayer request by its ID /// Delete a prayer request by its ID
let deleteById (reqId : PrayerRequestId) conn = backgroundTask { let deleteById (reqId : PrayerRequestId) =
let! _ = Custom.nonQuery "DELETE FROM pt.prayer_request WHERE id = @id" [ "@id", Sql.uuid reqId.Value ]
Sql.existingConnection conn
|> Sql.query "DELETE FROM pt.prayer_request WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid reqId.Value ]
|> Sql.executeNonQueryAsync
return ()
}
/// Get all (or active) requests for a small group as of now or the specified date /// Get all (or active) requests for a small group as of now or the specified date
let forGroup (opts : PrayerRequestOptions) conn = let forGroup (opts : PrayerRequestOptions) =
let theDate = defaultArg opts.ListDate (SmallGroup.localDateNow opts.Clock opts.SmallGroup) let theDate = defaultArg opts.ListDate (SmallGroup.localDateNow opts.Clock opts.SmallGroup)
let where, parameters = let where, parameters =
if opts.ActiveOnly then if opts.ActiveOnly then
@@ -294,33 +251,29 @@ module PrayerRequests =
(theDate.AtStartOfDayInZone(SmallGroup.timeZone opts.SmallGroup) (theDate.AtStartOfDayInZone(SmallGroup.timeZone opts.SmallGroup)
- Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire) - Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire)
.ToInstant ()) .ToInstant ())
""" AND ( updated_date > @asOf " AND ( updated_date > @asOf
OR expiration = @manual OR expiration = @manual
OR request_type = @longTerm OR request_type = @longTerm
OR request_type = @expecting) OR request_type = @expecting)
AND expiration <> @forced""", AND expiration <> @forced",
[ "@asOf", Sql.parameter asOf [ "@asOf", Sql.parameter asOf
"@manual", Sql.string (Expiration.toCode Manual) "@manual", Sql.string (Expiration.toCode Manual)
"@longTerm", Sql.string (PrayerRequestType.toCode LongTermRequest) "@longTerm", Sql.string (PrayerRequestType.toCode LongTermRequest)
"@expecting", Sql.string (PrayerRequestType.toCode Expecting) "@expecting", Sql.string (PrayerRequestType.toCode Expecting)
"@forced", Sql.string (Expiration.toCode Forced) ] "@forced", Sql.string (Expiration.toCode Forced) ]
else "", [] else "", []
Sql.existingConnection conn Custom.list
|> Sql.query $""" $"SELECT *
SELECT *
FROM pt.prayer_request FROM pt.prayer_request
WHERE small_group_id = @groupId {where} WHERE small_group_id = @groupId {where}
ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort} ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort}
{paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}""" {paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}"
|> Sql.parameters (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) mapToPrayerRequest
|> Sql.executeAsync mapToPrayerRequest
/// Save a prayer request /// Save a prayer request
let save (req : PrayerRequest) conn = backgroundTask { let save (req : PrayerRequest) =
let! _ = Custom.nonQuery
Sql.existingConnection conn "INSERT into pt.prayer_request (
|> Sql.query """
INSERT into pt.prayer_request (
id, request_type, user_id, small_group_id, entered_date, updated_date, requestor, request_text, id, request_type, user_id, small_group_id, entered_date, updated_date, requestor, request_text,
notify_chaplain, expiration notify_chaplain, expiration
) VALUES ( ) VALUES (
@@ -332,8 +285,7 @@ module PrayerRequests =
requestor = EXCLUDED.requestor, requestor = EXCLUDED.requestor,
request_text = EXCLUDED.request_text, request_text = EXCLUDED.request_text,
notify_chaplain = EXCLUDED.notify_chaplain, notify_chaplain = EXCLUDED.notify_chaplain,
expiration = EXCLUDED.expiration""" expiration = EXCLUDED.expiration"
|> Sql.parameters
[ "@id", Sql.uuid req.Id.Value [ "@id", Sql.uuid req.Id.Value
"@type", Sql.string (PrayerRequestType.toCode req.RequestType) "@type", Sql.string (PrayerRequestType.toCode req.RequestType)
"@userId", Sql.uuid req.UserId.Value "@userId", Sql.uuid req.UserId.Value
@@ -343,149 +295,123 @@ module PrayerRequests =
"@requestor", Sql.stringOrNone req.Requestor "@requestor", Sql.stringOrNone req.Requestor
"@text", Sql.string req.Text "@text", Sql.string req.Text
"@notifyChaplain", Sql.bool req.NotifyChaplain "@notifyChaplain", Sql.bool req.NotifyChaplain
"@expiration", Sql.string (Expiration.toCode req.Expiration) "@expiration", Sql.string (Expiration.toCode req.Expiration) ]
]
|> Sql.executeNonQueryAsync
return ()
}
/// Search prayer requests for the given term /// Search prayer requests for the given term
let searchForGroup group searchTerm pageNbr conn = let searchForGroup group searchTerm pageNbr =
Sql.existingConnection conn Custom.list
|> Sql.query $""" $"SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND request_text ILIKE @search
SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND request_text ILIKE @search
UNION UNION
SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search
ORDER BY {orderBy group.Preferences.RequestSort} ORDER BY {orderBy group.Preferences.RequestSort}
{paginate pageNbr group.Preferences.PageSize}""" {paginate pageNbr group.Preferences.PageSize}"
|> Sql.parameters [ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ] [ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ] mapToPrayerRequest
|> Sql.executeAsync mapToPrayerRequest
/// Retrieve a prayer request by its ID /// Retrieve a prayer request by its ID
let tryById (reqId : PrayerRequestId) conn = backgroundTask { let tryById (reqId : PrayerRequestId) =
let! req = Custom.single "SELECT * FROM pt.prayer_request WHERE id = @id" [ "@id", Sql.uuid reqId.Value ]
Sql.existingConnection conn mapToPrayerRequest
|> Sql.query "SELECT * FROM pt.prayer_request WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid reqId.Value ]
|> Sql.executeAsync mapToPrayerRequest
return List.tryHead req
}
/// Update the expiration for the given prayer request /// Update the expiration for the given prayer request
let updateExpiration (req : PrayerRequest) withTime conn = backgroundTask { let updateExpiration (req : PrayerRequest) withTime =
let sql, parameters = let sql, parameters =
if withTime then if withTime then
", updated_date = @updated", ", updated_date = @updated",
[ "@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate)) ] [ "@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate)) ]
else "", [] else "", []
let! _ = Custom.nonQuery $"UPDATE pt.prayer_request SET expiration = @expiration{sql} WHERE id = @id"
Sql.existingConnection conn
|> Sql.query $"UPDATE pt.prayer_request SET expiration = @expiration{sql} WHERE id = @id"
|> Sql.parameters
([ "@expiration", Sql.string (Expiration.toCode req.Expiration) ([ "@expiration", Sql.string (Expiration.toCode req.Expiration)
"@id", Sql.uuid req.Id.Value ] "@id", Sql.uuid req.Id.Value ]
|> List.append parameters) |> List.append parameters)
|> Sql.executeNonQueryAsync
return ()
}
/// 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 /// Count the number of small groups for a church
let countByChurch (churchId : ChurchId) conn = let countByChurch (churchId : ChurchId) =
Sql.existingConnection conn Custom.scalar "SELECT COUNT(id) AS group_count FROM pt.small_group WHERE church_id = @churchId"
|> Sql.query "SELECT COUNT(id) AS group_count FROM pt.small_group WHERE church_id = @churchId" [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "group_count")
|> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ]
|> Sql.executeRowAsync (fun row -> row.int "group_count")
/// Delete a small group by its ID /// Delete a small group by its ID
let deleteById (groupId : SmallGroupId) conn = backgroundTask { let deleteById (groupId : SmallGroupId) = backgroundTask {
let idParam = [ [ "@groupId", Sql.uuid groupId.Value ] ] let idParam = [ [ "@groupId", Sql.uuid groupId.Value ] ]
let! _ = let! _ =
Sql.existingConnection conn Configuration.dataSource ()
|> Sql.fromDataSource
|> Sql.executeTransactionAsync |> Sql.executeTransactionAsync
[ "DELETE FROM pt.prayer_request WHERE small_group_id = @groupId", idParam [ "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.user_small_group WHERE small_group_id = @groupId", idParam
"DELETE FROM pt.list_preference 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 ] "DELETE FROM pt.small_group WHERE id = @groupId", idParam ]
return () ()
} }
/// Get information for all small groups /// Get information for all small groups
let infoForAll conn = let infoForAll () =
Sql.existingConnection conn Custom.list
|> Sql.query """ "SELECT sg.id, sg.group_name, c.church_name, lp.time_zone_id, lp.is_public
SELECT sg.id, sg.group_name, c.church_name, lp.time_zone_id, lp.is_public
FROM pt.small_group sg FROM pt.small_group sg
INNER JOIN pt.church c ON c.id = sg.church_id INNER JOIN pt.church c ON c.id = sg.church_id
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id
ORDER BY sg.group_name""" ORDER BY sg.group_name"
|> Sql.executeAsync mapToSmallGroupInfo [] mapToSmallGroupInfo
/// 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 () =
Sql.existingConnection conn Custom.list
|> Sql.query """ "SELECT g.group_name, g.id, c.church_name
SELECT g.group_name, g.id, c.church_name
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 mapToSmallGroupItem [] mapToSmallGroupItem
/// Get a list of small group IDs and descriptions for groups with a group password /// Get a list of small group IDs and descriptions for groups with a group password
let listProtected conn = let listProtected () =
Sql.existingConnection conn Custom.list
|> Sql.query """ "SELECT g.group_name, g.id, c.church_name, lp.is_public
SELECT g.group_name, g.id, c.church_name, lp.is_public
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
INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id
WHERE COALESCE(lp.group_password, '') <> '' WHERE COALESCE(lp.group_password, '') <> ''
ORDER BY c.church_name, g.group_name""" ORDER BY c.church_name, g.group_name"
|> Sql.executeAsync mapToSmallGroupItem [] mapToSmallGroupItem
/// Get a list of small group IDs and descriptions for groups that are public or have a group password /// Get a list of small group IDs and descriptions for groups that are public or have a group password
let listPublicAndProtected conn = let listPublicAndProtected () =
Sql.existingConnection conn Custom.list
|> Sql.query """ "SELECT g.group_name, g.id, c.church_name, lp.time_zone_id, lp.is_public
SELECT g.group_name, g.id, c.church_name, lp.time_zone_id, lp.is_public
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
INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id
WHERE lp.is_public = TRUE WHERE lp.is_public = TRUE
OR COALESCE(lp.group_password, '') <> '' OR COALESCE(lp.group_password, '') <> ''
ORDER BY c.church_name, g.group_name""" ORDER BY c.church_name, g.group_name"
|> Sql.executeAsync mapToSmallGroupInfo [] mapToSmallGroupInfo
/// Log on for a small group (includes list preferences) /// Log on for a small group (includes list preferences)
let logOn (groupId : SmallGroupId) password conn = backgroundTask { let logOn (groupId : SmallGroupId) password =
let! group = Custom.single
Sql.existingConnection conn "SELECT sg.*, lp.*
|> Sql.query """
SELECT sg.*, lp.*
FROM pt.small_group sg FROM pt.small_group sg
INNER JOIN pt.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
AND lp.group_password = @password""" AND lp.group_password = @password"
|> Sql.parameters [ "@id", Sql.uuid groupId.Value; "@password", Sql.string password ] [ "@id", Sql.uuid groupId.Value; "@password", Sql.string password ] mapToSmallGroupWithPreferences
|> Sql.executeAsync mapToSmallGroupWithPreferences
return List.tryHead group
}
/// Save a small group /// Save a small group
let save (group : SmallGroup) isNew conn = backgroundTask { let save (group : SmallGroup) isNew = backgroundTask {
let! _ = let! _ =
Sql.existingConnection conn Configuration.dataSource ()
|> Sql.fromDataSource
|> Sql.executeTransactionAsync [ |> Sql.executeTransactionAsync [
""" INSERT INTO pt.small_group ( "INSERT INTO pt.small_group (
id, church_id, group_name id, church_id, group_name
) VALUES ( ) VALUES (
@id, @churchId, @name @id, @churchId, @name
) ON CONFLICT (id) DO UPDATE ) ON CONFLICT (id) DO UPDATE
SET church_id = EXCLUDED.church_id, SET church_id = EXCLUDED.church_id,
group_name = EXCLUDED.group_name""", group_name = EXCLUDED.group_name",
[ [ "@id", Sql.uuid group.Id.Value [ [ "@id", Sql.uuid group.Id.Value
"@churchId", Sql.uuid group.ChurchId.Value "@churchId", Sql.uuid group.ChurchId.Value
"@name", Sql.string group.Name ] ] "@name", Sql.string group.Name ] ]
@@ -493,15 +419,13 @@ module SmallGroups =
"INSERT INTO pt.list_preference (small_group_id) VALUES (@id)", "INSERT INTO pt.list_preference (small_group_id) VALUES (@id)",
[ [ "@id", Sql.uuid group.Id.Value ] ] [ [ "@id", Sql.uuid group.Id.Value ] ]
] ]
return () ()
} }
/// Save a small group's list preferences /// Save a small group's list preferences
let savePreferences (pref : ListPreferences) conn = backgroundTask { let savePreferences (pref : ListPreferences) =
let! _ = Custom.nonQuery
Sql.existingConnection conn "UPDATE pt.list_preference
|> Sql.query """
UPDATE pt.list_preference
SET days_to_keep_new = @daysToKeepNew, SET days_to_keep_new = @daysToKeepNew,
days_to_expire = @daysToExpire, days_to_expire = @daysToExpire,
long_term_update_weeks = @longTermUpdateWeeks, long_term_update_weeks = @longTermUpdateWeeks,
@@ -519,8 +443,7 @@ module SmallGroups =
time_zone_id = @timeZoneId, time_zone_id = @timeZoneId,
page_size = @pageSize, page_size = @pageSize,
as_of_date_display = @asOfDateDisplay as_of_date_display = @asOfDateDisplay
WHERE small_group_id = @groupId""" WHERE small_group_id = @groupId"
|> Sql.parameters
[ "@groupId", Sql.uuid pref.SmallGroupId.Value [ "@groupId", Sql.uuid pref.SmallGroupId.Value
"@daysToKeepNew", Sql.int pref.DaysToKeepNew "@daysToKeepNew", Sql.int pref.DaysToKeepNew
"@daysToExpire", Sql.int pref.DaysToExpire "@daysToExpire", Sql.int pref.DaysToExpire
@@ -538,103 +461,70 @@ module SmallGroups =
"@isPublic", Sql.bool pref.IsPublic "@isPublic", Sql.bool pref.IsPublic
"@timeZoneId", Sql.string (TimeZoneId.toString pref.TimeZoneId) "@timeZoneId", Sql.string (TimeZoneId.toString pref.TimeZoneId)
"@pageSize", Sql.int pref.PageSize "@pageSize", Sql.int pref.PageSize
"@asOfDateDisplay", Sql.string (AsOfDateDisplay.toCode pref.AsOfDateDisplay) "@asOfDateDisplay", Sql.string (AsOfDateDisplay.toCode pref.AsOfDateDisplay) ]
]
|> Sql.executeNonQueryAsync
return ()
}
/// Get a small group by its ID /// Get a small group by its ID
let tryById (groupId : SmallGroupId) conn = backgroundTask { let tryById (groupId : SmallGroupId) =
let! group = Custom.single "SELECT * FROM pt.small_group WHERE id = @id" [ "@id", Sql.uuid groupId.Value ] mapToSmallGroup
Sql.existingConnection conn
|> 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 /// Get a small group by its ID with its list preferences populated
let tryByIdWithPreferences (groupId : SmallGroupId) conn = backgroundTask { let tryByIdWithPreferences (groupId : SmallGroupId) =
let! group = Custom.single
Sql.existingConnection conn "SELECT sg.*, lp.*
|> Sql.query """
SELECT sg.*, lp.*
FROM pt.small_group sg FROM pt.small_group sg
INNER JOIN pt.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 ] [ "@id", Sql.uuid groupId.Value ] mapToSmallGroupWithPreferences
|> Sql.executeAsync mapToSmallGroupWithPreferences
return List.tryHead group
}
/// Functions to manipulate users /// Functions to manipulate users
module Users = module Users =
/// Retrieve all PrayerTracker users /// Retrieve all PrayerTracker users
let all conn = let all () =
Sql.existingConnection conn Custom.list "SELECT * FROM pt.pt_user ORDER BY last_name, first_name" [] mapToUser
|> Sql.query "SELECT * FROM pt.pt_user ORDER BY last_name, first_name"
|> Sql.executeAsync mapToUser
/// Count the number of users for a church /// Count the number of users for a church
let countByChurch (churchId : ChurchId) conn = let countByChurch (churchId : ChurchId) =
Sql.existingConnection conn Custom.scalar
|> Sql.query """ "SELECT COUNT(u.id) AS user_count
SELECT COUNT(u.id) AS user_count
FROM pt.pt_user u FROM pt.pt_user u
WHERE EXISTS ( WHERE EXISTS (
SELECT 1 SELECT 1
FROM pt.user_small_group usg FROM pt.user_small_group usg
INNER JOIN pt.small_group sg ON sg.id = usg.small_group_id INNER JOIN pt.small_group sg ON sg.id = usg.small_group_id
WHERE usg.user_id = u.id WHERE usg.user_id = u.id
AND sg.church_id = @churchId)""" AND sg.church_id = @churchId)"
|> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ] [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "user_count")
|> Sql.executeRowAsync (fun row -> row.int "user_count")
/// Count the number of users for a small group /// Count the number of users for a small group
let countByGroup (groupId : SmallGroupId) conn = let countByGroup (groupId : SmallGroupId) =
Sql.existingConnection conn Custom.scalar "SELECT COUNT(user_id) AS user_count FROM pt.user_small_group WHERE small_group_id = @groupId"
|> Sql.query "SELECT COUNT(user_id) AS user_count FROM pt.user_small_group WHERE small_group_id = @groupId" [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "user_count")
|> 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) =
let! _ = Custom.nonQuery "DELETE FROM pt.pt_user WHERE id = @id" [ "@id", Sql.uuid userId.Value ]
Sql.existingConnection conn
|> Sql.query "DELETE FROM pt.pt_user WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid userId.Value ]
|> Sql.executeNonQueryAsync
return ()
}
/// Get the IDs of the small groups for which the given user is authorized /// Get the IDs of the small groups for which the given user is authorized
let groupIdsByUserId (userId : UserId) conn = let groupIdsByUserId (userId : UserId) =
Sql.existingConnection conn Custom.list "SELECT small_group_id FROM pt.user_small_group WHERE user_id = @id"
|> Sql.query "SELECT small_group_id FROM pt.user_small_group WHERE user_id = @id" [ "@id", Sql.uuid userId.Value ] (fun row -> SmallGroupId (row.uuid "small_group_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 /// Get a list of users authorized to administer the given small group
let listByGroupId (groupId : SmallGroupId) conn = let listByGroupId (groupId : SmallGroupId) =
Sql.existingConnection conn Custom.list
|> Sql.query """ "SELECT u.*
SELECT u.*
FROM pt.pt_user u FROM pt.pt_user u
INNER JOIN pt.user_small_group usg ON usg.user_id = u.id INNER JOIN pt.user_small_group usg ON usg.user_id = u.id
WHERE usg.small_group_id = @groupId WHERE usg.small_group_id = @groupId
ORDER BY u.last_name, u.first_name""" ORDER BY u.last_name, u.first_name"
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ] [ "@groupId", Sql.uuid groupId.Value ] mapToUser
|> Sql.executeAsync mapToUser
/// Save a user's information /// Save a user's information
let save (user : User) conn = backgroundTask { let save (user : User) =
let! _ = Custom.nonQuery
Sql.existingConnection conn "INSERT INTO pt.pt_user (
|> Sql.query """
INSERT INTO pt.pt_user (
id, first_name, last_name, email, is_admin, password_hash id, first_name, last_name, email, is_admin, password_hash
) VALUES ( ) VALUES (
@id, @firstName, @lastName, @email, @isAdmin, @passwordHash @id, @firstName, @lastName, @email, @isAdmin, @passwordHash
@@ -643,66 +533,40 @@ module Users =
last_name = EXCLUDED.last_name, last_name = EXCLUDED.last_name,
email = EXCLUDED.email, email = EXCLUDED.email,
is_admin = EXCLUDED.is_admin, is_admin = EXCLUDED.is_admin,
password_hash = EXCLUDED.password_hash""" password_hash = EXCLUDED.password_hash"
|> Sql.parameters
[ "@id", Sql.uuid user.Id.Value [ "@id", Sql.uuid user.Id.Value
"@firstName", Sql.string user.FirstName "@firstName", Sql.string user.FirstName
"@lastName", Sql.string user.LastName "@lastName", Sql.string user.LastName
"@email", Sql.string user.Email "@email", Sql.string user.Email
"@isAdmin", Sql.bool user.IsAdmin "@isAdmin", Sql.bool user.IsAdmin
"@passwordHash", Sql.string user.PasswordHash "@passwordHash", Sql.string user.PasswordHash ]
]
|> Sql.executeNonQueryAsync
return ()
}
/// Find a user by its e-mail address and authorized small group /// Find a user by its e-mail address and authorized small group
let tryByEmailAndGroup email (groupId : SmallGroupId) conn = backgroundTask { let tryByEmailAndGroup email (groupId : SmallGroupId) =
let! user = Custom.single
Sql.existingConnection conn "SELECT u.*
|> Sql.query """
SELECT u.*
FROM pt.pt_user u FROM pt.pt_user u
INNER JOIN pt.user_small_group usg ON usg.user_id = u.id AND usg.small_group_id = @groupId INNER JOIN pt.user_small_group usg ON usg.user_id = u.id AND usg.small_group_id = @groupId
WHERE u.email = @email""" WHERE u.email = @email"
|> Sql.parameters [ "@email", Sql.string email; "@groupId", Sql.uuid groupId.Value ] [ "@email", Sql.string email; "@groupId", Sql.uuid groupId.Value ] mapToUser
|> Sql.executeAsync mapToUser
return List.tryHead user
}
/// Find a user by their database ID /// Find a user by their database ID
let tryById (userId : UserId) conn = backgroundTask { let tryById (userId : UserId) =
let! user = Custom.single "SELECT * FROM pt.pt_user WHERE id = @id" [ "@id", Sql.uuid userId.Value ] mapToUser
Sql.existingConnection conn
|> Sql.query "SELECT * FROM pt.pt_user WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid userId.Value ]
|> Sql.executeAsync mapToUser
return List.tryHead user
}
/// Update a user's last seen date/time /// Update a user's last seen date/time
let updateLastSeen (userId : UserId) (now : Instant) conn = backgroundTask { let updateLastSeen (userId : UserId) (now : Instant) =
let! _ = Custom.nonQuery "UPDATE pt.pt_user SET last_seen = @now WHERE id = @id"
Sql.existingConnection conn [ "@id", Sql.uuid userId.Value; "@now", Sql.parameter (NpgsqlParameter ("@now", now)) ]
|> Sql.query "UPDATE pt.pt_user SET last_seen = @now WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid userId.Value; "@now", Sql.parameter (NpgsqlParameter ("@now", now)) ]
|> Sql.executeNonQueryAsync
return ()
}
/// Update a user's password hash /// Update a user's password hash
let updatePassword (user : User) conn = backgroundTask { let updatePassword (user : User) =
let! _ = Custom.nonQuery "UPDATE pt.pt_user SET password_hash = @passwordHash WHERE id = @id"
Sql.existingConnection conn [ "@id", Sql.uuid user.Id.Value; "@passwordHash", Sql.string user.PasswordHash ]
|> Sql.query "UPDATE pt.pt_user SET password_hash = @passwordHash WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid user.Id.Value; "@passwordHash", Sql.string user.PasswordHash ]
|> Sql.executeNonQueryAsync
return ()
}
/// Update a user's authorized small groups /// Update a user's authorized small groups
let updateSmallGroups (userId : UserId) groupIds conn = backgroundTask { let updateSmallGroups (userId : UserId) groupIds = backgroundTask {
let! existingGroupIds = groupIdsByUserId userId conn let! existingGroupIds = groupIdsByUserId userId
let toAdd = let toAdd =
groupIds |> List.filter (fun it -> existingGroupIds |> List.exists (fun grpId -> grpId = it) |> not) groupIds |> List.filter (fun it -> existingGroupIds |> List.exists (fun grpId -> grpId = it) |> not)
let toDelete = let toDelete =
@@ -718,7 +582,8 @@ module Users =
} }
if not (Seq.isEmpty queries) then if not (Seq.isEmpty queries) then
let! _ = let! _ =
Sql.existingConnection conn Configuration.dataSource ()
|> Sql.fromDataSource
|> Sql.executeTransactionAsync (List.ofSeq queries) |> Sql.executeTransactionAsync (List.ofSeq queries)
() ()
} }

View File

@@ -47,33 +47,30 @@ module private CacheHelpers =
p.ParameterName, Sql.parameter p p.ParameterName, Sql.parameter p
open BitBadger.Npgsql.FSharp.Documents
/// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog /// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog
type DistributedCache (connStr : string) = type DistributedCache () =
// ~~~ INITIALIZATION ~~~ // ~~~ INITIALIZATION ~~~
do do
task { task {
let! exists = let! exists =
Sql.connect connStr Custom.scalar
|> Sql.query $" $"SELECT EXISTS
SELECT EXISTS
(SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session') (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session')
AS does_exist" AS does_exist"
|> Sql.executeRowAsync (fun row -> row.bool "does_exist") [] (fun row -> row.bool "does_exist")
if not exists then if not exists then
let! _ = do! Custom.nonQuery
Sql.connect connStr
|> Sql.query
"CREATE TABLE session ( "CREATE TABLE session (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
payload BYTEA NOT NULL, payload BYTEA NOT NULL,
expire_at TIMESTAMPTZ NOT NULL, expire_at TIMESTAMPTZ NOT NULL,
sliding_expiration INTERVAL, sliding_expiration INTERVAL,
absolute_expiration TIMESTAMPTZ); absolute_expiration TIMESTAMPTZ);
CREATE INDEX idx_session_expiration ON session (expire_at)" CREATE INDEX idx_session_expiration ON session (expire_at)" []
|> Sql.executeNonQueryAsync
()
} |> sync } |> sync
// ~~~ SUPPORT FUNCTIONS ~~~ // ~~~ SUPPORT FUNCTIONS ~~~
@@ -82,16 +79,14 @@ type DistributedCache (connStr : string) =
let getEntry key = backgroundTask { let getEntry key = backgroundTask {
let idParam = "@id", Sql.string key let idParam = "@id", Sql.string key
let! tryEntry = let! tryEntry =
Sql.connect connStr Custom.single "SELECT * FROM session WHERE id = @id" [ idParam ]
|> Sql.query "SELECT * FROM session WHERE id = @id" (fun row ->
|> Sql.parameters [ idParam ]
|> Sql.executeAsync (fun row ->
{ Id = row.string "id" { Id = row.string "id"
Payload = row.bytea "payload" Payload = row.bytea "payload"
ExpireAt = row.fieldValue<Instant> "expire_at" ExpireAt = row.fieldValue<Instant> "expire_at"
SlidingExpiration = row.fieldValueOrNone<Duration> "sliding_expiration" SlidingExpiration = row.fieldValueOrNone<Duration> "sliding_expiration"
AbsoluteExpiration = row.fieldValueOrNone<Instant> "absolute_expiration" }) AbsoluteExpiration = row.fieldValueOrNone<Instant> "absolute_expiration" })
match List.tryHead tryEntry with match tryEntry with
| Some entry -> | Some entry ->
let now = getNow () let now = getNow ()
let slideExp = defaultArg entry.SlidingExpiration Duration.MinValue let slideExp = defaultArg entry.SlidingExpiration Duration.MinValue
@@ -103,12 +98,8 @@ type DistributedCache (connStr : string) =
true, { entry with ExpireAt = absExp } true, { entry with ExpireAt = absExp }
else true, { entry with ExpireAt = now.Plus slideExp } else true, { entry with ExpireAt = now.Plus slideExp }
if needsRefresh then if needsRefresh then
let! _ = do! Custom.nonQuery "UPDATE session SET expire_at = @expireAt WHERE id = @id"
Sql.connect connStr [ expireParam item.ExpireAt; idParam ]
|> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|> Sql.parameters [ expireParam item.ExpireAt; idParam ]
|> Sql.executeNonQueryAsync
()
return if item.ExpireAt > now then Some entry else None return if item.ExpireAt > now then Some entry else None
| None -> return None | None -> return None
} }
@@ -120,26 +111,16 @@ type DistributedCache (connStr : string) =
let purge () = backgroundTask { let purge () = backgroundTask {
let now = getNow () let now = getNow ()
if lastPurge.Plus (Duration.FromMinutes 30L) < now then if lastPurge.Plus (Duration.FromMinutes 30L) < now then
let! _ = do! Custom.nonQuery "DELETE FROM session WHERE expire_at < @expireAt" [ expireParam now ]
Sql.connect connStr
|> Sql.query "DELETE FROM session WHERE expire_at < @expireAt"
|> Sql.parameters [ expireParam now ]
|> Sql.executeNonQueryAsync
lastPurge <- now lastPurge <- now
} }
/// Remove a cache entry /// Remove a cache entry
let removeEntry key = backgroundTask { let removeEntry key =
let! _ = Custom.nonQuery "DELETE FROM session WHERE id = @id" [ "@id", Sql.string key ]
Sql.connect connStr
|> Sql.query "DELETE FROM session WHERE id = @id"
|> Sql.parameters [ "@id", Sql.string key ]
|> Sql.executeNonQueryAsync
()
}
/// Save an entry /// Save an entry
let saveEntry (opts : DistributedCacheEntryOptions) key payload = backgroundTask { let saveEntry (opts : DistributedCacheEntryOptions) key payload =
let now = getNow () let now = getNow ()
let expireAt, slideExp, absExp = let expireAt, slideExp, absExp =
if opts.SlidingExpiration.HasValue then if opts.SlidingExpiration.HasValue then
@@ -155,9 +136,7 @@ type DistributedCache (connStr : string) =
// Default to 2 hour sliding expiration // Default to 2 hour sliding expiration
let slide = Duration.FromHours 2 let slide = Duration.FromHours 2
now.Plus slide, Some slide, None now.Plus slide, Some slide, None
let! _ = Custom.nonQuery
Sql.connect connStr
|> Sql.query
"INSERT INTO session ( "INSERT INTO session (
id, payload, expire_at, sliding_expiration, absolute_expiration id, payload, expire_at, sliding_expiration, absolute_expiration
) VALUES ( ) VALUES (
@@ -167,15 +146,11 @@ type DistributedCache (connStr : string) =
expire_at = EXCLUDED.expire_at, expire_at = EXCLUDED.expire_at,
sliding_expiration = EXCLUDED.sliding_expiration, sliding_expiration = EXCLUDED.sliding_expiration,
absolute_expiration = EXCLUDED.absolute_expiration" absolute_expiration = EXCLUDED.absolute_expiration"
|> Sql.parameters
[ "@id", Sql.string key [ "@id", Sql.string key
"@payload", Sql.bytea payload "@payload", Sql.bytea payload
expireParam expireAt expireParam expireAt
optParam "slideExp" slideExp optParam "slideExp" slideExp
optParam "absExp" absExp ] optParam "absExp" absExp ]
|> Sql.executeNonQueryAsync
()
}
// ~~~ IMPLEMENTATION FUNCTIONS ~~~ // ~~~ IMPLEMENTATION FUNCTIONS ~~~

View File

@@ -1,9 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Entities.fs" /> <Compile Include="Entities.fs" />
<Compile Include="Access.fs" /> <Compile Include="Access.fs" />
@@ -11,11 +7,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BitBadger.Npgsql.FSharp.Documents" Version="1.0.0-beta3" />
<PackageReference Include="Giraffe" Version="6.0.0" /> <PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="NodaTime" Version="3.1.2" /> <PackageReference Include="NodaTime" Version="3.1.9" />
<PackageReference Update="FSharp.Core" Version="6.0.5" /> <PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
<PackageReference Include="Npgsql.FSharp" Version="5.3.0" /> <PackageReference Include="Npgsql.NodaTime" Version="7.0.4" />
<PackageReference Include="Npgsql.NodaTime" Version="6.0.6" /> <PackageReference Update="FSharp.Core" Version="7.0.300" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,7 +2,6 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -16,8 +15,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="9.0.4" /> <PackageReference Include="Expecto" Version="9.0.4" />
<PackageReference Include="NodaTime.Testing" Version="3.1.2" /> <PackageReference Include="NodaTime.Testing" Version="3.1.9" />
<PackageReference Update="FSharp.Core" Version="6.0.5" /> <PackageReference Update="FSharp.Core" Version="7.0.300" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -604,7 +604,7 @@ let requestListTests =
} }
let html = htmlList.AsHtml _s let html = htmlList.AsHtml _s
let expected = let expected =
htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("d", null) htmlList.Requests[0].UpdatedDate.InZone(SmallGroup.timeZone reqList.SmallGroup).Date.ToString ("d", null)
|> sprintf """<strong>Zeb</strong> &ndash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>""" |> sprintf """<strong>Zeb</strong> &ndash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>"""
// spot check; if one request has it, they all should // spot check; if one request has it, they all should
Expect.stringContains html expected "Expected short as-of date not found" Expect.stringContains html expected "Expected short as-of date not found"
@@ -619,7 +619,7 @@ let requestListTests =
} }
let html = htmlList.AsHtml _s let html = htmlList.AsHtml _s
let expected = let expected =
htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("D", null) htmlList.Requests[0].UpdatedDate.InZone(SmallGroup.timeZone reqList.SmallGroup).Date.ToString ("D", null)
|> sprintf """<strong>Zeb</strong> &ndash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>""" |> sprintf """<strong>Zeb</strong> &ndash; zyx<i style="font-size:9.60pt">&nbsp; (as of %s)</i>"""
// spot check; if one request has it, they all should // spot check; if one request has it, they all should
Expect.stringContains html expected "Expected long as-of date not found" Expect.stringContains html expected "Expected long as-of date not found"
@@ -650,7 +650,7 @@ let requestListTests =
} }
let text = textList.AsText _s let text = textList.AsText _s
let expected = let expected =
textList.Requests[0].UpdatedDate.InUtc().Date.ToString ("d", null) textList.Requests[0].UpdatedDate.InZone(SmallGroup.timeZone reqList.SmallGroup).Date.ToString ("d", null)
|> sprintf " + Zeb - zyx (as of %s)" |> sprintf " + Zeb - zyx (as of %s)"
// spot check; if one request has it, they all should // spot check; if one request has it, they all should
Expect.stringContains text expected "Expected short as-of date not found" Expect.stringContains text expected "Expected short as-of date not found"
@@ -665,7 +665,7 @@ let requestListTests =
} }
let text = textList.AsText _s let text = textList.AsText _s
let expected = let expected =
textList.Requests[0].UpdatedDate.InUtc().Date.ToString ("D", null) textList.Requests[0].UpdatedDate.InZone(SmallGroup.timeZone reqList.SmallGroup).Date.ToString ("D", null)
|> sprintf " + Zeb - zyx (as of %s)" |> sprintf " + Zeb - zyx (as of %s)"
// spot check; if one request has it, they all should // spot check; if one request has it, they all should
Expect.stringContains text expected "Expected long as-of date not found" Expect.stringContains text expected "Expected long as-of date not found"

View File

@@ -19,4 +19,4 @@ let localizer = lazy (stringLocFactory.Create ("Common", resAsmName))
/// Get a view localizer /// Get a view localizer
let forView (view : string) = let forView (view : string) =
htmlLocFactory.Create ($"""Views.{view.Replace ('/', '.')}""", resAsmName) htmlLocFactory.Create ($"Views.{view.Replace ('/', '.')}", resAsmName)

View File

@@ -304,14 +304,14 @@ let private contentSection viewInfo pgTitle (content : XmlNode) = [
| Some onLoad -> | Some onLoad ->
let doCall = if onLoad.EndsWith ")" then "" else "()" let doCall = if onLoad.EndsWith ")" then "" else "()"
script [] [ script [] [
rawText $""" rawText $"
window.doOnLoad = () => {{ window.doOnLoad = () => {{
if (window.PT) {{ if (window.PT) {{
{onLoad}{doCall} {onLoad}{doCall}
delete window.doOnLoad delete window.doOnLoad
}} else {{ setTimeout(window.doOnLoad, 500) }} }} else {{ setTimeout(window.doOnLoad, 500) }}
}} }}
window.doOnLoad()""" window.doOnLoad()"
] ]
| None -> () | None -> ()
] ]

View File

@@ -1,9 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Utils.fs" /> <Compile Include="Utils.fs" />
<Compile Include="ViewModels.fs" /> <Compile Include="ViewModels.fs" />
@@ -18,16 +14,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.0" /> <PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.9.2" />
<PackageReference Include="MailKit" Version="3.3.0" /> <PackageReference Include="MailKit" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Update="FSharp.Core" Version="6.0.5" /> <PackageReference Update="FSharp.Core" Version="7.0.300" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -2,9 +2,6 @@
module PrayerTracker.Utils module PrayerTracker.Utils
open System open System
open System.Security.Cryptography
open System.Text
open Giraffe open Giraffe
/// Parse a short-GUID-based ID from a string /// Parse a short-GUID-based ID from a string

View File

@@ -35,6 +35,7 @@ module Configure =
(ctx.Configuration.GetSection >> opts.Configure >> ignore) "Kestrel" (ctx.Configuration.GetSection >> opts.Configure >> ignore) "Kestrel"
open System.Globalization open System.Globalization
open BitBadger.Npgsql.FSharp.Documents
open Microsoft.AspNetCore.Authentication.Cookies open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.AspNetCore.Localization open Microsoft.AspNetCore.Localization
open Microsoft.Extensions.Caching.Distributed open Microsoft.Extensions.Caching.Distributed
@@ -63,21 +64,18 @@ module Configure =
opts.SlidingExpiration <- true opts.SlidingExpiration <- true
opts.AccessDeniedPath <- "/error/403") opts.AccessDeniedPath <- "/error/403")
let _ = svc.AddAuthorization () let _ = svc.AddAuthorization ()
let _ =
svc.AddSingleton<IDistributedCache> (fun sp -> let cfg = svc.BuildServiceProvider().GetService<IConfiguration> ()
let cfg = sp.GetService<IConfiguration> () let dsb = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PrayerTracker")
DistributedCache (cfg.GetConnectionString "PrayerTracker") :> IDistributedCache) let _ = dsb.UseNodaTime()
Configuration.useDataSource (dsb.Build ())
let _ = svc.AddSingleton<IDistributedCache, DistributedCache> ()
let _ = svc.AddSession () let _ = svc.AddSession ()
let _ = svc.AddAntiforgery () let _ = svc.AddAntiforgery ()
let _ = svc.AddRouting () let _ = svc.AddRouting ()
let _ = svc.AddSingleton<IClock> SystemClock.Instance let _ = svc.AddSingleton<IClock> SystemClock.Instance
let _ =
svc.AddScoped<NpgsqlConnection>(fun sp ->
let cfg = sp.GetService<IConfiguration> ()
let conn = new NpgsqlConnection (cfg.GetConnectionString "PrayerTracker")
conn.OpenAsync () |> Async.AwaitTask |> Async.RunSynchronously
conn)
let _ = NpgsqlConnection.GlobalTypeMapper.UseNodaTime ()
() ()
open Giraffe open Giraffe
@@ -214,47 +212,6 @@ module Configure =
/// The web application /// The web application
module App = module App =
open System.Text
open Microsoft.Extensions.DependencyInjection
open Npgsql
open Npgsql.FSharp
open PrayerTracker.Entities
let migratePasswords (app : IWebHost) =
task {
let config = app.Services.GetService<IConfiguration> ()
use conn = new NpgsqlConnection (config.GetConnectionString "PrayerTracker")
do! conn.OpenAsync ()
let! v1Users =
Sql.existingConnection conn
|> Sql.query "SELECT id, password_hash FROM pt.pt_user WHERE salt IS NULL"
|> Sql.executeAsync (fun row -> UserId (row.uuid "id"), row.string "password_hash")
for userId, oldHash in v1Users do
let pw = Convert.ToBase64String [| 254uy; yield! (Encoding.UTF8.GetBytes oldHash) |]
let! _ =
Sql.existingConnection conn
|> Sql.query "UPDATE pt.pt_user SET password_hash = @hash WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid userId.Value; "@hash", Sql.string pw ]
|> Sql.executeNonQueryAsync
()
printfn $"Updated {v1Users.Length} users with version 1 password"
let! v2Users =
Sql.existingConnection conn
|> Sql.query "SELECT id, password_hash, salt FROM pt.pt_user WHERE salt IS NOT NULL"
|> Sql.executeAsync (fun row -> UserId (row.uuid "id"), row.string "password_hash", row.uuid "salt")
for userId, oldHash, salt in v2Users do
let pw =
Convert.ToBase64String
[| 255uy; yield! (salt.ToByteArray ()); yield! (Encoding.UTF8.GetBytes oldHash) |]
let! _ =
Sql.existingConnection conn
|> Sql.query "UPDATE pt.pt_user SET password_hash = @hash WHERE id = @id"
|> Sql.parameters [ "@id", Sql.uuid userId.Value; "@hash", Sql.string pw ]
|> Sql.executeNonQueryAsync
()
printfn $"Updated {v2Users.Length} users with version 2 password"
} |> Async.AwaitTask |> Async.RunSynchronously
open System.IO open System.IO
[<EntryPoint>] [<EntryPoint>]
@@ -270,8 +227,5 @@ module App =
.ConfigureLogging(Configure.logging) .ConfigureLogging(Configure.logging)
.Configure(System.Action<IApplicationBuilder> Configure.app) .Configure(System.Action<IApplicationBuilder> Configure.app)
.Build() .Build()
if args.Length > 0 then if args.Length > 0 then printfn $"Unrecognized option {args[0]}" else app.Run ()
if args[0] = "migrate-passwords" then migratePasswords app
else printfn $"Unrecognized option {args[0]}"
else app.Run ()
0 0

View File

@@ -8,21 +8,20 @@ open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
/// Find statistics for the given church /// Find statistics for the given church
let private findStats churchId conn = task { let private findStats churchId = task {
let! groups = SmallGroups.countByChurch churchId conn let! groups = SmallGroups.countByChurch churchId
let! requests = PrayerRequests.countByChurch churchId conn let! requests = PrayerRequests.countByChurch churchId
let! users = Users.countByChurch churchId conn let! users = Users.countByChurch churchId
return shortGuid churchId.Value, { SmallGroups = groups; PrayerRequests = requests; Users = users } 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
let conn = ctx.Conn match! Churches.tryById churchId with
match! Churches.tryById churchId conn with
| Some church -> | Some church ->
let! _, stats = findStats churchId conn let! _, stats = findStats churchId
do! Churches.deleteById churchId conn do! Churches.deleteById churchId
addInfo ctx addInfo ctx
ctx.Strings["The church “{0}” and its {1} small group(s) (with {2} prayer request(s)) were deleted successfully; revoked access from {3} user(s)", ctx.Strings["The church “{0}” and its {1} small group(s) (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]
@@ -32,7 +31,7 @@ let delete chId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun
open System open System
/// GET /church/[church-id]/edit // GET /church/[church-id]/edit
let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
if churchId = Guid.Empty then if churchId = Guid.Empty then
return! return!
@@ -40,7 +39,7 @@ let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> ta
|> Views.Church.edit EditChurch.empty ctx |> Views.Church.edit EditChurch.empty ctx
|> renderHtml next ctx |> renderHtml next ctx
else else
match! Churches.tryById (ChurchId churchId) ctx.Conn with match! Churches.tryById (ChurchId churchId) with
| Some church -> | Some church ->
return! return!
viewInfo ctx viewInfo ctx
@@ -49,27 +48,26 @@ let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> ta
| None -> return! fourOhFour ctx | None -> return! fourOhFour ctx
} }
/// GET /churches // GET /churches
let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
let conn = ctx.Conn let! churches = Churches.all ()
let! churches = Churches.all conn let stats = churches |> List.map (fun c -> findStats c.Id |> Async.AwaitTask |> Async.RunSynchronously)
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
} }
/// 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
| Ok model -> | Ok model ->
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 Churches.tryById (idFromShort ChurchId model.ChurchId) ctx.Conn else Churches.tryById (idFromShort ChurchId model.ChurchId)
match church with match church with
| Some ch -> | Some ch ->
do! Churches.save (model.PopulateChurch ch) ctx.Conn do! Churches.save (model.PopulateChurch ch)
let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower () let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower ()
addInfo ctx ctx.Strings["Successfully {0} church “{1}”", act, model.Name] addInfo ctx ctx.Strings["Successfully {0} church “{1}”", act, model.Name]
return! redirectTo false "/churches" next ctx return! redirectTo false "/churches" next ctx

View File

@@ -2,10 +2,8 @@
module PrayerTracker.Email module PrayerTracker.Email
open MailKit.Net.Smtp open MailKit.Net.Smtp
open MailKit.Security
open Microsoft.Extensions.Localization open Microsoft.Extensions.Localization
open MimeKit open MimeKit
open MimeKit.Text
open PrayerTracker.Entities open PrayerTracker.Entities
/// Parameters required to send an e-mail /// Parameters required to send an e-mail
@@ -35,11 +33,13 @@ type EmailOptions =
/// The e-mail address from which e-mail is sent /// The e-mail address from which e-mail is sent
let private fromAddress = "prayer@bitbadger.solutions" let private fromAddress = "prayer@bitbadger.solutions"
open MailKit.Security
open Microsoft.Extensions.Configuration
/// Get an SMTP client connection /// Get an SMTP client connection
// FIXME: make host configurable let getConnection (cfg : IConfiguration) = task {
let getConnection () = task {
let client = new SmtpClient () let client = new SmtpClient ()
do! client.ConnectAsync ("127.0.0.1", 25, SecureSocketOptions.None) do! client.ConnectAsync (cfg.GetConnectionString "SmtpServer", 25, SecureSocketOptions.None)
return client return client
} }
@@ -51,6 +51,8 @@ let createMessage opts =
msg.ReplyTo.Add (MailboxAddress (opts.Group.Preferences.EmailFromName, opts.Group.Preferences.EmailFromAddress)) msg.ReplyTo.Add (MailboxAddress (opts.Group.Preferences.EmailFromName, opts.Group.Preferences.EmailFromAddress))
msg msg
open MimeKit.Text
/// Create an HTML-format e-mail message /// Create an HTML-format e-mail message
let createHtmlMessage opts = let createHtmlMessage opts =
let bodyText = let bodyText =

View File

@@ -76,14 +76,11 @@ type HttpContext with
/// The system clock (via DI) /// The system clock (via DI)
member this.Clock = this.GetService<IClock> () member this.Clock = this.GetService<IClock> ()
/// The PostgreSQL connection (configured via DI)
member this.Conn = this.GetService<NpgsqlConnection> ()
/// The current instant /// The current instant
member this.Now = this.Clock.GetCurrentInstant () member this.Now = this.Clock.GetCurrentInstant ()
/// The common string localizer /// The common string localizer
member this.Strings = Views.I18N.localizer.Force () member _.Strings = Views.I18N.localizer.Force ()
/// The currently logged on small group (sets the value in the session if it is missing) /// The currently logged on small group (sets the value in the session if it is missing)
member this.CurrentGroup () = task { member this.CurrentGroup () = task {
@@ -92,7 +89,7 @@ type HttpContext with
| None -> | None ->
match this.User.SmallGroupId with match this.User.SmallGroupId with
| Some groupId -> | Some groupId ->
match! SmallGroups.tryByIdWithPreferences groupId this.Conn with match! SmallGroups.tryByIdWithPreferences groupId with
| Some group -> | Some group ->
this.Session.CurrentGroup <- Some group this.Session.CurrentGroup <- Some group
return Some group return Some group
@@ -107,10 +104,10 @@ type HttpContext with
| None -> | None ->
match this.User.UserId with match this.User.UserId with
| Some userId -> | Some userId ->
match! Users.tryById userId this.Conn with match! Users.tryById userId with
| Some user -> | Some user ->
// Set last seen for user // Set last seen for user
do! Users.updateLastSeen userId this.Now this.Conn do! Users.updateLastSeen userId this.Now
this.Session.CurrentUser <- Some user this.Session.CurrentUser <- Some user
return Some user return Some user
| None -> return None | None -> return None

View File

@@ -7,19 +7,19 @@ open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Localization open Microsoft.AspNetCore.Localization
open PrayerTracker open PrayerTracker
/// GET /error/[error-code] // GET /error/[error-code]
let error code : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> let error code : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
viewInfo ctx viewInfo ctx
|> Views.Home.error code |> Views.Home.error code
|> renderHtml next ctx |> renderHtml next ctx
/// GET / // GET /
let homePage : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> let homePage : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
viewInfo ctx viewInfo ctx
|> Views.Home.index |> Views.Home.index
|> renderHtml next ctx |> renderHtml next ctx
/// GET /language/[culture] // GET /language/[culture]
let language culture : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> let language culture : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
try try
match culture with match culture with
@@ -42,13 +42,13 @@ let language culture : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fu
let url = match string ctx.Request.Headers["Referer"] with null | "" -> "/" | r -> r let url = match string ctx.Request.Headers["Referer"] with null | "" -> "/" | r -> r
redirectTo false url next ctx redirectTo false url next ctx
/// GET /legal/privacy-policy // GET /legal/privacy-policy
let privacyPolicy : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> let privacyPolicy : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
viewInfo ctx viewInfo ctx
|> Views.Home.privacyPolicy |> Views.Home.privacyPolicy
|> renderHtml next ctx |> renderHtml next ctx
/// GET /legal/terms-of-service // GET /legal/terms-of-service
let tos : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> let tos : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
viewInfo ctx viewInfo ctx
|> Views.Home.termsOfService |> Views.Home.termsOfService
@@ -57,7 +57,7 @@ let tos : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
open Microsoft.AspNetCore.Authentication open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies open Microsoft.AspNetCore.Authentication.Cookies
/// GET /log-off // GET /log-off
let logOff : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { let logOff : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task {
ctx.Session.Clear () ctx.Session.Clear ()
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
@@ -65,7 +65,7 @@ let logOff : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx
return! redirectTo false "/" next ctx return! redirectTo false "/" next ctx
} }
/// GET /unauthorized // GET /unauthorized
let unauthorized : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> let unauthorized : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
viewInfo ctx viewInfo ctx
|> Views.Home.unauthorized |> Views.Home.unauthorized

View File

@@ -9,7 +9,7 @@ open PrayerTracker.ViewModels
/// Retrieve a prayer request, and ensure that it belongs to the current class /// Retrieve a prayer request, and ensure that it belongs to the current class
let private findRequest (ctx : HttpContext) reqId = task { let private findRequest (ctx : HttpContext) reqId = task {
match! PrayerRequests.tryById reqId ctx.Conn with match! PrayerRequests.tryById reqId with
| Some req when req.SmallGroupId = ctx.Session.CurrentGroup.Value.Id -> return Ok req | Some req when req.SmallGroupId = ctx.Session.CurrentGroup.Value.Id -> return Ok req
| Some _ -> | Some _ ->
addError ctx ctx.Strings["The prayer request you tried to access is not assigned to your group"] addError ctx ctx.Strings["The prayer request you tried to access is not assigned to your group"]
@@ -28,7 +28,7 @@ let private generateRequestList (ctx : HttpContext) date = task {
ListDate = Some listDate ListDate = Some listDate
ActiveOnly = true ActiveOnly = true
PageNumber = 0 PageNumber = 0
} ctx.Conn }
return return
{ Requests = reqs { Requests = reqs
Date = listDate Date = listDate
@@ -49,7 +49,7 @@ let private parseListDate (date : string option) =
open System open System
/// GET /prayer-request/[request-id]/edit // GET /prayer-request/[request-id]/edit
let edit reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let edit reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let now = SmallGroup.localDateNow ctx.Clock group let now = SmallGroup.localDateNow ctx.Clock group
@@ -79,14 +79,16 @@ let edit reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
| Result.Error e -> return! e | Result.Error e -> return! e
} }
/// GET /prayer-requests/email/[date] open Microsoft.Extensions.Configuration
// GET /prayer-requests/email/[date]
let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let s = ctx.Strings let s = ctx.Strings
let listDate = parseListDate (Some date) let listDate = parseListDate (Some date)
let! list = generateRequestList ctx listDate let! list = generateRequestList ctx listDate
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let! recipients = Members.forGroup group.Id ctx.Conn let! recipients = Members.forGroup group.Id
use! client = Email.getConnection () use! client = Email.getConnection (ctx.GetService<IConfiguration> ())
do! Email.sendEmails do! Email.sendEmails
{ Client = client { Client = client
Recipients = recipients Recipients = recipients
@@ -102,31 +104,31 @@ let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|> renderHtml next ctx |> renderHtml next ctx
} }
/// POST /prayer-request/[request-id]/delete // POST /prayer-request/[request-id]/delete
let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
let requestId = PrayerRequestId reqId let requestId = PrayerRequestId reqId
match! findRequest ctx requestId with match! findRequest ctx requestId with
| Ok req -> | Ok req ->
do! PrayerRequests.deleteById req.Id ctx.Conn do! PrayerRequests.deleteById req.Id
addInfo ctx ctx.Strings["The prayer request was deleted successfully"] addInfo ctx ctx.Strings["The prayer request was deleted successfully"]
return! redirectTo false "/prayer-requests" next ctx return! redirectTo false "/prayer-requests" next ctx
| Result.Error e -> return! e | Result.Error e -> return! e
} }
/// GET /prayer-request/[request-id]/expire // GET /prayer-request/[request-id]/expire
let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let requestId = PrayerRequestId reqId let requestId = PrayerRequestId reqId
match! findRequest ctx requestId with match! findRequest ctx requestId with
| Ok req -> | Ok req ->
do! PrayerRequests.updateExpiration { req with Expiration = Forced } false ctx.Conn do! PrayerRequests.updateExpiration { req with Expiration = Forced } false
addInfo ctx ctx.Strings["Successfully {0} prayer request", ctx.Strings["Expired"].Value.ToLower ()] addInfo ctx ctx.Strings["Successfully {0} prayer request", ctx.Strings["Expired"].Value.ToLower ()]
return! redirectTo false "/prayer-requests" next ctx return! redirectTo false "/prayer-requests" next ctx
| Result.Error e -> return! e | Result.Error e -> return! e
} }
/// GET /prayer-requests/[group-id]/list // GET /prayer-requests/[group-id]/list
let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task {
match! SmallGroups.tryByIdWithPreferences (SmallGroupId groupId) ctx.Conn with match! SmallGroups.tryByIdWithPreferences (SmallGroupId groupId) with
| Some group when group.Preferences.IsPublic -> | Some group when group.Preferences.IsPublic ->
let! reqs = let! reqs =
PrayerRequests.forGroup PrayerRequests.forGroup
@@ -135,7 +137,7 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne
ListDate = None ListDate = None
ActiveOnly = true ActiveOnly = true
PageNumber = 0 PageNumber = 0
} ctx.Conn }
return! return!
viewInfo ctx viewInfo ctx
|> Views.PrayerRequest.list |> Views.PrayerRequest.list
@@ -153,18 +155,18 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne
| None -> return! fourOhFour ctx | None -> return! fourOhFour ctx
} }
/// GET /prayer-requests/lists // GET /prayer-requests/lists
let lists : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { let lists : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task {
let! groups = SmallGroups.listPublicAndProtected ctx.Conn let! groups = SmallGroups.listPublicAndProtected ()
return! return!
viewInfo ctx viewInfo ctx
|> Views.PrayerRequest.lists groups |> Views.PrayerRequest.lists groups
|> renderHtml next ctx |> renderHtml next ctx
} }
/// GET /prayer-requests[/inactive?] // GET /prayer-requests[/inactive?]
/// - OR - // - OR -
/// GET /prayer-requests?search=[search-query] // GET /prayer-requests?search=[search-query]
let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let pageNbr = let pageNbr =
@@ -174,7 +176,7 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
let! model = backgroundTask { let! model = backgroundTask {
match ctx.GetQueryStringValue "search" with match ctx.GetQueryStringValue "search" with
| Ok search -> | Ok search ->
let! reqs = PrayerRequests.searchForGroup group search pageNbr ctx.Conn let! reqs = PrayerRequests.searchForGroup group search pageNbr
return return
{ MaintainRequests.empty with { MaintainRequests.empty with
Requests = reqs Requests = reqs
@@ -189,7 +191,7 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
ListDate = None ListDate = None
ActiveOnly = onlyActive ActiveOnly = onlyActive
PageNumber = pageNbr PageNumber = pageNbr
} ctx.Conn }
return return
{ MaintainRequests.empty with { MaintainRequests.empty with
Requests = reqs Requests = reqs
@@ -203,7 +205,7 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
/// GET /prayer-request/print/[date] // GET /prayer-request/print/[date]
let print date : HttpHandler = requireAccess [ User; Group ] >=> fun next ctx -> task { let print date : HttpHandler = requireAccess [ User; Group ] >=> fun next ctx -> task {
let! list = generateRequestList ctx (parseListDate (Some date)) let! list = generateRequestList ctx (parseListDate (Some date))
return! return!
@@ -211,12 +213,12 @@ let print date : HttpHandler = requireAccess [ User; Group ] >=> fun next ctx ->
|> renderHtml next ctx |> renderHtml next ctx
} }
/// GET /prayer-request/[request-id]/restore // GET /prayer-request/[request-id]/restore
let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let requestId = PrayerRequestId reqId let requestId = PrayerRequestId reqId
match! findRequest ctx requestId with match! findRequest ctx requestId with
| Ok req -> | Ok req ->
do! PrayerRequests.updateExpiration { req with Expiration = Automatic; UpdatedDate = ctx.Now } true ctx.Conn do! PrayerRequests.updateExpiration { req with Expiration = Automatic; UpdatedDate = ctx.Now } true
addInfo ctx ctx.Strings["Successfully {0} prayer request", ctx.Strings["Restored"].Value.ToLower ()] addInfo ctx ctx.Strings["Successfully {0} prayer request", ctx.Strings["Restored"].Value.ToLower ()]
return! redirectTo false "/prayer-requests" next ctx return! redirectTo false "/prayer-requests" next ctx
| Result.Error e -> return! e | Result.Error e -> return! e
@@ -224,7 +226,7 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas
open System.Threading.Tasks open System.Threading.Tasks
/// POST /prayer-request/save // POST /prayer-request/save
let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditRequest> () with match! ctx.TryBindFormAsync<EditRequest> () with
| Ok model -> | Ok model ->
@@ -237,7 +239,7 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct
UserId = ctx.User.UserId.Value UserId = ctx.User.UserId.Value
} }
|> (Some >> Task.FromResult) |> (Some >> Task.FromResult)
else PrayerRequests.tryById (idFromShort PrayerRequestId model.RequestId) ctx.Conn else PrayerRequests.tryById (idFromShort PrayerRequestId model.RequestId)
match req with match req with
| Some pr when pr.SmallGroupId = group.Id -> | Some pr when pr.SmallGroupId = group.Id ->
let now = SmallGroup.localDateNow ctx.Clock group let now = SmallGroup.localDateNow ctx.Clock group
@@ -257,7 +259,7 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct
{ it with EnteredDate = dt; UpdatedDate = dt } { it with EnteredDate = dt; UpdatedDate = dt }
| it when defaultArg model.SkipDateUpdate false -> it | it when defaultArg model.SkipDateUpdate false -> it
| it -> { it with UpdatedDate = ctx.Now } | it -> { it with UpdatedDate = ctx.Now }
do! PrayerRequests.save updated ctx.Conn do! PrayerRequests.save updated
let act = if model.IsNew then "Added" else "Updated" let act = if model.IsNew then "Added" else "Updated"
addInfo ctx ctx.Strings["Successfully {0} prayer request", ctx.Strings[act].Value.ToLower ()] addInfo ctx ctx.Strings["Successfully {0} prayer request", ctx.Strings[act].Value.ToLower ()]
return! redirectTo false "/prayer-requests" next ctx return! redirectTo false "/prayer-requests" next ctx
@@ -266,7 +268,7 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// GET /prayer-request/view/[date?] // GET /prayer-request/view/[date?]
let view date : HttpHandler = requireAccess [ User; Group ] >=> fun next ctx -> task { let view date : HttpHandler = requireAccess [ User; Group ] >=> fun next ctx -> task {
let! list = generateRequestList ctx (parseListDate date) let! list = generateRequestList ctx (parseListDate date)
return! return!

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<PublishSingleFile>True</PublishSingleFile> <PublishSingleFile>False</PublishSingleFile>
<SelfContained>False</SelfContained> <SelfContained>False</SelfContained>
</PropertyGroup> </PropertyGroup>
@@ -24,10 +24,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="6.0.0" /> <PackageReference Include="Giraffe.Htmx" Version="1.9.2" />
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.1" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" /> <PackageReference Update="FSharp.Core" Version="7.0.300" />
<PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -7,21 +7,20 @@ open PrayerTracker.Data
open PrayerTracker.Entities open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
/// GET /small-group/announcement // GET /small-group/announcement
let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx -> let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
{ viewInfo ctx with HelpLink = Some Help.sendAnnouncement } { viewInfo ctx with HelpLink = Some Help.sendAnnouncement }
|> Views.SmallGroup.announcement ctx.Session.CurrentUser.Value.IsAdmin ctx |> Views.SmallGroup.announcement ctx.Session.CurrentUser.Value.IsAdmin ctx
|> renderHtml next ctx |> renderHtml 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 groupId = SmallGroupId grpId let groupId = SmallGroupId grpId
let conn = ctx.Conn match! SmallGroups.tryById groupId with
match! SmallGroups.tryById groupId conn with
| Some grp -> | Some grp ->
let! reqs = PrayerRequests.countByGroup groupId conn let! reqs = PrayerRequests.countByGroup groupId
let! users = Users.countByGroup groupId conn let! users = Users.countByGroup groupId
do! SmallGroups.deleteById groupId conn do! SmallGroups.deleteById groupId
addInfo ctx addInfo ctx
ctx.Strings["The group “{0}” and its {1} prayer request(s) were deleted successfully; revoked access from {2} user(s)", ctx.Strings["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]
@@ -29,22 +28,22 @@ let delete grpId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fu
| None -> return! fourOhFour ctx | None -> return! fourOhFour ctx
} }
/// 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 group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let memberId = MemberId mbrId let memberId = MemberId mbrId
match! Members.tryById memberId ctx.Conn with match! Members.tryById memberId with
| Some mbr when mbr.SmallGroupId = group.Id -> | Some mbr when mbr.SmallGroupId = group.Id ->
do! Members.deleteById memberId ctx.Conn do! Members.deleteById memberId
addHtmlInfo ctx ctx.Strings["The group member &ldquo;{0}&rdquo; was deleted successfully", mbr.Name] addHtmlInfo ctx ctx.Strings["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 _
| None -> return! fourOhFour ctx | None -> return! fourOhFour ctx
} }
/// 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 = Churches.all ctx.Conn let! churches = Churches.all ()
let groupId = SmallGroupId grpId let groupId = SmallGroupId grpId
if groupId.Value = Guid.Empty then if groupId.Value = Guid.Empty then
return! return!
@@ -52,7 +51,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! SmallGroups.tryById groupId ctx.Conn with match! SmallGroups.tryById groupId with
| Some grp -> | Some grp ->
return! return!
viewInfo ctx viewInfo ctx
@@ -61,7 +60,7 @@ let edit grpId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task
| None -> return! fourOhFour ctx | None -> return! fourOhFour ctx
} }
/// GET /small-group/member/[member-id]/edit // GET /small-group/member/[member-id]/edit
let editMember mbrId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let editMember mbrId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let types = ReferenceList.emailTypeList group.Preferences.DefaultEmailType ctx.Strings let types = ReferenceList.emailTypeList group.Preferences.DefaultEmailType ctx.Strings
@@ -72,7 +71,7 @@ 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! Members.tryById memberId ctx.Conn with match! Members.tryById memberId with
| Some mbr when mbr.SmallGroupId = group.Id -> | Some mbr when mbr.SmallGroupId = group.Id ->
return! return!
viewInfo ctx viewInfo ctx
@@ -82,9 +81,9 @@ let editMember mbrId : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
| None -> return! fourOhFour ctx | None -> return! fourOhFour 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 = SmallGroups.listProtected ctx.Conn let! groups = SmallGroups.listProtected ()
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 }
@@ -96,11 +95,11 @@ open System.Security.Claims
open Microsoft.AspNetCore.Authentication open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies open Microsoft.AspNetCore.Authentication.Cookies
/// POST /small-group/log-on/submit // POST /small-group/log-on/submit
let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task { let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<GroupLogOn> () with match! ctx.TryBindFormAsync<GroupLogOn> () with
| Ok model -> | Ok model ->
match! SmallGroups.logOn (idFromShort SmallGroupId model.SmallGroupId) model.Password ctx.Conn with match! SmallGroups.logOn (idFromShort SmallGroupId model.SmallGroupId) model.Password with
| Some group -> | Some group ->
ctx.Session.CurrentGroup <- Some group ctx.Session.CurrentGroup <- Some group
let identity = ClaimsIdentity ( let identity = ClaimsIdentity (
@@ -119,19 +118,19 @@ let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validat
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// GET /small-groups // GET /small-groups
let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
let! groups = SmallGroups.infoForAll ctx.Conn let! groups = SmallGroups.infoForAll ()
return! return!
viewInfo ctx viewInfo ctx
|> Views.SmallGroup.maintain groups ctx |> Views.SmallGroup.maintain groups ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
/// GET /small-group/members // GET /small-group/members
let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
let! members = Members.forGroup group.Id ctx.Conn let! members = Members.forGroup group.Id
let types = ReferenceList.emailTypeList group.Preferences.DefaultEmailType ctx.Strings |> Map.ofSeq let types = ReferenceList.emailTypeList group.Preferences.DefaultEmailType ctx.Strings |> Map.ofSeq
return! return!
{ viewInfo ctx with HelpLink = Some Help.maintainGroupMembers } { viewInfo ctx with HelpLink = Some Help.maintainGroupMembers }
@@ -139,20 +138,19 @@ let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|> renderHtml next ctx |> renderHtml next ctx
} }
/// 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 = PrayerRequests.forGroup let! reqs = PrayerRequests.forGroup
{ SmallGroup = group { SmallGroup = group
Clock = ctx.Clock Clock = ctx.Clock
ListDate = None ListDate = None
ActiveOnly = true ActiveOnly = true
PageNumber = 0 PageNumber = 0
} conn }
let! reqCount = PrayerRequests.countByGroup group.Id conn let! reqCount = PrayerRequests.countByGroup group.Id
let! mbrCount = Members.countByGroup group.Id conn let! mbrCount = Members.countByGroup group.Id
let! admins = Users.listByGroupId group.Id conn let! admins = Users.listByGroupId group.Id
let model = let model =
{ TotalActiveReqs = List.length reqs { TotalActiveReqs = List.length reqs
AllReqs = reqCount AllReqs = reqCount
@@ -173,7 +171,7 @@ let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|> renderHtml next ctx |> renderHtml next ctx
} }
/// GET /small-group/preferences // GET /small-group/preferences
let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
return! return!
{ viewInfo ctx with HelpLink = Some Help.groupPreferences } { viewInfo ctx with HelpLink = Some Help.groupPreferences }
@@ -183,16 +181,16 @@ let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task
open System.Threading.Tasks open System.Threading.Tasks
/// POST /small-group/save // POST /small-group/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditSmallGroup> () with match! ctx.TryBindFormAsync<EditSmallGroup> () with
| Ok model -> | Ok model ->
let! tryGroup = let! tryGroup =
if model.IsNew then Task.FromResult (Some { SmallGroup.empty with Id = (Guid.NewGuid >> SmallGroupId) () }) if model.IsNew then Task.FromResult (Some { SmallGroup.empty with Id = (Guid.NewGuid >> SmallGroupId) () })
else SmallGroups.tryById (idFromShort SmallGroupId model.SmallGroupId) ctx.Conn else SmallGroups.tryById (idFromShort SmallGroupId model.SmallGroupId)
match tryGroup with match tryGroup with
| Some group -> | Some group ->
do! SmallGroups.save (model.populateGroup group) model.IsNew ctx.Conn do! SmallGroups.save (model.populateGroup group) model.IsNew
let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower () let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower ()
addHtmlInfo ctx ctx.Strings["Successfully {0} group “{1}”", act, model.Name] addHtmlInfo ctx ctx.Strings["Successfully {0} group “{1}”", act, model.Name]
return! redirectTo false "/small-groups" next ctx return! redirectTo false "/small-groups" next ctx
@@ -200,7 +198,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// POST /small-group/member/save // POST /small-group/member/save
let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditMember> () with match! ctx.TryBindFormAsync<EditMember> () with
| Ok model -> | Ok model ->
@@ -208,7 +206,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun n
let! tryMbr = let! tryMbr =
if model.IsNew then if model.IsNew then
Task.FromResult (Some { Member.empty with Id = (Guid.NewGuid >> MemberId) (); SmallGroupId = group.Id }) Task.FromResult (Some { Member.empty with Id = (Guid.NewGuid >> MemberId) (); SmallGroupId = group.Id })
else Members.tryById (idFromShort MemberId model.MemberId) ctx.Conn else Members.tryById (idFromShort MemberId model.MemberId)
match tryMbr with match tryMbr with
| Some mbr when mbr.SmallGroupId = group.Id -> | Some mbr when mbr.SmallGroupId = group.Id ->
do! Members.save do! Members.save
@@ -216,7 +214,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun n
Name = model.Name Name = model.Name
Email = model.Email Email = model.Email
Format = String.noneIfBlank model.Format |> Option.map EmailFormat.fromCode Format = String.noneIfBlank model.Format |> Option.map EmailFormat.fromCode
} ctx.Conn }
let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower () let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower ()
addInfo ctx ctx.Strings["Successfully {0} group member", act] addInfo ctx ctx.Strings["Successfully {0} group member", act]
return! redirectTo false "/small-group/members" next ctx return! redirectTo false "/small-group/members" next ctx
@@ -225,7 +223,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun n
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// POST /small-group/preferences/save // POST /small-group/preferences/save
let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditPreferences> () with match! ctx.TryBindFormAsync<EditPreferences> () with
| Ok model -> | Ok model ->
@@ -233,10 +231,10 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
// we can repopulate the session instance. That way, if the update fails, the page should still show the // we can repopulate the session instance. That way, if the update fails, the page should still show the
// database values, not the then out-of-sync session ones. // database values, not the then out-of-sync session ones.
let group = ctx.Session.CurrentGroup.Value let group = ctx.Session.CurrentGroup.Value
match! SmallGroups.tryByIdWithPreferences group.Id ctx.Conn with match! SmallGroups.tryByIdWithPreferences group.Id with
| Some group -> | Some group ->
let pref = model.PopulatePreferences group.Preferences let pref = model.PopulatePreferences group.Preferences
do! SmallGroups.savePreferences pref ctx.Conn do! SmallGroups.savePreferences pref
// Refresh session instance // Refresh session instance
ctx.Session.CurrentGroup <- Some { group with Preferences = pref } ctx.Session.CurrentGroup <- Some { group with Preferences = pref }
addInfo ctx ctx.Strings["Group preferences updated successfully"] addInfo ctx ctx.Strings["Group preferences updated successfully"]
@@ -247,8 +245,9 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
open Giraffe.ViewEngine open Giraffe.ViewEngine
open PrayerTracker.Views.CommonFunctions open PrayerTracker.Views.CommonFunctions
open Microsoft.Extensions.Configuration
/// POST /small-group/announcement/send // POST /small-group/announcement/send
let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<Announcement> () with match! ctx.TryBindFormAsync<Announcement> () with
| Ok model -> | Ok model ->
@@ -266,11 +265,11 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
// Send the e-mails // Send the e-mails
let! recipients = task { let! recipients = task {
if model.SendToClass = "N" && usr.IsAdmin then if model.SendToClass = "N" && usr.IsAdmin then
let! users = Users.all ctx.Conn let! users = Users.all ()
return users |> List.map (fun u -> { Member.empty with Name = u.Name; Email = u.Email }) return users |> List.map (fun u -> { Member.empty with Name = u.Name; Email = u.Email })
else return! Members.forGroup group.Id ctx.Conn else return! Members.forGroup group.Id
} }
use! client = Email.getConnection () use! client = Email.getConnection (ctx.GetService<IConfiguration> ())
do! Email.sendEmails do! Email.sendEmails
{ Client = client { Client = client
Recipients = recipients Recipients = recipients
@@ -297,7 +296,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
Text = requestText Text = requestText
EnteredDate = now.Date.AtStartOfDayInZone(zone).ToInstant() EnteredDate = now.Date.AtStartOfDayInZone(zone).ToInstant()
UpdatedDate = now.InZoneLeniently(zone).ToInstant() UpdatedDate = now.InZoneLeniently(zone).ToInstant()
} ctx.Conn }
// Tell 'em what they've won, Johnny! // Tell 'em what they've won, Johnny!
let toWhom = let toWhom =
if model.SendToClass = "N" then s["{0} users", s["PrayerTracker"]].Value if model.SendToClass = "N" then s["{0} users", s["PrayerTracker"]].Value

View File

@@ -9,6 +9,8 @@ open PrayerTracker.Data
open PrayerTracker.Entities open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
#nowarn "44" // The default Rfc2898DeriveBytes is used to identify passwords to be upgraded
/// Password hashing implementation extending ASP.NET Core's identity implementation /// Password hashing implementation extending ASP.NET Core's identity implementation
[<AutoOpen>] [<AutoOpen>]
module Hashing = module Hashing =
@@ -53,15 +55,15 @@ module Hashing =
/// Retrieve a user from the database by password, upgrading password hashes if required /// Retrieve a user from the database by password, upgrading password hashes if required
let private findUserByPassword model conn = task { let private findUserByPassword model = task {
match! Users.tryByEmailAndGroup model.Email (idFromShort SmallGroupId model.SmallGroupId) conn with match! Users.tryByEmailAndGroup model.Email (idFromShort SmallGroupId model.SmallGroupId) with
| Some user -> | Some user ->
let hasher = PrayerTrackerPasswordHasher () let hasher = PrayerTrackerPasswordHasher ()
match hasher.VerifyHashedPassword (user, user.PasswordHash, model.Password) with match hasher.VerifyHashedPassword (user, user.PasswordHash, model.Password) with
| PasswordVerificationResult.Success -> return Some user | PasswordVerificationResult.Success -> return Some user
| PasswordVerificationResult.SuccessRehashNeeded -> | PasswordVerificationResult.SuccessRehashNeeded ->
let upgraded = { user with PasswordHash = hasher.HashPassword (user, model.Password) } let upgraded = { user with PasswordHash = hasher.HashPassword (user, model.Password) }
do! Users.updatePassword upgraded conn do! Users.updatePassword upgraded
return Some upgraded return Some upgraded
| _ -> return None | _ -> return None
| None -> return None | None -> return None
@@ -74,14 +76,14 @@ let sanitizeUrl providedUrl defaultUrl =
elif Seq.exists Char.IsControl url then defaultUrl elif Seq.exists Char.IsControl url then defaultUrl
else url else url
/// POST /user/password/change // POST /user/password/change
let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<ChangePassword> () with match! ctx.TryBindFormAsync<ChangePassword> () with
| Ok model -> | Ok model ->
let curUsr = ctx.Session.CurrentUser.Value let curUsr = ctx.Session.CurrentUser.Value
let hasher = PrayerTrackerPasswordHasher () let hasher = PrayerTrackerPasswordHasher ()
let! user = task { let! user = task {
match! Users.tryById curUsr.Id ctx.Conn with match! Users.tryById curUsr.Id with
| Some usr -> | Some usr ->
if hasher.VerifyHashedPassword (usr, usr.PasswordHash, model.OldPassword) if hasher.VerifyHashedPassword (usr, usr.PasswordHash, model.OldPassword)
= PasswordVerificationResult.Success then = PasswordVerificationResult.Success then
@@ -91,7 +93,7 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> f
} }
match user with match user with
| Some usr when model.NewPassword = model.NewPasswordConfirm -> | Some usr when model.NewPassword = model.NewPasswordConfirm ->
do! Users.updatePassword { usr with PasswordHash = hasher.HashPassword (usr, model.NewPassword) } ctx.Conn do! Users.updatePassword { usr with PasswordHash = hasher.HashPassword (usr, model.NewPassword) }
addInfo ctx ctx.Strings["Your password was changed successfully"] addInfo ctx ctx.Strings["Your password was changed successfully"]
return! redirectTo false "/" next ctx return! redirectTo false "/" next ctx
| Some _ -> | Some _ ->
@@ -103,12 +105,12 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> f
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// POST /user/[user-id]/delete // POST /user/[user-id]/delete
let delete usrId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { let delete usrId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
let userId = UserId usrId let userId = UserId usrId
match! Users.tryById userId ctx.Conn with match! Users.tryById userId with
| Some user -> | Some user ->
do! Users.deleteById userId ctx.Conn do! Users.deleteById userId
addInfo ctx ctx.Strings["Successfully deleted user {0}", user.Name] addInfo ctx ctx.Strings["Successfully deleted user {0}", user.Name]
return! redirectTo false "/users" next ctx return! redirectTo false "/users" next ctx
| _ -> return! fourOhFour ctx | _ -> return! fourOhFour ctx
@@ -120,14 +122,14 @@ open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.AspNetCore.Html open Microsoft.AspNetCore.Html
/// POST /user/log-on // POST /user/log-on
let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task { let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<UserLogOn> () with match! ctx.TryBindFormAsync<UserLogOn> () with
| Ok model -> | Ok model ->
let s = ctx.Strings let s = ctx.Strings
match! findUserByPassword model ctx.Conn with match! findUserByPassword model with
| Some user -> | Some user ->
match! SmallGroups.tryByIdWithPreferences (idFromShort SmallGroupId model.SmallGroupId) ctx.Conn with match! SmallGroups.tryByIdWithPreferences (idFromShort SmallGroupId model.SmallGroupId) with
| Some group -> | Some group ->
ctx.Session.CurrentUser <- Some user ctx.Session.CurrentUser <- Some user
ctx.Session.CurrentGroup <- Some group ctx.Session.CurrentGroup <- Some group
@@ -141,7 +143,7 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsr
AuthenticationProperties ( AuthenticationProperties (
IssuedUtc = DateTimeOffset.UtcNow, IssuedUtc = DateTimeOffset.UtcNow,
IsPersistent = defaultArg model.RememberMe false)) IsPersistent = defaultArg model.RememberMe false))
do! Users.updateLastSeen user.Id ctx.Now ctx.Conn do! Users.updateLastSeen user.Id ctx.Now
addHtmlInfo ctx s["Log On Successful Welcome to {0}", s["PrayerTracker"]] addHtmlInfo ctx s["Log On Successful Welcome to {0}", s["PrayerTracker"]]
return! redirectTo false (sanitizeUrl model.RedirectUrl "/small-group") next ctx return! redirectTo false (sanitizeUrl model.RedirectUrl "/small-group") next ctx
| None -> return! fourOhFour ctx | None -> return! fourOhFour ctx
@@ -163,7 +165,7 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsr
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// GET /user/[user-id]/edit // GET /user/[user-id]/edit
let edit usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let edit usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
let userId = UserId usrId let userId = UserId usrId
if userId.Value = Guid.Empty then if userId.Value = Guid.Empty then
@@ -172,7 +174,7 @@ let edit usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task
|> Views.User.edit EditUser.empty ctx |> Views.User.edit EditUser.empty ctx
|> renderHtml next ctx |> renderHtml next ctx
else else
match! Users.tryById userId ctx.Conn with match! Users.tryById userId with
| Some user -> | Some user ->
return! return!
viewInfo ctx viewInfo ctx
@@ -181,9 +183,9 @@ let edit usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task
| _ -> return! fourOhFour ctx | _ -> return! fourOhFour ctx
} }
/// GET /user/log-on // GET /user/log-on
let logOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { let logOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task {
let! groups = SmallGroups.listAll ctx.Conn let! groups = SmallGroups.listAll ()
let url = Option.ofObj <| ctx.Session.GetString Key.Session.redirectUrl let url = Option.ofObj <| ctx.Session.GetString Key.Session.redirectUrl
match url with match url with
| Some _ -> | Some _ ->
@@ -196,16 +198,16 @@ let logOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
/// GET /users // GET /users
let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
let! users = Users.all ctx.Conn let! users = Users.all ()
return! return!
viewInfo ctx viewInfo ctx
|> Views.User.maintain users ctx |> Views.User.maintain users ctx
|> renderHtml next ctx |> renderHtml next ctx
} }
/// GET /user/password // GET /user/password
let password : HttpHandler = requireAccess [ User ] >=> fun next ctx -> let password : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
{ viewInfo ctx with HelpLink = Some Help.changePassword } { viewInfo ctx with HelpLink = Some Help.changePassword }
|> Views.User.changePassword ctx |> Views.User.changePassword ctx
@@ -213,18 +215,18 @@ let password : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
open System.Threading.Tasks open System.Threading.Tasks
/// POST /user/save // POST /user/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditUser> () with match! ctx.TryBindFormAsync<EditUser> () with
| Ok model -> | Ok model ->
let! user = let! user =
if model.IsNew then Task.FromResult (Some { User.empty with Id = (Guid.NewGuid >> UserId) () }) if model.IsNew then Task.FromResult (Some { User.empty with Id = (Guid.NewGuid >> UserId) () })
else Users.tryById (idFromShort UserId model.UserId) ctx.Conn else Users.tryById (idFromShort UserId model.UserId)
match user with match user with
| Some usr -> | Some usr ->
let hasher = PrayerTrackerPasswordHasher () let hasher = PrayerTrackerPasswordHasher ()
let updatedUser = model.PopulateUser usr (fun pw -> hasher.HashPassword (usr, pw)) let updatedUser = model.PopulateUser usr (fun pw -> hasher.HashPassword (usr, pw))
do! Users.save updatedUser ctx.Conn do! Users.save updatedUser
let s = ctx.Strings let s = ctx.Strings
if model.IsNew then if model.IsNew then
let h = CommonFunctions.htmlString let h = CommonFunctions.htmlString
@@ -244,7 +246,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next c
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// POST /user/small-groups/save // POST /user/small-groups/save
let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task { let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<AssignGroups> () with match! ctx.TryBindFormAsync<AssignGroups> () with
| Ok model -> | Ok model ->
@@ -254,19 +256,19 @@ let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun
return! redirectTo false $"/user/{model.UserId}/small-groups" next ctx return! redirectTo false $"/user/{model.UserId}/small-groups" next ctx
| _ -> | _ ->
do! Users.updateSmallGroups (idFromShort UserId model.UserId) do! Users.updateSmallGroups (idFromShort UserId model.UserId)
(model.SmallGroups.Split ',' |> Array.map (idFromShort SmallGroupId) |> List.ofArray) ctx.Conn (model.SmallGroups.Split ',' |> Array.map (idFromShort SmallGroupId) |> List.ofArray)
addInfo ctx ctx.Strings["Successfully updated group permissions for {0}", model.UserName] addInfo ctx ctx.Strings["Successfully updated group permissions for {0}", model.UserName]
return! redirectTo false "/users" next ctx return! redirectTo false "/users" next ctx
| Result.Error e -> return! bindError e next ctx | Result.Error e -> return! bindError e next ctx
} }
/// GET /user/[user-id]/small-groups // GET /user/[user-id]/small-groups
let smallGroups usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task { let smallGroups usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
let userId = UserId usrId let userId = UserId usrId
match! Users.tryById userId ctx.Conn with match! Users.tryById userId with
| Some user -> | Some user ->
let! groups = SmallGroups.listAll ctx.Conn let! groups = SmallGroups.listAll ()
let! groupIds = Users.groupIdsByUserId userId ctx.Conn let! groupIds = Users.groupIdsByUserId userId
let curGroups = groupIds |> List.map (fun g -> shortGuid g.Value) let curGroups = groupIds |> List.map (fun g -> shortGuid g.Value)
return! return!
viewInfo ctx viewInfo ctx

View File

@@ -1,3 +0,0 @@
Set-Location PrayerTracker
dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true --self-contained false
Set-Location bin\Release\net6.0\linux-x64\publish

View File

@@ -1,110 +0,0 @@
-- Church
ALTER TABLE pt."Church" RENAME COLUMN "ChurchId" TO id;
ALTER TABLE pt."Church" RENAME COLUMN "Name" TO church_name;
ALTER TABLE pt."Church" RENAME COLUMN "City" TO city;
ALTER TABLE pt."Church" RENAME COLUMN "ST" TO state;
ALTER TABLE pt."Church" RENAME COLUMN "HasVirtualPrayerRoomInterface" TO has_vps_interface;
ALTER TABLE pt."Church" RENAME COLUMN "InterfaceAddress" TO interface_address;
ALTER TABLE pt."Church" RENAME CONSTRAINT "PK_Church" TO pk_church;
ALTER TABLE pt."Church" RENAME TO church;
-- List Preference
ALTER TABLE pt."ListPreference" RENAME COLUMN "SmallGroupId" TO small_group_id;
ALTER TABLE pt."ListPreference" RENAME COLUMN "DaysToExpire" TO days_to_expire;
ALTER TABLE pt."ListPreference" RENAME COLUMN "DaysToKeepNew" TO days_to_keep_new;
ALTER TABLE pt."ListPreference" RENAME COLUMN "LongTermUpdateWeeks" TO long_term_update_weeks;
ALTER TABLE pt."ListPreference" RENAME COLUMN "EmailFromName" TO email_from_name;
ALTER TABLE pt."ListPreference" RENAME COLUMN "EmailFromAddress" TO email_from_address;
ALTER TABLE pt."ListPreference" RENAME COLUMN "ListFonts" TO fonts;
ALTER TABLE pt."ListPreference" RENAME COLUMN "HeadingColor" TO heading_color;
ALTER TABLE pt."ListPreference" RENAME COLUMN "LineColor" TO line_color;
ALTER TABLE pt."ListPreference" RENAME COLUMN "HeadingFontSize" TO heading_font_size;
ALTER TABLE pt."ListPreference" RENAME COLUMN "TextFontSize" TO text_font_size;
ALTER TABLE pt."ListPreference" RENAME COLUMN "RequestSort" TO request_sort;
ALTER TABLE pt."ListPreference" RENAME COLUMN "GroupPassword" TO group_password;
ALTER TABLE pt."ListPreference" RENAME COLUMN "DefaultEmailType" TO default_email_type;
ALTER TABLE pt."ListPreference" RENAME COLUMN "IsPublic" TO is_public;
ALTER TABLE pt."ListPreference" RENAME COLUMN "TimeZoneId" TO time_zone_id;
ALTER TABLE pt."ListPreference" RENAME COLUMN "PageSize" TO page_size;
ALTER TABLE pt."ListPreference" RENAME COLUMN "AsOfDateDisplay" TO as_of_date_display;
ALTER TABLE pt."ListPreference" RENAME CONSTRAINT "PK_ListPreference" TO pk_list_preference;
ALTER TABLE pt."ListPreference" RENAME CONSTRAINT "FK_ListPreference_SmallGroup_SmallGroupId" TO fk_list_preference_small_group_id;
ALTER TABLE pt."ListPreference" DROP CONSTRAINT "FK_ListPreference_TimeZone_TimeZoneId";
ALTER TABLE pt."ListPreference" RENAME TO list_preference;
ALTER INDEX pt."IX_ListPreference_TimeZoneId" RENAME TO ix_list_preference_time_zone_id;
ALTER TABLE pt.list_preference ALTER COLUMN email_from_address SET DEFAULT 'prayer@bitbadger.solutions';
ALTER TABLE pt.list_preference ALTER COLUMN fonts SET DEFAULT 'native';
-- Small Group Member
ALTER TABLE pt."Member" RENAME COLUMN "MemberId" TO id;
ALTER TABLE pt."Member" RENAME COLUMN "SmallGroupId" TO small_group_id;
ALTER TABLE pt."Member" RENAME COLUMN "MemberName" TO member_name;
ALTER TABLE pt."Member" RENAME COLUMN "Email" TO email;
ALTER TABLE pt."Member" RENAME COLUMN "Format" TO email_format;
ALTER TABLE pt."Member" RENAME CONSTRAINT "PK_Member" TO pk_member;
ALTER TABLE pt."Member" RENAME CONSTRAINT "FK_Member_SmallGroup_SmallGroupId" TO fk_member_small_group_id;
ALTER TABLE pt."Member" RENAME TO member;
ALTER INDEX pt."IX_Member_SmallGroupId" RENAME TO ix_member_small_group_id;
-- Prayer Request
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "PrayerRequestId" TO id;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "RequestType" TO request_type;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "UserId" TO user_id;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "SmallGroupId" TO small_group_id;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "EnteredDate" TO entered_date;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "UpdatedDate" TO updated_date;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "Requestor" TO requestor;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "Text" TO request_text;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "NotifyChaplain" TO notify_chaplain;
ALTER TABLE pt."PrayerRequest" RENAME COLUMN "Expiration" TO expiration;
ALTER TABLE pt."PrayerRequest" RENAME CONSTRAINT "PK_PrayerRequest" TO pk_prayer_request;
ALTER TABLE pt."PrayerRequest" RENAME CONSTRAINT "FK_PrayerRequest_User_UserId" TO fk_prayer_request_user_id;
ALTER TABLE pt."PrayerRequest" RENAME CONSTRAINT "FK_PrayerRequest_SmallGroup_SmallGroupId" TO fk_prayer_request_small_group_id;
ALTER TABLE pt."PrayerRequest" RENAME TO prayer_request;
ALTER INDEX pt."IX_PrayerRequest_UserId" RENAME TO ix_prayer_request_user_id;
ALTER INDEX pt."IX_PrayerRequest_SmallGroupId" RENAME TO ix_prayer_request_small_group_id;
ALTER INDEX pt."IX_PrayerRequest_Requestor_TRGM" RENAME TO ix_prayer_request_trgm_requestor;
ALTER INDEX pt."IX_PrayerRequest_Text_TRGM" RENAME TO ix_prayer_request_trgm_request_text;
-- Small Group
ALTER TABLE pt."SmallGroup" RENAME COLUMN "SmallGroupId" TO id;
ALTER TABLE pt."SmallGroup" RENAME COLUMN "ChurchId" TO church_id;
ALTER TABLE pt."SmallGroup" RENAME COLUMN "Name" TO group_name;
ALTER TABLE pt."SmallGroup" RENAME CONSTRAINT "PK_SmallGroup" TO pk_small_group;
ALTER TABLE pt."SmallGroup" RENAME CONSTRAINT "FK_SmallGroup_Church_ChurchId" TO fk_small_group_church_id;
ALTER TABLE pt."SmallGroup" RENAME TO small_group;
ALTER INDEX pt."IX_SmallGroup_ChurchId" RENAME TO ix_small_group_church_id;
-- Time Zone (goes away)
DROP TABLE pt."TimeZone";
-- User
ALTER TABLE pt."User" RENAME COLUMN "UserId" TO id;
ALTER TABLE pt."User" RENAME COLUMN "FirstName" TO first_name;
ALTER TABLE pt."User" RENAME COLUMN "LastName" TO last_name;
ALTER TABLE pt."User" RENAME COLUMN "EmailAddress" TO email;
ALTER TABLE pt."User" RENAME COLUMN "IsSystemAdmin" TO is_admin;
ALTER TABLE pt."User" RENAME COLUMN "PasswordHash" TO password_hash;
ALTER TABLE pt."User" RENAME COLUMN "Salt" TO salt;
ALTER TABLE pt."User" RENAME CONSTRAINT "PK_User" TO pk_pt_user;
ALTER TABLE pt."User" RENAME TO pt_user;
ALTER TABLE pt.pt_user ADD COLUMN last_seen timestamptz;
-- User / Small Group
ALTER TABLE pt."User_SmallGroup" RENAME COLUMN "UserId" TO user_id;
ALTER TABLE pt."User_SmallGroup" RENAME COLUMN "SmallGroupId" TO small_group_id;
ALTER TABLE pt."User_SmallGroup" RENAME CONSTRAINT "PK_User_SmallGroup" TO pk_user_small_group;
ALTER TABLE pt."User_SmallGroup" RENAME CONSTRAINT "FK_User_SmallGroup_User_UserId" TO fk_user_small_group_user_id;
ALTER TABLE pt."User_SmallGroup" RENAME CONSTRAINT "FK_User_SmallGroup_SmallGroup_SmallGroupId" TO fk_user_small_group_small_group_id;
ALTER TABLE pt."User_SmallGroup" RENAME TO user_small_group;
ALTER INDEX pt."IX_User_SmallGroup_SmallGroupId" RENAME TO ix_user_small_group_small_group_id;
-- #41 - change to timestamptz
SET TimeZone = 'UTC';
ALTER TABLE pt.prayer_request ALTER COLUMN entered_date TYPE timestamptz;
ALTER TABLE pt.prayer_request ALTER COLUMN updated_date TYPE timestamptz;

View File

@@ -1,27 +0,0 @@
create extension pg_trgm;
set search_path=pt,public;
create index "IX_PrayerRequest_Requestor_TRGM" on "PrayerRequest" using GIN (COALESCE("Requestor", '') gin_trgm_ops);
create index "IX_PrayerRequest_Text_TRGM" on "PrayerRequest" using GIN ("Text" gin_trgm_ops);
alter table "ListPreference" add column "PageSize" int not null default 100;
alter table "ListPreference" add column "AsOfDateDisplay" varchar(1) not null default 'N';
/* RequestType to 1 character code */
update "PrayerRequest" set "RequestType" = 'C' where "RequestType" = 'Current';
update "PrayerRequest" set "RequestType" = 'L' where "RequestType" = 'Recurring';
update "PrayerRequest" set "RequestType" = 'P' where "RequestType" = 'Praise';
update "PrayerRequest" set "RequestType" = 'E' where "RequestType" = 'Expecting';
update "PrayerRequest" set "RequestType" = 'A' where "RequestType" = 'Announcement';
alter table "PrayerRequest" alter column "RequestType" set data type varchar(1);
/* Change expiration to a 1-character code field */
alter table "PrayerRequest" add column "Expiration" varchar(1);
update "PrayerRequest" set "Expiration" = case when "IsManuallyExpired" then 'F' when "DoNotExpire" then 'M' else 'A' end;
alter table "PrayerRequest" alter column "Expiration" set not null;
alter table "PrayerRequest" drop column "DoNotExpire";
alter table "PrayerRequest" drop column "IsManuallyExpired";
/* Change e-mail type to 1-character code field in list preferences and members */
update "ListPreference" set "DefaultEmailType" = 'H' where "DefaultEmailType" = 'Html';
update "ListPreference" set "DefaultEmailType" = 'P' where "DefaultEmailType" = 'Text';
alter table "ListPreference" alter column "DefaultEmailType" set default 'H';
alter table "ListPreference" alter column "DefaultEmailType" set data type varchar(1);
update "Member" set "Format" = 'H' where "Format" = 'Html';
update "Member" set "Format" = 'P' where "Format" = 'Text';
alter table "Member" alter column "Format" set data type varchar(1);