Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd5f32e320 | |||
| 370fbb0c3e | |||
| 14608de5be | |||
| ec6eea21c8 | |||
|
|
3b0d304595 | ||
| 50a91cd725 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -335,3 +335,4 @@ docs/_site
|
||||
**/*.db*
|
||||
|
||||
.ionide
|
||||
.vscode
|
||||
|
||||
@@ -26,7 +26,7 @@ Target.create "Test" (fun _ ->
|
||||
let testPath = $"{projPath}.Tests"
|
||||
DotNet.build (fun opts -> { opts with NoLogo = true }) $"{testPath}/PrayerTracker.Tests.fsproj"
|
||||
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" ])
|
||||
|
||||
Target.create "Publish" (fun _ ->
|
||||
|
||||
@@ -1,97 +1,98 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activesupport (3.2.22.5)
|
||||
i18n (~> 0.6, >= 0.6.4)
|
||||
multi_json (~> 1.0)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
activesupport (7.0.4.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
coffee-script (2.4.1)
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.11.1)
|
||||
colorator (1.1.0)
|
||||
commonmarker (0.17.13)
|
||||
ruby-enum (~> 0.5)
|
||||
concurrent-ruby (1.1.7)
|
||||
dnsruby (1.61.5)
|
||||
simpleidn (~> 0.1)
|
||||
em-websocket (0.5.2)
|
||||
commonmarker (0.23.9)
|
||||
concurrent-ruby (1.2.2)
|
||||
dnsruby (1.70.0)
|
||||
simpleidn (~> 0.2.1)
|
||||
em-websocket (0.5.3)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
ethon (0.12.0)
|
||||
ffi (>= 1.3.0)
|
||||
http_parser.rb (~> 0)
|
||||
ethon (0.16.0)
|
||||
ffi (>= 1.15.0)
|
||||
eventmachine (1.2.7)
|
||||
eventmachine (1.2.7-x64-mingw32)
|
||||
execjs (2.7.0)
|
||||
faraday (1.1.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords
|
||||
ffi (1.13.1)
|
||||
ffi (1.13.1-x64-mingw32)
|
||||
execjs (2.8.1)
|
||||
faraday (2.7.5)
|
||||
faraday-net_http (>= 2.0, < 3.1)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-net_http (3.0.2)
|
||||
ffi (1.15.5)
|
||||
forwardable-extended (2.6.0)
|
||||
gemoji (3.0.1)
|
||||
github-pages (209)
|
||||
github-pages-health-check (= 1.16.1)
|
||||
jekyll (= 3.9.0)
|
||||
github-pages (228)
|
||||
github-pages-health-check (= 1.17.9)
|
||||
jekyll (= 3.9.3)
|
||||
jekyll-avatar (= 0.7.0)
|
||||
jekyll-coffeescript (= 1.1.1)
|
||||
jekyll-commonmark-ghpages (= 0.1.6)
|
||||
jekyll-commonmark-ghpages (= 0.4.0)
|
||||
jekyll-default-layout (= 0.1.4)
|
||||
jekyll-feed (= 0.15.1)
|
||||
jekyll-gist (= 1.5.0)
|
||||
jekyll-github-metadata (= 2.13.0)
|
||||
jekyll-include-cache (= 0.2.1)
|
||||
jekyll-mentions (= 1.6.0)
|
||||
jekyll-optional-front-matter (= 0.3.2)
|
||||
jekyll-paginate (= 1.1.0)
|
||||
jekyll-readme-index (= 0.3.0)
|
||||
jekyll-redirect-from (= 0.16.0)
|
||||
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-seo-tag (= 2.6.1)
|
||||
jekyll-seo-tag (= 2.8.0)
|
||||
jekyll-sitemap (= 1.4.0)
|
||||
jekyll-swiss (= 1.0.0)
|
||||
jekyll-theme-architect (= 0.1.1)
|
||||
jekyll-theme-cayman (= 0.1.1)
|
||||
jekyll-theme-dinky (= 0.1.1)
|
||||
jekyll-theme-hacker (= 0.1.2)
|
||||
jekyll-theme-leap-day (= 0.1.1)
|
||||
jekyll-theme-merlot (= 0.1.1)
|
||||
jekyll-theme-midnight (= 0.1.1)
|
||||
jekyll-theme-minimal (= 0.1.1)
|
||||
jekyll-theme-modernist (= 0.1.1)
|
||||
jekyll-theme-primer (= 0.5.4)
|
||||
jekyll-theme-slate (= 0.1.1)
|
||||
jekyll-theme-tactile (= 0.1.1)
|
||||
jekyll-theme-time-machine (= 0.1.1)
|
||||
jekyll-theme-architect (= 0.2.0)
|
||||
jekyll-theme-cayman (= 0.2.0)
|
||||
jekyll-theme-dinky (= 0.2.0)
|
||||
jekyll-theme-hacker (= 0.2.0)
|
||||
jekyll-theme-leap-day (= 0.2.0)
|
||||
jekyll-theme-merlot (= 0.2.0)
|
||||
jekyll-theme-midnight (= 0.2.0)
|
||||
jekyll-theme-minimal (= 0.2.0)
|
||||
jekyll-theme-modernist (= 0.2.0)
|
||||
jekyll-theme-primer (= 0.6.0)
|
||||
jekyll-theme-slate (= 0.2.0)
|
||||
jekyll-theme-tactile (= 0.2.0)
|
||||
jekyll-theme-time-machine (= 0.2.0)
|
||||
jekyll-titles-from-headings (= 0.5.3)
|
||||
jemoji (= 0.12.0)
|
||||
kramdown (= 2.3.0)
|
||||
kramdown (= 2.3.2)
|
||||
kramdown-parser-gfm (= 1.1.0)
|
||||
liquid (= 4.0.3)
|
||||
liquid (= 4.0.4)
|
||||
mercenary (~> 0.3)
|
||||
minima (= 2.5.1)
|
||||
nokogiri (>= 1.10.4, < 2.0)
|
||||
rouge (= 3.23.0)
|
||||
nokogiri (>= 1.13.6, < 2.0)
|
||||
rouge (= 3.26.0)
|
||||
terminal-table (~> 1.4)
|
||||
github-pages-health-check (1.16.1)
|
||||
github-pages-health-check (1.17.9)
|
||||
addressable (~> 2.3)
|
||||
dnsruby (~> 1.60)
|
||||
octokit (~> 4.0)
|
||||
public_suffix (~> 3.0)
|
||||
public_suffix (>= 3.0, < 5.0)
|
||||
typhoeus (~> 1.3)
|
||||
html-pipeline (2.14.0)
|
||||
html-pipeline (2.14.3)
|
||||
activesupport (>= 2)
|
||||
nokogiri (>= 1.4)
|
||||
http_parser.rb (0.6.0)
|
||||
i18n (0.9.5)
|
||||
http_parser.rb (0.8.0)
|
||||
i18n (1.13.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (3.9.0)
|
||||
jekyll (3.9.3)
|
||||
addressable (~> 2.4)
|
||||
colorator (~> 1.0)
|
||||
em-websocket (~> 0.5)
|
||||
i18n (~> 0.7)
|
||||
i18n (>= 0.7, < 2)
|
||||
jekyll-sass-converter (~> 1.0)
|
||||
jekyll-watch (~> 2.0)
|
||||
kramdown (>= 1.17, < 3)
|
||||
@@ -105,13 +106,13 @@ GEM
|
||||
jekyll-coffeescript (1.1.1)
|
||||
coffee-script (~> 2.2)
|
||||
coffee-script-source (~> 1.11.1)
|
||||
jekyll-commonmark (1.3.1)
|
||||
commonmarker (~> 0.14)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-commonmark-ghpages (0.1.6)
|
||||
commonmarker (~> 0.17.6)
|
||||
jekyll-commonmark (~> 1.2)
|
||||
rouge (>= 2.0, < 4.0)
|
||||
jekyll-commonmark (1.4.0)
|
||||
commonmarker (~> 0.22)
|
||||
jekyll-commonmark-ghpages (0.4.0)
|
||||
commonmarker (~> 0.23.7)
|
||||
jekyll (~> 3.9.0)
|
||||
jekyll-commonmark (~> 1.4.0)
|
||||
rouge (>= 2.0, < 5.0)
|
||||
jekyll-default-layout (0.1.4)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-feed (0.15.1)
|
||||
@@ -121,6 +122,8 @@ GEM
|
||||
jekyll-github-metadata (2.13.0)
|
||||
jekyll (>= 3.4, < 5.0)
|
||||
octokit (~> 4.0, != 4.4.0)
|
||||
jekyll-include-cache (0.2.1)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-mentions (1.6.0)
|
||||
html-pipeline (~> 2.3)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
@@ -133,57 +136,57 @@ GEM
|
||||
jekyll (>= 3.3, < 5.0)
|
||||
jekyll-relative-links (0.6.1)
|
||||
jekyll (>= 3.3, < 5.0)
|
||||
jekyll-remote-theme (0.4.2)
|
||||
jekyll-remote-theme (0.4.3)
|
||||
addressable (~> 2.0)
|
||||
jekyll (>= 3.5, < 5.0)
|
||||
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
|
||||
rubyzip (>= 1.3.0, < 3.0)
|
||||
jekyll-sass-converter (1.5.2)
|
||||
sass (~> 3.4)
|
||||
jekyll-seo-tag (2.6.1)
|
||||
jekyll (>= 3.3, < 5.0)
|
||||
jekyll-seo-tag (2.8.0)
|
||||
jekyll (>= 3.8, < 5.0)
|
||||
jekyll-sitemap (1.4.0)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-swiss (1.0.0)
|
||||
jekyll-theme-architect (0.1.1)
|
||||
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-theme-architect (0.2.0)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-leap-day (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-theme-cayman (0.2.0)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-merlot (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-theme-dinky (0.2.0)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-midnight (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-theme-hacker (0.2.0)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-minimal (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-theme-leap-day (0.2.0)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-modernist (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-theme-merlot (0.2.0)
|
||||
jekyll (> 3.5, < 5.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-github-metadata (~> 2.9)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-slate (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-theme-slate (0.2.0)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-tactile (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-theme-tactile (0.2.0)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-theme-time-machine (0.1.1)
|
||||
jekyll (~> 3.5)
|
||||
jekyll-theme-time-machine (0.2.0)
|
||||
jekyll (> 3.5, < 5.0)
|
||||
jekyll-seo-tag (~> 2.0)
|
||||
jekyll-titles-from-headings (0.5.3)
|
||||
jekyll (>= 3.3, < 5.0)
|
||||
@@ -193,75 +196,64 @@ GEM
|
||||
gemoji (~> 3.0)
|
||||
html-pipeline (~> 2.2)
|
||||
jekyll (>= 3.0, < 5.0)
|
||||
kramdown (2.3.0)
|
||||
kramdown (2.3.2)
|
||||
rexml
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
liquid (4.0.3)
|
||||
listen (3.3.1)
|
||||
liquid (4.0.4)
|
||||
listen (3.8.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
mercenary (0.3.6)
|
||||
mini_portile2 (2.8.0)
|
||||
minima (2.5.1)
|
||||
jekyll (>= 3.5, < 5.0)
|
||||
jekyll-feed (~> 0.9)
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.1.1)
|
||||
nokogiri (1.13.6)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
minitest (5.18.0)
|
||||
nokogiri (1.15.1-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.6-x64-mingw32)
|
||||
racc (~> 1.4)
|
||||
octokit (4.19.0)
|
||||
faraday (>= 0.9)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
octokit (4.25.1)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
pathutil (0.16.2)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (3.1.1)
|
||||
racc (1.6.0)
|
||||
rb-fsevent (0.10.4)
|
||||
public_suffix (4.0.7)
|
||||
racc (1.6.2)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.2.5)
|
||||
rouge (3.23.0)
|
||||
ruby-enum (0.8.0)
|
||||
i18n
|
||||
ruby2_keywords (0.0.2)
|
||||
rubyzip (2.3.0)
|
||||
rouge (3.26.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
safe_yaml (1.0.5)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sawyer (0.8.2)
|
||||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (> 0.8, < 2.0)
|
||||
simpleidn (0.1.1)
|
||||
faraday (>= 0.17.3, < 3)
|
||||
simpleidn (0.2.1)
|
||||
unf (~> 0.1.4)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.3)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2020.4)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unf_ext (0.0.7.7-x64-mingw32)
|
||||
unicode-display_width (1.7.0)
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x64-mingw32
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
github-pages
|
||||
tzinfo-data
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
2.3.15
|
||||
|
||||
2
src/.dockerignore
Normal file
2
src/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
**/bin/*
|
||||
**/obj/*
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<AssemblyVersion>8.0.0.0</AssemblyVersion>
|
||||
<FileVersion>8.0.0.0</FileVersion>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<AssemblyVersion>8.1.0.0</AssemblyVersion>
|
||||
<FileVersion>8.1.0.0</FileVersion>
|
||||
<Authors>danieljsummers</Authors>
|
||||
<Company>Bit Badger Solutions</Company>
|
||||
<Version>8.0.0</Version>
|
||||
<Version>8.1.0</Version>
|
||||
<DebugType>Embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
25
src/Dockerfile
Normal file
25
src/Dockerfile
Normal 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" ]
|
||||
@@ -103,126 +103,93 @@ module private Helpers =
|
||||
}
|
||||
|
||||
|
||||
open BitBadger.Npgsql.FSharp.Documents
|
||||
|
||||
/// Functions to manipulate churches
|
||||
module Churches =
|
||||
|
||||
/// Get a list of all churches
|
||||
let all conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT * FROM pt.church ORDER BY church_name"
|
||||
|> Sql.executeAsync mapToChurch
|
||||
let all () =
|
||||
Custom.list "SELECT * FROM pt.church ORDER BY church_name" [] mapToChurch
|
||||
|
||||
/// 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 where = "WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)"
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
Configuration.dataSource ()
|
||||
|> Sql.fromDataSource
|
||||
|> Sql.executeTransactionAsync
|
||||
[ $"DELETE FROM pt.prayer_request {where}", idParam
|
||||
$"DELETE FROM pt.user_small_group {where}", idParam
|
||||
$"DELETE FROM pt.list_preference {where}", idParam
|
||||
"DELETE FROM pt.small_group WHERE church_id = @churchId", idParam
|
||||
"DELETE FROM pt.church WHERE id = @churchId", idParam ]
|
||||
return ()
|
||||
()
|
||||
}
|
||||
|
||||
/// Save a church's information
|
||||
let save (church : Church) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
INSERT INTO pt.church (
|
||||
id, church_name, city, state, has_vps_interface, interface_address
|
||||
) VALUES (
|
||||
@id, @name, @city, @state, @hasVpsInterface, @interfaceAddress
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET church_name = EXCLUDED.church_name,
|
||||
city = EXCLUDED.city,
|
||||
state = EXCLUDED.state,
|
||||
has_vps_interface = EXCLUDED.has_vps_interface,
|
||||
interface_address = EXCLUDED.interface_address"""
|
||||
|> Sql.parameters
|
||||
[ "@id", Sql.uuid church.Id.Value
|
||||
"@name", Sql.string church.Name
|
||||
"@city", Sql.string church.City
|
||||
"@state", Sql.string church.State
|
||||
"@hasVpsInterface", Sql.bool church.HasVpsInterface
|
||||
"@interfaceAddress", Sql.stringOrNone church.InterfaceAddress ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
let save (church : Church) =
|
||||
Custom.nonQuery
|
||||
"INSERT INTO pt.church (
|
||||
id, church_name, city, state, has_vps_interface, interface_address
|
||||
) VALUES (
|
||||
@id, @name, @city, @state, @hasVpsInterface, @interfaceAddress
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET church_name = EXCLUDED.church_name,
|
||||
city = EXCLUDED.city,
|
||||
state = EXCLUDED.state,
|
||||
has_vps_interface = EXCLUDED.has_vps_interface,
|
||||
interface_address = EXCLUDED.interface_address"
|
||||
[ "@id", Sql.uuid church.Id.Value
|
||||
"@name", Sql.string church.Name
|
||||
"@city", Sql.string church.City
|
||||
"@state", Sql.string church.State
|
||||
"@hasVpsInterface", Sql.bool church.HasVpsInterface
|
||||
"@interfaceAddress", Sql.stringOrNone church.InterfaceAddress ]
|
||||
|
||||
/// Find a church by its ID
|
||||
let tryById (churchId : ChurchId) conn = backgroundTask {
|
||||
let! church =
|
||||
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
|
||||
}
|
||||
let tryById (churchId : ChurchId) =
|
||||
Custom.single "SELECT * FROM pt.church WHERE id = @id" [ "@id", Sql.uuid churchId.Value ] mapToChurch
|
||||
|
||||
|
||||
/// Functions to manipulate small group members
|
||||
module Members =
|
||||
|
||||
/// Count members for the given small group
|
||||
let countByGroup (groupId : SmallGroupId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT COUNT(id) AS mbr_count FROM pt.member WHERE small_group_id = @groupId"
|
||||
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ]
|
||||
|> Sql.executeRowAsync (fun row -> row.int "mbr_count")
|
||||
let countByGroup (groupId : SmallGroupId) =
|
||||
Custom.scalar "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")
|
||||
|
||||
/// Delete a small group member by its ID
|
||||
let deleteById (memberId : MemberId) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "DELETE FROM pt.member WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.uuid memberId.Value ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
let deleteById (memberId : MemberId) =
|
||||
Custom.nonQuery "DELETE FROM pt.member WHERE id = @id" [ "@id", Sql.uuid memberId.Value ]
|
||||
|
||||
/// Retrieve all members for a given small group
|
||||
let forGroup (groupId : SmallGroupId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT * FROM pt.member WHERE small_group_id = @groupId ORDER BY member_name"
|
||||
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ]
|
||||
|> Sql.executeAsync mapToMember
|
||||
let forGroup (groupId : SmallGroupId) =
|
||||
Custom.list "SELECT * FROM pt.member WHERE small_group_id = @groupId ORDER BY member_name"
|
||||
[ "@groupId", Sql.uuid groupId.Value ] mapToMember
|
||||
|
||||
/// Save a small group member
|
||||
let save (mbr : Member) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
INSERT INTO pt.member (
|
||||
id, small_group_id, member_name, email, email_format
|
||||
) VALUES (
|
||||
@id, @groupId, @name, @email, @format
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET member_name = EXCLUDED.member_name,
|
||||
email = EXCLUDED.email,
|
||||
email_format = EXCLUDED.email_format"""
|
||||
|> Sql.parameters
|
||||
[ "@id", Sql.uuid mbr.Id.Value
|
||||
"@groupId", Sql.uuid mbr.SmallGroupId.Value
|
||||
"@name", Sql.string mbr.Name
|
||||
"@email", Sql.string mbr.Email
|
||||
"@format", Sql.stringOrNone (mbr.Format |> Option.map EmailFormat.toCode) ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
let save (mbr : Member) =
|
||||
Custom.nonQuery
|
||||
"INSERT INTO pt.member (
|
||||
id, small_group_id, member_name, email, email_format
|
||||
) VALUES (
|
||||
@id, @groupId, @name, @email, @format
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET member_name = EXCLUDED.member_name,
|
||||
email = EXCLUDED.email,
|
||||
email_format = EXCLUDED.email_format"
|
||||
[ "@id", Sql.uuid mbr.Id.Value
|
||||
"@groupId", Sql.uuid mbr.SmallGroupId.Value
|
||||
"@name", Sql.string mbr.Name
|
||||
"@email", Sql.string mbr.Email
|
||||
"@format", Sql.stringOrNone (mbr.Format |> Option.map EmailFormat.toCode) ]
|
||||
|
||||
/// Retrieve a small group member by its ID
|
||||
let tryById (memberId : MemberId) conn = backgroundTask {
|
||||
let! mbr =
|
||||
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
|
||||
}
|
||||
let tryById (memberId : MemberId) =
|
||||
Custom.single "SELECT * FROM pt.member WHERE id = @id" [ "@id", Sql.uuid memberId.Value ] mapToMember
|
||||
|
||||
|
||||
/// Options to retrieve a list of requests
|
||||
@@ -258,34 +225,24 @@ module PrayerRequests =
|
||||
if pageNbr > 0 then $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" else ""
|
||||
|
||||
/// Count the number of prayer requests for a church
|
||||
let countByChurch (churchId : ChurchId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT COUNT(id) AS req_count
|
||||
FROM pt.prayer_request
|
||||
WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)"""
|
||||
|> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ]
|
||||
|> Sql.executeRowAsync (fun row -> row.int "req_count")
|
||||
let countByChurch (churchId : ChurchId) =
|
||||
Custom.scalar
|
||||
"SELECT COUNT(id) AS req_count
|
||||
FROM pt.prayer_request
|
||||
WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)"
|
||||
[ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "req_count")
|
||||
|
||||
/// Count the number of prayer requests for a small group
|
||||
let countByGroup (groupId : SmallGroupId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT COUNT(id) AS req_count FROM pt.prayer_request WHERE small_group_id = @groupId"
|
||||
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ]
|
||||
|> Sql.executeRowAsync (fun row -> row.int "req_count")
|
||||
let countByGroup (groupId : SmallGroupId) =
|
||||
Custom.scalar "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")
|
||||
|
||||
/// Delete a prayer request by its ID
|
||||
let deleteById (reqId : PrayerRequestId) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "DELETE FROM pt.prayer_request WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.uuid reqId.Value ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
let deleteById (reqId : PrayerRequestId) =
|
||||
Custom.nonQuery "DELETE FROM pt.prayer_request WHERE id = @id" [ "@id", Sql.uuid reqId.Value ]
|
||||
|
||||
/// 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 where, parameters =
|
||||
if opts.ActiveOnly then
|
||||
@@ -294,198 +251,167 @@ module PrayerRequests =
|
||||
(theDate.AtStartOfDayInZone(SmallGroup.timeZone opts.SmallGroup)
|
||||
- Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire)
|
||||
.ToInstant ())
|
||||
""" AND ( updated_date > @asOf
|
||||
" AND ( updated_date > @asOf
|
||||
OR expiration = @manual
|
||||
OR request_type = @longTerm
|
||||
OR request_type = @expecting)
|
||||
AND expiration <> @forced""",
|
||||
AND expiration <> @forced",
|
||||
[ "@asOf", Sql.parameter asOf
|
||||
"@manual", Sql.string (Expiration.toCode Manual)
|
||||
"@longTerm", Sql.string (PrayerRequestType.toCode LongTermRequest)
|
||||
"@expecting", Sql.string (PrayerRequestType.toCode Expecting)
|
||||
"@forced", Sql.string (Expiration.toCode Forced) ]
|
||||
else "", []
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query $"""
|
||||
SELECT *
|
||||
FROM pt.prayer_request
|
||||
WHERE small_group_id = @groupId {where}
|
||||
ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort}
|
||||
{paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}"""
|
||||
|> Sql.parameters (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters)
|
||||
|> Sql.executeAsync mapToPrayerRequest
|
||||
Custom.list
|
||||
$"SELECT *
|
||||
FROM pt.prayer_request
|
||||
WHERE small_group_id = @groupId {where}
|
||||
ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort}
|
||||
{paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}"
|
||||
(("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) mapToPrayerRequest
|
||||
|
||||
/// Save a prayer request
|
||||
let save (req : PrayerRequest) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
INSERT into pt.prayer_request (
|
||||
id, request_type, user_id, small_group_id, entered_date, updated_date, requestor, request_text,
|
||||
notify_chaplain, expiration
|
||||
) VALUES (
|
||||
@id, @type, @userId, @groupId, @entered, @updated, @requestor, @text,
|
||||
@notifyChaplain, @expiration
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET request_type = EXCLUDED.request_type,
|
||||
updated_date = EXCLUDED.updated_date,
|
||||
requestor = EXCLUDED.requestor,
|
||||
request_text = EXCLUDED.request_text,
|
||||
notify_chaplain = EXCLUDED.notify_chaplain,
|
||||
expiration = EXCLUDED.expiration"""
|
||||
|> Sql.parameters
|
||||
[ "@id", Sql.uuid req.Id.Value
|
||||
"@type", Sql.string (PrayerRequestType.toCode req.RequestType)
|
||||
"@userId", Sql.uuid req.UserId.Value
|
||||
"@groupId", Sql.uuid req.SmallGroupId.Value
|
||||
"@entered", Sql.parameter (NpgsqlParameter ("@entered", req.EnteredDate))
|
||||
"@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate))
|
||||
"@requestor", Sql.stringOrNone req.Requestor
|
||||
"@text", Sql.string req.Text
|
||||
"@notifyChaplain", Sql.bool req.NotifyChaplain
|
||||
"@expiration", Sql.string (Expiration.toCode req.Expiration)
|
||||
]
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
let save (req : PrayerRequest) =
|
||||
Custom.nonQuery
|
||||
"INSERT into pt.prayer_request (
|
||||
id, request_type, user_id, small_group_id, entered_date, updated_date, requestor, request_text,
|
||||
notify_chaplain, expiration
|
||||
) VALUES (
|
||||
@id, @type, @userId, @groupId, @entered, @updated, @requestor, @text,
|
||||
@notifyChaplain, @expiration
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET request_type = EXCLUDED.request_type,
|
||||
updated_date = EXCLUDED.updated_date,
|
||||
requestor = EXCLUDED.requestor,
|
||||
request_text = EXCLUDED.request_text,
|
||||
notify_chaplain = EXCLUDED.notify_chaplain,
|
||||
expiration = EXCLUDED.expiration"
|
||||
[ "@id", Sql.uuid req.Id.Value
|
||||
"@type", Sql.string (PrayerRequestType.toCode req.RequestType)
|
||||
"@userId", Sql.uuid req.UserId.Value
|
||||
"@groupId", Sql.uuid req.SmallGroupId.Value
|
||||
"@entered", Sql.parameter (NpgsqlParameter ("@entered", req.EnteredDate))
|
||||
"@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate))
|
||||
"@requestor", Sql.stringOrNone req.Requestor
|
||||
"@text", Sql.string req.Text
|
||||
"@notifyChaplain", Sql.bool req.NotifyChaplain
|
||||
"@expiration", Sql.string (Expiration.toCode req.Expiration) ]
|
||||
|
||||
/// Search prayer requests for the given term
|
||||
let searchForGroup group searchTerm pageNbr conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query $"""
|
||||
SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND request_text ILIKE @search
|
||||
UNION
|
||||
SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search
|
||||
ORDER BY {orderBy group.Preferences.RequestSort}
|
||||
{paginate pageNbr group.Preferences.PageSize}"""
|
||||
|> Sql.parameters [ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ]
|
||||
|> Sql.executeAsync mapToPrayerRequest
|
||||
let searchForGroup group searchTerm pageNbr =
|
||||
Custom.list
|
||||
$"SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND request_text ILIKE @search
|
||||
UNION
|
||||
SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search
|
||||
ORDER BY {orderBy group.Preferences.RequestSort}
|
||||
{paginate pageNbr group.Preferences.PageSize}"
|
||||
[ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ] mapToPrayerRequest
|
||||
|
||||
/// Retrieve a prayer request by its ID
|
||||
let tryById (reqId : PrayerRequestId) conn = backgroundTask {
|
||||
let! req =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT * FROM pt.prayer_request WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.uuid reqId.Value ]
|
||||
|> Sql.executeAsync mapToPrayerRequest
|
||||
return List.tryHead req
|
||||
}
|
||||
let tryById (reqId : PrayerRequestId) =
|
||||
Custom.single "SELECT * FROM pt.prayer_request WHERE id = @id" [ "@id", Sql.uuid reqId.Value ]
|
||||
mapToPrayerRequest
|
||||
|
||||
/// Update the expiration for the given prayer request
|
||||
let updateExpiration (req : PrayerRequest) withTime conn = backgroundTask {
|
||||
let updateExpiration (req : PrayerRequest) withTime =
|
||||
let sql, parameters =
|
||||
if withTime then
|
||||
", updated_date = @updated",
|
||||
[ "@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate)) ]
|
||||
else "", []
|
||||
let! _ =
|
||||
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)
|
||||
"@id", Sql.uuid req.Id.Value ]
|
||||
|> List.append parameters)
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
Custom.nonQuery $"UPDATE pt.prayer_request SET expiration = @expiration{sql} WHERE id = @id"
|
||||
([ "@expiration", Sql.string (Expiration.toCode req.Expiration)
|
||||
"@id", Sql.uuid req.Id.Value ]
|
||||
|> List.append parameters)
|
||||
|
||||
|
||||
/// Functions to retrieve small group information
|
||||
module SmallGroups =
|
||||
|
||||
/// Count the number of small groups for a church
|
||||
let countByChurch (churchId : ChurchId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT COUNT(id) AS group_count FROM pt.small_group WHERE church_id = @churchId"
|
||||
|> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ]
|
||||
|> Sql.executeRowAsync (fun row -> row.int "group_count")
|
||||
let countByChurch (churchId : ChurchId) =
|
||||
Custom.scalar "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")
|
||||
|
||||
/// 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! _ =
|
||||
Sql.existingConnection conn
|
||||
Configuration.dataSource ()
|
||||
|> Sql.fromDataSource
|
||||
|> Sql.executeTransactionAsync
|
||||
[ "DELETE FROM pt.prayer_request WHERE small_group_id = @groupId", idParam
|
||||
"DELETE FROM pt.user_small_group WHERE small_group_id = @groupId", idParam
|
||||
"DELETE FROM pt.list_preference WHERE small_group_id = @groupId", idParam
|
||||
"DELETE FROM pt.small_group WHERE id = @groupId", idParam ]
|
||||
return ()
|
||||
()
|
||||
}
|
||||
|
||||
/// Get information for all small groups
|
||||
let infoForAll conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT sg.id, sg.group_name, c.church_name, lp.time_zone_id, lp.is_public
|
||||
FROM pt.small_group sg
|
||||
INNER JOIN pt.church c ON c.id = sg.church_id
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id
|
||||
ORDER BY sg.group_name"""
|
||||
|> Sql.executeAsync mapToSmallGroupInfo
|
||||
let infoForAll () =
|
||||
Custom.list
|
||||
"SELECT sg.id, sg.group_name, c.church_name, lp.time_zone_id, lp.is_public
|
||||
FROM pt.small_group sg
|
||||
INNER JOIN pt.church c ON c.id = sg.church_id
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id
|
||||
ORDER BY sg.group_name"
|
||||
[] mapToSmallGroupInfo
|
||||
|
||||
/// Get a list of small group IDs along with a description that includes the church name
|
||||
let listAll conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT g.group_name, g.id, c.church_name
|
||||
FROM pt.small_group g
|
||||
INNER JOIN pt.church c ON c.id = g.church_id
|
||||
ORDER BY c.church_name, g.group_name"""
|
||||
|> Sql.executeAsync mapToSmallGroupItem
|
||||
let listAll () =
|
||||
Custom.list
|
||||
"SELECT g.group_name, g.id, c.church_name
|
||||
FROM pt.small_group g
|
||||
INNER JOIN pt.church c ON c.id = g.church_id
|
||||
ORDER BY c.church_name, g.group_name"
|
||||
[] mapToSmallGroupItem
|
||||
|
||||
/// Get a list of small group IDs and descriptions for groups with a group password
|
||||
let listProtected conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT g.group_name, g.id, c.church_name, lp.is_public
|
||||
FROM pt.small_group g
|
||||
INNER JOIN pt.church c ON c.id = g.church_id
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id
|
||||
WHERE COALESCE(lp.group_password, '') <> ''
|
||||
ORDER BY c.church_name, g.group_name"""
|
||||
|> Sql.executeAsync mapToSmallGroupItem
|
||||
let listProtected () =
|
||||
Custom.list
|
||||
"SELECT g.group_name, g.id, c.church_name, lp.is_public
|
||||
FROM pt.small_group g
|
||||
INNER JOIN pt.church c ON c.id = g.church_id
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id
|
||||
WHERE COALESCE(lp.group_password, '') <> ''
|
||||
ORDER BY c.church_name, g.group_name"
|
||||
[] mapToSmallGroupItem
|
||||
|
||||
/// Get a list of small group IDs and descriptions for groups that are public or have a group password
|
||||
let listPublicAndProtected conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT g.group_name, g.id, c.church_name, lp.time_zone_id, lp.is_public
|
||||
FROM pt.small_group g
|
||||
INNER JOIN pt.church c ON c.id = g.church_id
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id
|
||||
WHERE lp.is_public = TRUE
|
||||
OR COALESCE(lp.group_password, '') <> ''
|
||||
ORDER BY c.church_name, g.group_name"""
|
||||
|> Sql.executeAsync mapToSmallGroupInfo
|
||||
let listPublicAndProtected () =
|
||||
Custom.list
|
||||
"SELECT g.group_name, g.id, c.church_name, lp.time_zone_id, lp.is_public
|
||||
FROM pt.small_group g
|
||||
INNER JOIN pt.church c ON c.id = g.church_id
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id
|
||||
WHERE lp.is_public = TRUE
|
||||
OR COALESCE(lp.group_password, '') <> ''
|
||||
ORDER BY c.church_name, g.group_name"
|
||||
[] mapToSmallGroupInfo
|
||||
|
||||
/// Log on for a small group (includes list preferences)
|
||||
let logOn (groupId : SmallGroupId) password conn = backgroundTask {
|
||||
let! group =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT sg.*, lp.*
|
||||
FROM pt.small_group sg
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id
|
||||
WHERE sg.id = @id
|
||||
AND lp.group_password = @password"""
|
||||
|> Sql.parameters [ "@id", Sql.uuid groupId.Value; "@password", Sql.string password ]
|
||||
|> Sql.executeAsync mapToSmallGroupWithPreferences
|
||||
return List.tryHead group
|
||||
}
|
||||
let logOn (groupId : SmallGroupId) password =
|
||||
Custom.single
|
||||
"SELECT sg.*, lp.*
|
||||
FROM pt.small_group sg
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id
|
||||
WHERE sg.id = @id
|
||||
AND lp.group_password = @password"
|
||||
[ "@id", Sql.uuid groupId.Value; "@password", Sql.string password ] mapToSmallGroupWithPreferences
|
||||
|
||||
/// Save a small group
|
||||
let save (group : SmallGroup) isNew conn = backgroundTask {
|
||||
let save (group : SmallGroup) isNew = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
Configuration.dataSource ()
|
||||
|> Sql.fromDataSource
|
||||
|> Sql.executeTransactionAsync [
|
||||
""" INSERT INTO pt.small_group (
|
||||
"INSERT INTO pt.small_group (
|
||||
id, church_id, group_name
|
||||
) VALUES (
|
||||
@id, @churchId, @name
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET church_id = EXCLUDED.church_id,
|
||||
group_name = EXCLUDED.group_name""",
|
||||
) VALUES (
|
||||
@id, @churchId, @name
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET church_id = EXCLUDED.church_id,
|
||||
group_name = EXCLUDED.group_name",
|
||||
[ [ "@id", Sql.uuid group.Id.Value
|
||||
"@churchId", Sql.uuid group.ChurchId.Value
|
||||
"@name", Sql.string group.Name ] ]
|
||||
@@ -493,216 +419,154 @@ module SmallGroups =
|
||||
"INSERT INTO pt.list_preference (small_group_id) VALUES (@id)",
|
||||
[ [ "@id", Sql.uuid group.Id.Value ] ]
|
||||
]
|
||||
return ()
|
||||
()
|
||||
}
|
||||
|
||||
/// Save a small group's list preferences
|
||||
let savePreferences (pref : ListPreferences) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
UPDATE pt.list_preference
|
||||
SET days_to_keep_new = @daysToKeepNew,
|
||||
days_to_expire = @daysToExpire,
|
||||
long_term_update_weeks = @longTermUpdateWeeks,
|
||||
email_from_name = @emailFromName,
|
||||
email_from_address = @emailFromAddress,
|
||||
fonts = @fonts,
|
||||
heading_color = @headingColor,
|
||||
line_color = @lineColor,
|
||||
heading_font_size = @headingFontSize,
|
||||
text_font_size = @textFontSize,
|
||||
request_sort = @requestSort,
|
||||
group_password = @groupPassword,
|
||||
default_email_type = @defaultEmailType,
|
||||
is_public = @isPublic,
|
||||
time_zone_id = @timeZoneId,
|
||||
page_size = @pageSize,
|
||||
as_of_date_display = @asOfDateDisplay
|
||||
WHERE small_group_id = @groupId"""
|
||||
|> Sql.parameters
|
||||
[ "@groupId", Sql.uuid pref.SmallGroupId.Value
|
||||
"@daysToKeepNew", Sql.int pref.DaysToKeepNew
|
||||
"@daysToExpire", Sql.int pref.DaysToExpire
|
||||
"@longTermUpdateWeeks", Sql.int pref.LongTermUpdateWeeks
|
||||
"@emailFromName", Sql.string pref.EmailFromName
|
||||
"@emailFromAddress", Sql.string pref.EmailFromAddress
|
||||
"@fonts", Sql.string pref.Fonts
|
||||
"@headingColor", Sql.string pref.HeadingColor
|
||||
"@lineColor", Sql.string pref.LineColor
|
||||
"@headingFontSize", Sql.int pref.HeadingFontSize
|
||||
"@textFontSize", Sql.int pref.TextFontSize
|
||||
"@requestSort", Sql.string (RequestSort.toCode pref.RequestSort)
|
||||
"@groupPassword", Sql.string pref.GroupPassword
|
||||
"@defaultEmailType", Sql.string (EmailFormat.toCode pref.DefaultEmailType)
|
||||
"@isPublic", Sql.bool pref.IsPublic
|
||||
"@timeZoneId", Sql.string (TimeZoneId.toString pref.TimeZoneId)
|
||||
"@pageSize", Sql.int pref.PageSize
|
||||
"@asOfDateDisplay", Sql.string (AsOfDateDisplay.toCode pref.AsOfDateDisplay)
|
||||
]
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
let savePreferences (pref : ListPreferences) =
|
||||
Custom.nonQuery
|
||||
"UPDATE pt.list_preference
|
||||
SET days_to_keep_new = @daysToKeepNew,
|
||||
days_to_expire = @daysToExpire,
|
||||
long_term_update_weeks = @longTermUpdateWeeks,
|
||||
email_from_name = @emailFromName,
|
||||
email_from_address = @emailFromAddress,
|
||||
fonts = @fonts,
|
||||
heading_color = @headingColor,
|
||||
line_color = @lineColor,
|
||||
heading_font_size = @headingFontSize,
|
||||
text_font_size = @textFontSize,
|
||||
request_sort = @requestSort,
|
||||
group_password = @groupPassword,
|
||||
default_email_type = @defaultEmailType,
|
||||
is_public = @isPublic,
|
||||
time_zone_id = @timeZoneId,
|
||||
page_size = @pageSize,
|
||||
as_of_date_display = @asOfDateDisplay
|
||||
WHERE small_group_id = @groupId"
|
||||
[ "@groupId", Sql.uuid pref.SmallGroupId.Value
|
||||
"@daysToKeepNew", Sql.int pref.DaysToKeepNew
|
||||
"@daysToExpire", Sql.int pref.DaysToExpire
|
||||
"@longTermUpdateWeeks", Sql.int pref.LongTermUpdateWeeks
|
||||
"@emailFromName", Sql.string pref.EmailFromName
|
||||
"@emailFromAddress", Sql.string pref.EmailFromAddress
|
||||
"@fonts", Sql.string pref.Fonts
|
||||
"@headingColor", Sql.string pref.HeadingColor
|
||||
"@lineColor", Sql.string pref.LineColor
|
||||
"@headingFontSize", Sql.int pref.HeadingFontSize
|
||||
"@textFontSize", Sql.int pref.TextFontSize
|
||||
"@requestSort", Sql.string (RequestSort.toCode pref.RequestSort)
|
||||
"@groupPassword", Sql.string pref.GroupPassword
|
||||
"@defaultEmailType", Sql.string (EmailFormat.toCode pref.DefaultEmailType)
|
||||
"@isPublic", Sql.bool pref.IsPublic
|
||||
"@timeZoneId", Sql.string (TimeZoneId.toString pref.TimeZoneId)
|
||||
"@pageSize", Sql.int pref.PageSize
|
||||
"@asOfDateDisplay", Sql.string (AsOfDateDisplay.toCode pref.AsOfDateDisplay) ]
|
||||
|
||||
/// Get a small group by its ID
|
||||
let tryById (groupId : SmallGroupId) conn = backgroundTask {
|
||||
let! group =
|
||||
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
|
||||
}
|
||||
let tryById (groupId : SmallGroupId) =
|
||||
Custom.single "SELECT * FROM pt.small_group WHERE id = @id" [ "@id", Sql.uuid groupId.Value ] mapToSmallGroup
|
||||
|
||||
/// Get a small group by its ID with its list preferences populated
|
||||
let tryByIdWithPreferences (groupId : SmallGroupId) conn = backgroundTask {
|
||||
let! group =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT sg.*, lp.*
|
||||
FROM pt.small_group sg
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id
|
||||
WHERE sg.id = @id"""
|
||||
|> Sql.parameters [ "@id", Sql.uuid groupId.Value ]
|
||||
|> Sql.executeAsync mapToSmallGroupWithPreferences
|
||||
return List.tryHead group
|
||||
}
|
||||
let tryByIdWithPreferences (groupId : SmallGroupId) =
|
||||
Custom.single
|
||||
"SELECT sg.*, lp.*
|
||||
FROM pt.small_group sg
|
||||
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id
|
||||
WHERE sg.id = @id"
|
||||
[ "@id", Sql.uuid groupId.Value ] mapToSmallGroupWithPreferences
|
||||
|
||||
|
||||
/// Functions to manipulate users
|
||||
module Users =
|
||||
|
||||
/// Retrieve all PrayerTracker users
|
||||
let all conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT * FROM pt.pt_user ORDER BY last_name, first_name"
|
||||
|> Sql.executeAsync mapToUser
|
||||
let all () =
|
||||
Custom.list "SELECT * FROM pt.pt_user ORDER BY last_name, first_name" [] mapToUser
|
||||
|
||||
/// Count the number of users for a church
|
||||
let countByChurch (churchId : ChurchId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT COUNT(u.id) AS user_count
|
||||
FROM pt.pt_user u
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM pt.user_small_group usg
|
||||
INNER JOIN pt.small_group sg ON sg.id = usg.small_group_id
|
||||
WHERE usg.user_id = u.id
|
||||
AND sg.church_id = @churchId)"""
|
||||
|> Sql.parameters [ "@churchId", Sql.uuid churchId.Value ]
|
||||
|> Sql.executeRowAsync (fun row -> row.int "user_count")
|
||||
let countByChurch (churchId : ChurchId) =
|
||||
Custom.scalar
|
||||
"SELECT COUNT(u.id) AS user_count
|
||||
FROM pt.pt_user u
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM pt.user_small_group usg
|
||||
INNER JOIN pt.small_group sg ON sg.id = usg.small_group_id
|
||||
WHERE usg.user_id = u.id
|
||||
AND sg.church_id = @churchId)"
|
||||
[ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "user_count")
|
||||
|
||||
/// Count the number of users for a small group
|
||||
let countByGroup (groupId : SmallGroupId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT COUNT(user_id) AS user_count FROM pt.user_small_group WHERE small_group_id = @groupId"
|
||||
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ]
|
||||
|> Sql.executeRowAsync (fun row -> row.int "user_count")
|
||||
let countByGroup (groupId : SmallGroupId) =
|
||||
Custom.scalar "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")
|
||||
|
||||
/// Delete a user by its database ID
|
||||
let deleteById (userId : UserId) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "DELETE FROM pt.pt_user WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.uuid userId.Value ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
let deleteById (userId : UserId) =
|
||||
Custom.nonQuery "DELETE FROM pt.pt_user WHERE id = @id" [ "@id", Sql.uuid userId.Value ]
|
||||
|
||||
/// Get the IDs of the small groups for which the given user is authorized
|
||||
let groupIdsByUserId (userId : UserId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT small_group_id FROM pt.user_small_group WHERE user_id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.uuid userId.Value ]
|
||||
|> Sql.executeAsync (fun row -> SmallGroupId (row.uuid "small_group_id"))
|
||||
let groupIdsByUserId (userId : UserId) =
|
||||
Custom.list "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"))
|
||||
|
||||
/// Get a list of users authorized to administer the given small group
|
||||
let listByGroupId (groupId : SmallGroupId) conn =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT u.*
|
||||
FROM pt.pt_user u
|
||||
INNER JOIN pt.user_small_group usg ON usg.user_id = u.id
|
||||
WHERE usg.small_group_id = @groupId
|
||||
ORDER BY u.last_name, u.first_name"""
|
||||
|> Sql.parameters [ "@groupId", Sql.uuid groupId.Value ]
|
||||
|> Sql.executeAsync mapToUser
|
||||
let listByGroupId (groupId : SmallGroupId) =
|
||||
Custom.list
|
||||
"SELECT u.*
|
||||
FROM pt.pt_user u
|
||||
INNER JOIN pt.user_small_group usg ON usg.user_id = u.id
|
||||
WHERE usg.small_group_id = @groupId
|
||||
ORDER BY u.last_name, u.first_name"
|
||||
[ "@groupId", Sql.uuid groupId.Value ] mapToUser
|
||||
|
||||
/// Save a user's information
|
||||
let save (user : User) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
INSERT INTO pt.pt_user (
|
||||
id, first_name, last_name, email, is_admin, password_hash
|
||||
) VALUES (
|
||||
@id, @firstName, @lastName, @email, @isAdmin, @passwordHash
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
email = EXCLUDED.email,
|
||||
is_admin = EXCLUDED.is_admin,
|
||||
password_hash = EXCLUDED.password_hash"""
|
||||
|> Sql.parameters
|
||||
[ "@id", Sql.uuid user.Id.Value
|
||||
"@firstName", Sql.string user.FirstName
|
||||
"@lastName", Sql.string user.LastName
|
||||
"@email", Sql.string user.Email
|
||||
"@isAdmin", Sql.bool user.IsAdmin
|
||||
"@passwordHash", Sql.string user.PasswordHash
|
||||
]
|
||||
|> Sql.executeNonQueryAsync
|
||||
return ()
|
||||
}
|
||||
let save (user : User) =
|
||||
Custom.nonQuery
|
||||
"INSERT INTO pt.pt_user (
|
||||
id, first_name, last_name, email, is_admin, password_hash
|
||||
) VALUES (
|
||||
@id, @firstName, @lastName, @email, @isAdmin, @passwordHash
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
email = EXCLUDED.email,
|
||||
is_admin = EXCLUDED.is_admin,
|
||||
password_hash = EXCLUDED.password_hash"
|
||||
[ "@id", Sql.uuid user.Id.Value
|
||||
"@firstName", Sql.string user.FirstName
|
||||
"@lastName", Sql.string user.LastName
|
||||
"@email", Sql.string user.Email
|
||||
"@isAdmin", Sql.bool user.IsAdmin
|
||||
"@passwordHash", Sql.string user.PasswordHash ]
|
||||
|
||||
/// Find a user by its e-mail address and authorized small group
|
||||
let tryByEmailAndGroup email (groupId : SmallGroupId) conn = backgroundTask {
|
||||
let! user =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query """
|
||||
SELECT 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
|
||||
WHERE u.email = @email"""
|
||||
|> Sql.parameters [ "@email", Sql.string email; "@groupId", Sql.uuid groupId.Value ]
|
||||
|> Sql.executeAsync mapToUser
|
||||
return List.tryHead user
|
||||
}
|
||||
let tryByEmailAndGroup email (groupId : SmallGroupId) =
|
||||
Custom.single
|
||||
"SELECT 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
|
||||
WHERE u.email = @email"
|
||||
[ "@email", Sql.string email; "@groupId", Sql.uuid groupId.Value ] mapToUser
|
||||
|
||||
/// Find a user by their database ID
|
||||
let tryById (userId : UserId) conn = backgroundTask {
|
||||
let! user =
|
||||
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
|
||||
}
|
||||
let tryById (userId : UserId) =
|
||||
Custom.single "SELECT * FROM pt.pt_user WHERE id = @id" [ "@id", Sql.uuid userId.Value ] mapToUser
|
||||
|
||||
/// Update a user's last seen date/time
|
||||
let updateLastSeen (userId : UserId) (now : Instant) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> 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 ()
|
||||
}
|
||||
let updateLastSeen (userId : UserId) (now : Instant) =
|
||||
Custom.nonQuery "UPDATE pt.pt_user SET last_seen = @now WHERE id = @id"
|
||||
[ "@id", Sql.uuid userId.Value; "@now", Sql.parameter (NpgsqlParameter ("@now", now)) ]
|
||||
|
||||
/// Update a user's password hash
|
||||
let updatePassword (user : User) conn = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> 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 ()
|
||||
}
|
||||
let updatePassword (user : User) =
|
||||
Custom.nonQuery "UPDATE pt.pt_user SET password_hash = @passwordHash WHERE id = @id"
|
||||
[ "@id", Sql.uuid user.Id.Value; "@passwordHash", Sql.string user.PasswordHash ]
|
||||
|
||||
/// Update a user's authorized small groups
|
||||
let updateSmallGroups (userId : UserId) groupIds conn = backgroundTask {
|
||||
let! existingGroupIds = groupIdsByUserId userId conn
|
||||
let updateSmallGroups (userId : UserId) groupIds = backgroundTask {
|
||||
let! existingGroupIds = groupIdsByUserId userId
|
||||
let toAdd =
|
||||
groupIds |> List.filter (fun it -> existingGroupIds |> List.exists (fun grpId -> grpId = it) |> not)
|
||||
let toDelete =
|
||||
@@ -718,7 +582,8 @@ module Users =
|
||||
}
|
||||
if not (Seq.isEmpty queries) then
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
Configuration.dataSource ()
|
||||
|> Sql.fromDataSource
|
||||
|> Sql.executeTransactionAsync (List.ofSeq queries)
|
||||
()
|
||||
}
|
||||
|
||||
@@ -47,33 +47,30 @@ module private CacheHelpers =
|
||||
p.ParameterName, Sql.parameter p
|
||||
|
||||
|
||||
open BitBadger.Npgsql.FSharp.Documents
|
||||
|
||||
/// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog
|
||||
type DistributedCache (connStr : string) =
|
||||
type DistributedCache () =
|
||||
|
||||
// ~~~ INITIALIZATION ~~~
|
||||
|
||||
do
|
||||
task {
|
||||
let! exists =
|
||||
Sql.connect connStr
|
||||
|> Sql.query $"
|
||||
SELECT EXISTS
|
||||
(SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session')
|
||||
AS does_exist"
|
||||
|> Sql.executeRowAsync (fun row -> row.bool "does_exist")
|
||||
Custom.scalar
|
||||
$"SELECT EXISTS
|
||||
(SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session')
|
||||
AS does_exist"
|
||||
[] (fun row -> row.bool "does_exist")
|
||||
if not exists then
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query
|
||||
do! Custom.nonQuery
|
||||
"CREATE TABLE session (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
payload BYTEA NOT NULL,
|
||||
expire_at TIMESTAMPTZ NOT NULL,
|
||||
sliding_expiration INTERVAL,
|
||||
absolute_expiration TIMESTAMPTZ);
|
||||
CREATE INDEX idx_session_expiration ON session (expire_at)"
|
||||
|> Sql.executeNonQueryAsync
|
||||
()
|
||||
CREATE INDEX idx_session_expiration ON session (expire_at)" []
|
||||
} |> sync
|
||||
|
||||
// ~~~ SUPPORT FUNCTIONS ~~~
|
||||
@@ -82,16 +79,14 @@ type DistributedCache (connStr : string) =
|
||||
let getEntry key = backgroundTask {
|
||||
let idParam = "@id", Sql.string key
|
||||
let! tryEntry =
|
||||
Sql.connect connStr
|
||||
|> Sql.query "SELECT * FROM session WHERE id = @id"
|
||||
|> Sql.parameters [ idParam ]
|
||||
|> Sql.executeAsync (fun row ->
|
||||
{ Id = row.string "id"
|
||||
Payload = row.bytea "payload"
|
||||
ExpireAt = row.fieldValue<Instant> "expire_at"
|
||||
SlidingExpiration = row.fieldValueOrNone<Duration> "sliding_expiration"
|
||||
AbsoluteExpiration = row.fieldValueOrNone<Instant> "absolute_expiration" })
|
||||
match List.tryHead tryEntry with
|
||||
Custom.single "SELECT * FROM session WHERE id = @id" [ idParam ]
|
||||
(fun row ->
|
||||
{ Id = row.string "id"
|
||||
Payload = row.bytea "payload"
|
||||
ExpireAt = row.fieldValue<Instant> "expire_at"
|
||||
SlidingExpiration = row.fieldValueOrNone<Duration> "sliding_expiration"
|
||||
AbsoluteExpiration = row.fieldValueOrNone<Instant> "absolute_expiration" })
|
||||
match tryEntry with
|
||||
| Some entry ->
|
||||
let now = getNow ()
|
||||
let slideExp = defaultArg entry.SlidingExpiration Duration.MinValue
|
||||
@@ -103,12 +98,8 @@ type DistributedCache (connStr : string) =
|
||||
true, { entry with ExpireAt = absExp }
|
||||
else true, { entry with ExpireAt = now.Plus slideExp }
|
||||
if needsRefresh then
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|
||||
|> Sql.parameters [ expireParam item.ExpireAt; idParam ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
()
|
||||
do! Custom.nonQuery "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|
||||
[ expireParam item.ExpireAt; idParam ]
|
||||
return if item.ExpireAt > now then Some entry else None
|
||||
| None -> return None
|
||||
}
|
||||
@@ -120,26 +111,16 @@ type DistributedCache (connStr : string) =
|
||||
let purge () = backgroundTask {
|
||||
let now = getNow ()
|
||||
if lastPurge.Plus (Duration.FromMinutes 30L) < now then
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query "DELETE FROM session WHERE expire_at < @expireAt"
|
||||
|> Sql.parameters [ expireParam now ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
do! Custom.nonQuery "DELETE FROM session WHERE expire_at < @expireAt" [ expireParam now ]
|
||||
lastPurge <- now
|
||||
}
|
||||
|
||||
/// Remove a cache entry
|
||||
let removeEntry key = backgroundTask {
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query "DELETE FROM session WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.string key ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
()
|
||||
}
|
||||
let removeEntry key =
|
||||
Custom.nonQuery "DELETE FROM session WHERE id = @id" [ "@id", Sql.string key ]
|
||||
|
||||
/// Save an entry
|
||||
let saveEntry (opts : DistributedCacheEntryOptions) key payload = backgroundTask {
|
||||
let saveEntry (opts : DistributedCacheEntryOptions) key payload =
|
||||
let now = getNow ()
|
||||
let expireAt, slideExp, absExp =
|
||||
if opts.SlidingExpiration.HasValue then
|
||||
@@ -155,27 +136,21 @@ type DistributedCache (connStr : string) =
|
||||
// Default to 2 hour sliding expiration
|
||||
let slide = Duration.FromHours 2
|
||||
now.Plus slide, Some slide, None
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query
|
||||
"INSERT INTO session (
|
||||
id, payload, expire_at, sliding_expiration, absolute_expiration
|
||||
) VALUES (
|
||||
@id, @payload, @expireAt, @slideExp, @absExp
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET payload = EXCLUDED.payload,
|
||||
expire_at = EXCLUDED.expire_at,
|
||||
sliding_expiration = EXCLUDED.sliding_expiration,
|
||||
absolute_expiration = EXCLUDED.absolute_expiration"
|
||||
|> Sql.parameters
|
||||
[ "@id", Sql.string key
|
||||
"@payload", Sql.bytea payload
|
||||
expireParam expireAt
|
||||
optParam "slideExp" slideExp
|
||||
optParam "absExp" absExp ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
()
|
||||
}
|
||||
Custom.nonQuery
|
||||
"INSERT INTO session (
|
||||
id, payload, expire_at, sliding_expiration, absolute_expiration
|
||||
) VALUES (
|
||||
@id, @payload, @expireAt, @slideExp, @absExp
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET payload = EXCLUDED.payload,
|
||||
expire_at = EXCLUDED.expire_at,
|
||||
sliding_expiration = EXCLUDED.sliding_expiration,
|
||||
absolute_expiration = EXCLUDED.absolute_expiration"
|
||||
[ "@id", Sql.string key
|
||||
"@payload", Sql.bytea payload
|
||||
expireParam expireAt
|
||||
optParam "slideExp" slideExp
|
||||
optParam "absExp" absExp ]
|
||||
|
||||
// ~~~ IMPLEMENTATION FUNCTIONS ~~~
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Entities.fs" />
|
||||
<Compile Include="Access.fs" />
|
||||
@@ -11,11 +7,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BitBadger.Npgsql.FSharp.Documents" Version="1.0.0-beta3" />
|
||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||
<PackageReference Include="NodaTime" Version="3.1.2" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
<PackageReference Include="Npgsql.FSharp" Version="5.3.0" />
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="6.0.6" />
|
||||
<PackageReference Include="NodaTime" Version="3.1.9" />
|
||||
<PackageReference Include="Npgsql.FSharp" Version="5.7.0" />
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="7.0.4" />
|
||||
<PackageReference Update="FSharp.Core" Version="7.0.300" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -16,8 +15,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Expecto" Version="9.0.4" />
|
||||
<PackageReference Include="NodaTime.Testing" Version="3.1.2" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
<PackageReference Include="NodaTime.Testing" Version="3.1.9" />
|
||||
<PackageReference Update="FSharp.Core" Version="7.0.300" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -604,7 +604,7 @@ let requestListTests =
|
||||
}
|
||||
let html = htmlList.AsHtml _s
|
||||
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> – zyx<i style="font-size:9.60pt"> (as of %s)</i>"""
|
||||
// spot check; if one request has it, they all should
|
||||
Expect.stringContains html expected "Expected short as-of date not found"
|
||||
@@ -619,7 +619,7 @@ let requestListTests =
|
||||
}
|
||||
let html = htmlList.AsHtml _s
|
||||
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> – zyx<i style="font-size:9.60pt"> (as of %s)</i>"""
|
||||
// spot check; if one request has it, they all should
|
||||
Expect.stringContains html expected "Expected long as-of date not found"
|
||||
@@ -650,7 +650,7 @@ let requestListTests =
|
||||
}
|
||||
let text = textList.AsText _s
|
||||
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)"
|
||||
// spot check; if one request has it, they all should
|
||||
Expect.stringContains text expected "Expected short as-of date not found"
|
||||
@@ -665,7 +665,7 @@ let requestListTests =
|
||||
}
|
||||
let text = textList.AsText _s
|
||||
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)"
|
||||
// spot check; if one request has it, they all should
|
||||
Expect.stringContains text expected "Expected long as-of date not found"
|
||||
|
||||
@@ -19,4 +19,4 @@ let localizer = lazy (stringLocFactory.Create ("Common", resAsmName))
|
||||
|
||||
/// Get a view localizer
|
||||
let forView (view : string) =
|
||||
htmlLocFactory.Create ($"""Views.{view.Replace ('/', '.')}""", resAsmName)
|
||||
htmlLocFactory.Create ($"Views.{view.Replace ('/', '.')}", resAsmName)
|
||||
|
||||
@@ -304,14 +304,14 @@ let private contentSection viewInfo pgTitle (content : XmlNode) = [
|
||||
| Some onLoad ->
|
||||
let doCall = if onLoad.EndsWith ")" then "" else "()"
|
||||
script [] [
|
||||
rawText $"""
|
||||
rawText $"
|
||||
window.doOnLoad = () => {{
|
||||
if (window.PT) {{
|
||||
{onLoad}{doCall}
|
||||
delete window.doOnLoad
|
||||
}} else {{ setTimeout(window.doOnLoad, 500) }}
|
||||
}}
|
||||
window.doOnLoad()"""
|
||||
window.doOnLoad()"
|
||||
]
|
||||
| None -> ()
|
||||
]
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Utils.fs" />
|
||||
<Compile Include="ViewModels.fs" />
|
||||
@@ -18,16 +14,15 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
|
||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.0" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.9.2" />
|
||||
<PackageReference Include="MailKit" Version="4.1.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.Extensions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Update="FSharp.Core" Version="7.0.300" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
module PrayerTracker.Utils
|
||||
|
||||
open System
|
||||
open System.Security.Cryptography
|
||||
open System.Text
|
||||
|
||||
open Giraffe
|
||||
|
||||
/// Parse a short-GUID-based ID from a string
|
||||
|
||||
@@ -35,6 +35,7 @@ module Configure =
|
||||
(ctx.Configuration.GetSection >> opts.Configure >> ignore) "Kestrel"
|
||||
|
||||
open System.Globalization
|
||||
open BitBadger.Npgsql.FSharp.Documents
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
open Microsoft.AspNetCore.Localization
|
||||
open Microsoft.Extensions.Caching.Distributed
|
||||
@@ -63,21 +64,18 @@ module Configure =
|
||||
opts.SlidingExpiration <- true
|
||||
opts.AccessDeniedPath <- "/error/403")
|
||||
let _ = svc.AddAuthorization ()
|
||||
let _ =
|
||||
svc.AddSingleton<IDistributedCache> (fun sp ->
|
||||
let cfg = sp.GetService<IConfiguration> ()
|
||||
DistributedCache (cfg.GetConnectionString "PrayerTracker") :> IDistributedCache)
|
||||
|
||||
let cfg = svc.BuildServiceProvider().GetService<IConfiguration> ()
|
||||
let dsb = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PrayerTracker")
|
||||
let _ = dsb.UseNodaTime()
|
||||
Configuration.useDataSource (dsb.Build ())
|
||||
|
||||
let _ = svc.AddSingleton<IDistributedCache, DistributedCache> ()
|
||||
let _ = svc.AddSession ()
|
||||
let _ = svc.AddAntiforgery ()
|
||||
let _ = svc.AddRouting ()
|
||||
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
|
||||
@@ -214,47 +212,6 @@ module Configure =
|
||||
/// The web application
|
||||
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
|
||||
|
||||
[<EntryPoint>]
|
||||
@@ -270,8 +227,5 @@ module App =
|
||||
.ConfigureLogging(Configure.logging)
|
||||
.Configure(System.Action<IApplicationBuilder> Configure.app)
|
||||
.Build()
|
||||
if args.Length > 0 then
|
||||
if args[0] = "migrate-passwords" then migratePasswords app
|
||||
else printfn $"Unrecognized option {args[0]}"
|
||||
else app.Run ()
|
||||
if args.Length > 0 then printfn $"Unrecognized option {args[0]}" else app.Run ()
|
||||
0
|
||||
|
||||
@@ -8,21 +8,20 @@ open PrayerTracker.Entities
|
||||
open PrayerTracker.ViewModels
|
||||
|
||||
/// Find statistics for the given church
|
||||
let private findStats churchId conn = task {
|
||||
let! groups = SmallGroups.countByChurch churchId conn
|
||||
let! requests = PrayerRequests.countByChurch churchId conn
|
||||
let! users = Users.countByChurch churchId conn
|
||||
let private findStats churchId = task {
|
||||
let! groups = SmallGroups.countByChurch churchId
|
||||
let! requests = PrayerRequests.countByChurch churchId
|
||||
let! users = Users.countByChurch churchId
|
||||
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 churchId = ChurchId chId
|
||||
let conn = ctx.Conn
|
||||
match! Churches.tryById churchId conn with
|
||||
match! Churches.tryById churchId with
|
||||
| Some church ->
|
||||
let! _, stats = findStats churchId conn
|
||||
do! Churches.deleteById churchId conn
|
||||
let! _, stats = findStats churchId
|
||||
do! Churches.deleteById churchId
|
||||
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)",
|
||||
church.Name, stats.SmallGroups, stats.PrayerRequests, stats.Users]
|
||||
@@ -32,7 +31,7 @@ let delete chId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun
|
||||
|
||||
open System
|
||||
|
||||
/// GET /church/[church-id]/edit
|
||||
// GET /church/[church-id]/edit
|
||||
let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
|
||||
if churchId = Guid.Empty then
|
||||
return!
|
||||
@@ -40,7 +39,7 @@ let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> ta
|
||||
|> Views.Church.edit EditChurch.empty ctx
|
||||
|> renderHtml next ctx
|
||||
else
|
||||
match! Churches.tryById (ChurchId churchId) ctx.Conn with
|
||||
match! Churches.tryById (ChurchId churchId) with
|
||||
| Some church ->
|
||||
return!
|
||||
viewInfo ctx
|
||||
@@ -49,27 +48,26 @@ let edit churchId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> ta
|
||||
| None -> return! fourOhFour ctx
|
||||
}
|
||||
|
||||
/// GET /churches
|
||||
// GET /churches
|
||||
let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
|
||||
let conn = ctx.Conn
|
||||
let! churches = Churches.all conn
|
||||
let stats = churches |> List.map (fun c -> findStats c.Id conn |> Async.AwaitTask |> Async.RunSynchronously)
|
||||
let! churches = Churches.all ()
|
||||
let stats = churches |> List.map (fun c -> findStats c.Id |> Async.AwaitTask |> Async.RunSynchronously)
|
||||
return!
|
||||
viewInfo ctx
|
||||
|> Views.Church.maintain churches (stats |> Map.ofList) ctx
|
||||
|> renderHtml next ctx
|
||||
}
|
||||
|
||||
/// POST /church/save
|
||||
// POST /church/save
|
||||
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
|
||||
match! ctx.TryBindFormAsync<EditChurch> () with
|
||||
| Ok model ->
|
||||
let! church =
|
||||
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
|
||||
| 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 ()
|
||||
addInfo ctx ctx.Strings["Successfully {0} church “{1}”", act, model.Name]
|
||||
return! redirectTo false "/churches" next ctx
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
module PrayerTracker.Email
|
||||
|
||||
open MailKit.Net.Smtp
|
||||
open MailKit.Security
|
||||
open Microsoft.Extensions.Localization
|
||||
open MimeKit
|
||||
open MimeKit.Text
|
||||
open PrayerTracker.Entities
|
||||
|
||||
/// Parameters required to send an e-mail
|
||||
@@ -35,11 +33,13 @@ type EmailOptions =
|
||||
/// The e-mail address from which e-mail is sent
|
||||
let private fromAddress = "prayer@bitbadger.solutions"
|
||||
|
||||
open MailKit.Security
|
||||
open Microsoft.Extensions.Configuration
|
||||
|
||||
/// Get an SMTP client connection
|
||||
// FIXME: make host configurable
|
||||
let getConnection () = task {
|
||||
let getConnection (cfg : IConfiguration) = task {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ let createMessage opts =
|
||||
msg.ReplyTo.Add (MailboxAddress (opts.Group.Preferences.EmailFromName, opts.Group.Preferences.EmailFromAddress))
|
||||
msg
|
||||
|
||||
open MimeKit.Text
|
||||
|
||||
/// Create an HTML-format e-mail message
|
||||
let createHtmlMessage opts =
|
||||
let bodyText =
|
||||
|
||||
@@ -76,14 +76,11 @@ type HttpContext with
|
||||
/// The system clock (via DI)
|
||||
member this.Clock = this.GetService<IClock> ()
|
||||
|
||||
/// The PostgreSQL connection (configured via DI)
|
||||
member this.Conn = this.GetService<NpgsqlConnection> ()
|
||||
|
||||
/// The current instant
|
||||
member this.Now = this.Clock.GetCurrentInstant ()
|
||||
|
||||
/// 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)
|
||||
member this.CurrentGroup () = task {
|
||||
@@ -92,7 +89,7 @@ type HttpContext with
|
||||
| None ->
|
||||
match this.User.SmallGroupId with
|
||||
| Some groupId ->
|
||||
match! SmallGroups.tryByIdWithPreferences groupId this.Conn with
|
||||
match! SmallGroups.tryByIdWithPreferences groupId with
|
||||
| Some group ->
|
||||
this.Session.CurrentGroup <- Some group
|
||||
return Some group
|
||||
@@ -107,10 +104,10 @@ type HttpContext with
|
||||
| None ->
|
||||
match this.User.UserId with
|
||||
| Some userId ->
|
||||
match! Users.tryById userId this.Conn with
|
||||
match! Users.tryById userId with
|
||||
| Some 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
|
||||
return Some user
|
||||
| None -> return None
|
||||
|
||||
@@ -7,19 +7,19 @@ open Microsoft.AspNetCore.Http
|
||||
open Microsoft.AspNetCore.Localization
|
||||
open PrayerTracker
|
||||
|
||||
/// GET /error/[error-code]
|
||||
// GET /error/[error-code]
|
||||
let error code : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
|
||||
viewInfo ctx
|
||||
|> Views.Home.error code
|
||||
|> renderHtml next ctx
|
||||
|
||||
/// GET /
|
||||
// GET /
|
||||
let homePage : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
|
||||
viewInfo ctx
|
||||
|> Views.Home.index
|
||||
|> renderHtml next ctx
|
||||
|
||||
/// GET /language/[culture]
|
||||
// GET /language/[culture]
|
||||
let language culture : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
|
||||
try
|
||||
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
|
||||
redirectTo false url next ctx
|
||||
|
||||
/// GET /legal/privacy-policy
|
||||
// GET /legal/privacy-policy
|
||||
let privacyPolicy : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
|
||||
viewInfo ctx
|
||||
|> Views.Home.privacyPolicy
|
||||
|> renderHtml next ctx
|
||||
|
||||
/// GET /legal/terms-of-service
|
||||
// GET /legal/terms-of-service
|
||||
let tos : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
|
||||
viewInfo ctx
|
||||
|> Views.Home.termsOfService
|
||||
@@ -57,7 +57,7 @@ let tos : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
|
||||
open Microsoft.AspNetCore.Authentication
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
|
||||
/// GET /log-off
|
||||
// GET /log-off
|
||||
let logOff : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task {
|
||||
ctx.Session.Clear ()
|
||||
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
|
||||
@@ -65,7 +65,7 @@ let logOff : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx
|
||||
return! redirectTo false "/" next ctx
|
||||
}
|
||||
|
||||
/// GET /unauthorized
|
||||
// GET /unauthorized
|
||||
let unauthorized : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx ->
|
||||
viewInfo ctx
|
||||
|> Views.Home.unauthorized
|
||||
|
||||
@@ -9,7 +9,7 @@ open PrayerTracker.ViewModels
|
||||
|
||||
/// Retrieve a prayer request, and ensure that it belongs to the current class
|
||||
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 _ ->
|
||||
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
|
||||
ActiveOnly = true
|
||||
PageNumber = 0
|
||||
} ctx.Conn
|
||||
}
|
||||
return
|
||||
{ Requests = reqs
|
||||
Date = listDate
|
||||
@@ -49,7 +49,7 @@ let private parseListDate (date : string option) =
|
||||
|
||||
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 group = ctx.Session.CurrentGroup.Value
|
||||
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
|
||||
}
|
||||
|
||||
/// 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 s = ctx.Strings
|
||||
let listDate = parseListDate (Some date)
|
||||
let! list = generateRequestList ctx listDate
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
let! recipients = Members.forGroup group.Id ctx.Conn
|
||||
use! client = Email.getConnection ()
|
||||
let! recipients = Members.forGroup group.Id
|
||||
use! client = Email.getConnection (ctx.GetService<IConfiguration> ())
|
||||
do! Email.sendEmails
|
||||
{ Client = client
|
||||
Recipients = recipients
|
||||
@@ -102,31 +104,31 @@ let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|
||||
|> 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 requestId = PrayerRequestId reqId
|
||||
match! findRequest ctx requestId with
|
||||
| Ok req ->
|
||||
do! PrayerRequests.deleteById req.Id ctx.Conn
|
||||
do! PrayerRequests.deleteById req.Id
|
||||
addInfo ctx ctx.Strings["The prayer request was deleted successfully"]
|
||||
return! redirectTo false "/prayer-requests" next ctx
|
||||
| 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 requestId = PrayerRequestId reqId
|
||||
match! findRequest ctx requestId with
|
||||
| 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 ()]
|
||||
return! redirectTo false "/prayer-requests" next ctx
|
||||
| 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 {
|
||||
match! SmallGroups.tryByIdWithPreferences (SmallGroupId groupId) ctx.Conn with
|
||||
match! SmallGroups.tryByIdWithPreferences (SmallGroupId groupId) with
|
||||
| Some group when group.Preferences.IsPublic ->
|
||||
let! reqs =
|
||||
PrayerRequests.forGroup
|
||||
@@ -135,7 +137,7 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne
|
||||
ListDate = None
|
||||
ActiveOnly = true
|
||||
PageNumber = 0
|
||||
} ctx.Conn
|
||||
}
|
||||
return!
|
||||
viewInfo ctx
|
||||
|> Views.PrayerRequest.list
|
||||
@@ -153,18 +155,18 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne
|
||||
| None -> return! fourOhFour ctx
|
||||
}
|
||||
|
||||
/// GET /prayer-requests/lists
|
||||
// GET /prayer-requests/lists
|
||||
let lists : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task {
|
||||
let! groups = SmallGroups.listPublicAndProtected ctx.Conn
|
||||
let! groups = SmallGroups.listPublicAndProtected ()
|
||||
return!
|
||||
viewInfo ctx
|
||||
|> Views.PrayerRequest.lists groups
|
||||
|> renderHtml next ctx
|
||||
}
|
||||
|
||||
/// GET /prayer-requests[/inactive?]
|
||||
/// - OR -
|
||||
/// GET /prayer-requests?search=[search-query]
|
||||
// GET /prayer-requests[/inactive?]
|
||||
// - OR -
|
||||
// GET /prayer-requests?search=[search-query]
|
||||
let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
let pageNbr =
|
||||
@@ -174,7 +176,7 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
|
||||
let! model = backgroundTask {
|
||||
match ctx.GetQueryStringValue "search" with
|
||||
| Ok search ->
|
||||
let! reqs = PrayerRequests.searchForGroup group search pageNbr ctx.Conn
|
||||
let! reqs = PrayerRequests.searchForGroup group search pageNbr
|
||||
return
|
||||
{ MaintainRequests.empty with
|
||||
Requests = reqs
|
||||
@@ -189,7 +191,7 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx
|
||||
ListDate = None
|
||||
ActiveOnly = onlyActive
|
||||
PageNumber = pageNbr
|
||||
} ctx.Conn
|
||||
}
|
||||
return
|
||||
{ MaintainRequests.empty with
|
||||
Requests = reqs
|
||||
@@ -203,7 +205,7 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun 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! list = generateRequestList ctx (parseListDate (Some date))
|
||||
return!
|
||||
@@ -211,12 +213,12 @@ let print date : HttpHandler = requireAccess [ User; Group ] >=> fun 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 requestId = PrayerRequestId reqId
|
||||
match! findRequest ctx requestId with
|
||||
| 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 ()]
|
||||
return! redirectTo false "/prayer-requests" next ctx
|
||||
| Result.Error e -> return! e
|
||||
@@ -224,7 +226,7 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas
|
||||
|
||||
open System.Threading.Tasks
|
||||
|
||||
/// POST /prayer-request/save
|
||||
// POST /prayer-request/save
|
||||
let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
|
||||
match! ctx.TryBindFormAsync<EditRequest> () with
|
||||
| Ok model ->
|
||||
@@ -237,7 +239,7 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct
|
||||
UserId = ctx.User.UserId.Value
|
||||
}
|
||||
|> (Some >> Task.FromResult)
|
||||
else PrayerRequests.tryById (idFromShort PrayerRequestId model.RequestId) ctx.Conn
|
||||
else PrayerRequests.tryById (idFromShort PrayerRequestId model.RequestId)
|
||||
match req with
|
||||
| Some pr when pr.SmallGroupId = group.Id ->
|
||||
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 when defaultArg model.SkipDateUpdate false -> it
|
||||
| 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"
|
||||
addInfo ctx ctx.Strings["Successfully {0} prayer request", ctx.Strings[act].Value.ToLower ()]
|
||||
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
|
||||
}
|
||||
|
||||
/// GET /prayer-request/view/[date?]
|
||||
// GET /prayer-request/view/[date?]
|
||||
let view date : HttpHandler = requireAccess [ User; Group ] >=> fun next ctx -> task {
|
||||
let! list = generateRequestList ctx (parseListDate date)
|
||||
return!
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<PublishSingleFile>True</PublishSingleFile>
|
||||
<PublishSingleFile>False</PublishSingleFile>
|
||||
<SelfContained>False</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -24,10 +24,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
<PackageReference Include="Giraffe.Htmx" Version="1.9.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.1" />
|
||||
<PackageReference Update="FSharp.Core" Version="7.0.300" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,21 +7,20 @@ open PrayerTracker.Data
|
||||
open PrayerTracker.Entities
|
||||
open PrayerTracker.ViewModels
|
||||
|
||||
/// GET /small-group/announcement
|
||||
// GET /small-group/announcement
|
||||
let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
|
||||
{ viewInfo ctx with HelpLink = Some Help.sendAnnouncement }
|
||||
|> Views.SmallGroup.announcement ctx.Session.CurrentUser.Value.IsAdmin 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 groupId = SmallGroupId grpId
|
||||
let conn = ctx.Conn
|
||||
match! SmallGroups.tryById groupId conn with
|
||||
match! SmallGroups.tryById groupId with
|
||||
| Some grp ->
|
||||
let! reqs = PrayerRequests.countByGroup groupId conn
|
||||
let! users = Users.countByGroup groupId conn
|
||||
do! SmallGroups.deleteById groupId conn
|
||||
let! reqs = PrayerRequests.countByGroup groupId
|
||||
let! users = Users.countByGroup groupId
|
||||
do! SmallGroups.deleteById groupId
|
||||
addInfo ctx
|
||||
ctx.Strings["The group “{0}” and its {1} prayer request(s) were deleted successfully; revoked access from {2} user(s)",
|
||||
grp.Name, reqs, users]
|
||||
@@ -29,22 +28,22 @@ let delete grpId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fu
|
||||
| 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 group = ctx.Session.CurrentGroup.Value
|
||||
let memberId = MemberId mbrId
|
||||
match! Members.tryById memberId ctx.Conn with
|
||||
match! Members.tryById memberId with
|
||||
| Some mbr when mbr.SmallGroupId = group.Id ->
|
||||
do! Members.deleteById memberId ctx.Conn
|
||||
do! Members.deleteById memberId
|
||||
addHtmlInfo ctx ctx.Strings["The group member “{0}” was deleted successfully", mbr.Name]
|
||||
return! redirectTo false "/small-group/members" next ctx
|
||||
| Some _
|
||||
| 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! churches = Churches.all ctx.Conn
|
||||
let! churches = Churches.all ()
|
||||
let groupId = SmallGroupId grpId
|
||||
if groupId.Value = Guid.Empty then
|
||||
return!
|
||||
@@ -52,7 +51,7 @@ let edit grpId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task
|
||||
|> Views.SmallGroup.edit EditSmallGroup.empty churches ctx
|
||||
|> renderHtml next ctx
|
||||
else
|
||||
match! SmallGroups.tryById groupId ctx.Conn with
|
||||
match! SmallGroups.tryById groupId with
|
||||
| Some grp ->
|
||||
return!
|
||||
viewInfo ctx
|
||||
@@ -61,7 +60,7 @@ let edit grpId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task
|
||||
| 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 group = ctx.Session.CurrentGroup.Value
|
||||
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
|
||||
|> renderHtml next ctx
|
||||
else
|
||||
match! Members.tryById memberId ctx.Conn with
|
||||
match! Members.tryById memberId with
|
||||
| Some mbr when mbr.SmallGroupId = group.Id ->
|
||||
return!
|
||||
viewInfo ctx
|
||||
@@ -82,9 +81,9 @@ let editMember mbrId : HttpHandler = requireAccess [ User ] >=> fun next 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! groups = SmallGroups.listProtected ctx.Conn
|
||||
let! groups = SmallGroups.listProtected ()
|
||||
let groupId = match grpId with Some gid -> shortGuid gid | None -> ""
|
||||
return!
|
||||
{ viewInfo ctx with HelpLink = Some Help.logOn }
|
||||
@@ -96,11 +95,11 @@ open System.Security.Claims
|
||||
open Microsoft.AspNetCore.Authentication
|
||||
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 {
|
||||
match! ctx.TryBindFormAsync<GroupLogOn> () with
|
||||
| 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 ->
|
||||
ctx.Session.CurrentGroup <- Some group
|
||||
let identity = ClaimsIdentity (
|
||||
@@ -119,19 +118,19 @@ let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validat
|
||||
| Result.Error e -> return! bindError e next ctx
|
||||
}
|
||||
|
||||
/// GET /small-groups
|
||||
// GET /small-groups
|
||||
let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
|
||||
let! groups = SmallGroups.infoForAll ctx.Conn
|
||||
let! groups = SmallGroups.infoForAll ()
|
||||
return!
|
||||
viewInfo ctx
|
||||
|> Views.SmallGroup.maintain groups ctx
|
||||
|> renderHtml next ctx
|
||||
}
|
||||
|
||||
/// GET /small-group/members
|
||||
// GET /small-group/members
|
||||
let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|
||||
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
|
||||
return!
|
||||
{ viewInfo ctx with HelpLink = Some Help.maintainGroupMembers }
|
||||
@@ -139,20 +138,19 @@ let members : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|
||||
|> renderHtml next ctx
|
||||
}
|
||||
|
||||
/// GET /small-group
|
||||
// GET /small-group
|
||||
let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
let conn = ctx.Conn
|
||||
let! reqs = PrayerRequests.forGroup
|
||||
{ SmallGroup = group
|
||||
Clock = ctx.Clock
|
||||
ListDate = None
|
||||
ActiveOnly = true
|
||||
PageNumber = 0
|
||||
} conn
|
||||
let! reqCount = PrayerRequests.countByGroup group.Id conn
|
||||
let! mbrCount = Members.countByGroup group.Id conn
|
||||
let! admins = Users.listByGroupId group.Id conn
|
||||
}
|
||||
let! reqCount = PrayerRequests.countByGroup group.Id
|
||||
let! mbrCount = Members.countByGroup group.Id
|
||||
let! admins = Users.listByGroupId group.Id
|
||||
let model =
|
||||
{ TotalActiveReqs = List.length reqs
|
||||
AllReqs = reqCount
|
||||
@@ -173,7 +171,7 @@ let overview : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|
||||
|> renderHtml next ctx
|
||||
}
|
||||
|
||||
/// GET /small-group/preferences
|
||||
// GET /small-group/preferences
|
||||
let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|
||||
return!
|
||||
{ viewInfo ctx with HelpLink = Some Help.groupPreferences }
|
||||
@@ -183,16 +181,16 @@ let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task
|
||||
|
||||
open System.Threading.Tasks
|
||||
|
||||
/// POST /small-group/save
|
||||
// POST /small-group/save
|
||||
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
|
||||
match! ctx.TryBindFormAsync<EditSmallGroup> () with
|
||||
| Ok model ->
|
||||
let! tryGroup =
|
||||
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
|
||||
| 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 ()
|
||||
addHtmlInfo ctx ctx.Strings["Successfully {0} group “{1}”", act, model.Name]
|
||||
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
|
||||
}
|
||||
|
||||
/// POST /small-group/member/save
|
||||
// POST /small-group/member/save
|
||||
let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
|
||||
match! ctx.TryBindFormAsync<EditMember> () with
|
||||
| Ok model ->
|
||||
@@ -208,7 +206,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun n
|
||||
let! tryMbr =
|
||||
if model.IsNew then
|
||||
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
|
||||
| Some mbr when mbr.SmallGroupId = group.Id ->
|
||||
do! Members.save
|
||||
@@ -216,7 +214,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun n
|
||||
Name = model.Name
|
||||
Email = model.Email
|
||||
Format = String.noneIfBlank model.Format |> Option.map EmailFormat.fromCode
|
||||
} ctx.Conn
|
||||
}
|
||||
let act = ctx.Strings[if model.IsNew then "Added" else "Updated"].Value.ToLower ()
|
||||
addInfo ctx ctx.Strings["Successfully {0} group member", act]
|
||||
return! redirectTo false "/small-group/members" next ctx
|
||||
@@ -225,18 +223,18 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun n
|
||||
| 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 {
|
||||
match! ctx.TryBindFormAsync<EditPreferences> () with
|
||||
| Ok model ->
|
||||
// Since the class is stored in the session, we'll use an intermediate instance to persist it; once that works,
|
||||
// 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.
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
match! SmallGroups.tryByIdWithPreferences group.Id ctx.Conn with
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
match! SmallGroups.tryByIdWithPreferences group.Id with
|
||||
| Some group ->
|
||||
let pref = model.PopulatePreferences group.Preferences
|
||||
do! SmallGroups.savePreferences pref ctx.Conn
|
||||
do! SmallGroups.savePreferences pref
|
||||
// Refresh session instance
|
||||
ctx.Session.CurrentGroup <- Some { group with Preferences = pref }
|
||||
addInfo ctx ctx.Strings["Group preferences updated successfully"]
|
||||
@@ -247,8 +245,9 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
|
||||
|
||||
open Giraffe.ViewEngine
|
||||
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 {
|
||||
match! ctx.TryBindFormAsync<Announcement> () with
|
||||
| Ok model ->
|
||||
@@ -266,11 +265,11 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
|
||||
// Send the e-mails
|
||||
let! recipients = task {
|
||||
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 })
|
||||
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
|
||||
{ Client = client
|
||||
Recipients = recipients
|
||||
@@ -297,7 +296,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
|
||||
Text = requestText
|
||||
EnteredDate = now.Date.AtStartOfDayInZone(zone).ToInstant()
|
||||
UpdatedDate = now.InZoneLeniently(zone).ToInstant()
|
||||
} ctx.Conn
|
||||
}
|
||||
// Tell 'em what they've won, Johnny!
|
||||
let toWhom =
|
||||
if model.SendToClass = "N" then s["{0} users", s["PrayerTracker"]].Value
|
||||
|
||||
@@ -9,6 +9,8 @@ open PrayerTracker.Data
|
||||
open PrayerTracker.Entities
|
||||
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
|
||||
[<AutoOpen>]
|
||||
module Hashing =
|
||||
@@ -53,15 +55,15 @@ module Hashing =
|
||||
|
||||
|
||||
/// Retrieve a user from the database by password, upgrading password hashes if required
|
||||
let private findUserByPassword model conn = task {
|
||||
match! Users.tryByEmailAndGroup model.Email (idFromShort SmallGroupId model.SmallGroupId) conn with
|
||||
let private findUserByPassword model = task {
|
||||
match! Users.tryByEmailAndGroup model.Email (idFromShort SmallGroupId model.SmallGroupId) with
|
||||
| Some user ->
|
||||
let hasher = PrayerTrackerPasswordHasher ()
|
||||
match hasher.VerifyHashedPassword (user, user.PasswordHash, model.Password) with
|
||||
| PasswordVerificationResult.Success -> return Some user
|
||||
| PasswordVerificationResult.SuccessRehashNeeded ->
|
||||
let upgraded = { user with PasswordHash = hasher.HashPassword (user, model.Password) }
|
||||
do! Users.updatePassword upgraded conn
|
||||
do! Users.updatePassword upgraded
|
||||
return Some upgraded
|
||||
| _ -> return None
|
||||
| None -> return None
|
||||
@@ -74,14 +76,14 @@ let sanitizeUrl providedUrl defaultUrl =
|
||||
elif Seq.exists Char.IsControl url then defaultUrl
|
||||
else url
|
||||
|
||||
/// POST /user/password/change
|
||||
// POST /user/password/change
|
||||
let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
|
||||
match! ctx.TryBindFormAsync<ChangePassword> () with
|
||||
| Ok model ->
|
||||
let curUsr = ctx.Session.CurrentUser.Value
|
||||
let hasher = PrayerTrackerPasswordHasher ()
|
||||
let! user = task {
|
||||
match! Users.tryById curUsr.Id ctx.Conn with
|
||||
match! Users.tryById curUsr.Id with
|
||||
| Some usr ->
|
||||
if hasher.VerifyHashedPassword (usr, usr.PasswordHash, model.OldPassword)
|
||||
= PasswordVerificationResult.Success then
|
||||
@@ -91,7 +93,7 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> f
|
||||
}
|
||||
match user with
|
||||
| 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"]
|
||||
return! redirectTo false "/" next ctx
|
||||
| Some _ ->
|
||||
@@ -103,12 +105,12 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> f
|
||||
| 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 userId = UserId usrId
|
||||
match! Users.tryById userId ctx.Conn with
|
||||
match! Users.tryById userId with
|
||||
| Some user ->
|
||||
do! Users.deleteById userId ctx.Conn
|
||||
do! Users.deleteById userId
|
||||
addInfo ctx ctx.Strings["Successfully deleted user {0}", user.Name]
|
||||
return! redirectTo false "/users" next ctx
|
||||
| _ -> return! fourOhFour ctx
|
||||
@@ -120,14 +122,14 @@ open Microsoft.AspNetCore.Authentication
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
open Microsoft.AspNetCore.Html
|
||||
|
||||
/// POST /user/log-on
|
||||
// POST /user/log-on
|
||||
let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
|
||||
match! ctx.TryBindFormAsync<UserLogOn> () with
|
||||
| Ok model ->
|
||||
let s = ctx.Strings
|
||||
match! findUserByPassword model ctx.Conn with
|
||||
match! findUserByPassword model with
|
||||
| Some user ->
|
||||
match! SmallGroups.tryByIdWithPreferences (idFromShort SmallGroupId model.SmallGroupId) ctx.Conn with
|
||||
match! SmallGroups.tryByIdWithPreferences (idFromShort SmallGroupId model.SmallGroupId) with
|
||||
| Some group ->
|
||||
ctx.Session.CurrentUser <- Some user
|
||||
ctx.Session.CurrentGroup <- Some group
|
||||
@@ -141,7 +143,7 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsr
|
||||
AuthenticationProperties (
|
||||
IssuedUtc = DateTimeOffset.UtcNow,
|
||||
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"]]
|
||||
return! redirectTo false (sanitizeUrl model.RedirectUrl "/small-group") next ctx
|
||||
| None -> return! fourOhFour ctx
|
||||
@@ -163,7 +165,7 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsr
|
||||
| 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 userId = UserId usrId
|
||||
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
|
||||
|> renderHtml next ctx
|
||||
else
|
||||
match! Users.tryById userId ctx.Conn with
|
||||
match! Users.tryById userId with
|
||||
| Some user ->
|
||||
return!
|
||||
viewInfo ctx
|
||||
@@ -181,9 +183,9 @@ let edit usrId : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task
|
||||
| _ -> return! fourOhFour ctx
|
||||
}
|
||||
|
||||
/// GET /user/log-on
|
||||
// GET /user/log-on
|
||||
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
|
||||
match url with
|
||||
| Some _ ->
|
||||
@@ -196,16 +198,16 @@ let logOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx
|
||||
|> renderHtml next ctx
|
||||
}
|
||||
|
||||
/// GET /users
|
||||
// GET /users
|
||||
let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
|
||||
let! users = Users.all ctx.Conn
|
||||
let! users = Users.all ()
|
||||
return!
|
||||
viewInfo ctx
|
||||
|> Views.User.maintain users ctx
|
||||
|> renderHtml next ctx
|
||||
}
|
||||
|
||||
/// GET /user/password
|
||||
// GET /user/password
|
||||
let password : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
|
||||
{ viewInfo ctx with HelpLink = Some Help.changePassword }
|
||||
|> Views.User.changePassword ctx
|
||||
@@ -213,18 +215,18 @@ let password : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
|
||||
|
||||
open System.Threading.Tasks
|
||||
|
||||
/// POST /user/save
|
||||
// POST /user/save
|
||||
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
|
||||
match! ctx.TryBindFormAsync<EditUser> () with
|
||||
| Ok model ->
|
||||
let! user =
|
||||
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
|
||||
| Some usr ->
|
||||
let hasher = PrayerTrackerPasswordHasher ()
|
||||
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
|
||||
if model.IsNew then
|
||||
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
|
||||
}
|
||||
|
||||
/// POST /user/small-groups/save
|
||||
// POST /user/small-groups/save
|
||||
let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
|
||||
match! ctx.TryBindFormAsync<AssignGroups> () with
|
||||
| Ok model ->
|
||||
@@ -254,19 +256,19 @@ let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun
|
||||
return! redirectTo false $"/user/{model.UserId}/small-groups" next ctx
|
||||
| _ ->
|
||||
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]
|
||||
return! redirectTo false "/users" 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 userId = UserId usrId
|
||||
match! Users.tryById userId ctx.Conn with
|
||||
match! Users.tryById userId with
|
||||
| Some user ->
|
||||
let! groups = SmallGroups.listAll ctx.Conn
|
||||
let! groupIds = Users.groupIdsByUserId userId ctx.Conn
|
||||
let! groups = SmallGroups.listAll ()
|
||||
let! groupIds = Users.groupIdsByUserId userId
|
||||
let curGroups = groupIds |> List.map (fun g -> shortGuid g.Value)
|
||||
return!
|
||||
viewInfo ctx
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user