Add htmx targets to forms (#36)

- Derive layout based on htmx headers
This commit is contained in:
Daniel J. Summers 2022-07-31 17:56:32 -04:00
parent 810b5d8258
commit 1547377527
15 changed files with 886 additions and 819 deletions

View File

@ -8,7 +8,7 @@ open PrayerTracker.ViewModels
let edit (m : EditChurch) ctx vi =
let pageTitle = if m.IsNew then "Add a New Church" else "Edit Church"
let s = I18N.localizer.Force ()
[ form [ _action "/church/save"; _method "post"; _class "pt-center-columns" ] [
[ form [ _action "/church/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
style [ _scoped ] [
rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }"
]
@ -28,9 +28,9 @@ let edit (m : EditChurch) ctx vi =
input [ _type "text"
_name (nameof m.State)
_id "state"
_required
_minlength "2"; _maxlength "2"
_value m.State ]
_value m.State
_required ]
]
]
div [ _class "pt-field-row" ] [
@ -90,8 +90,9 @@ let maintain (churches : Church list) (stats : Map<string, ChurchStats>) ctx vi
a [ _href $"/church/{chId}/edit"; _title s["Edit This Church"].Value ] [ icon "edit" ]
a [ _href delAction
_title s["Delete This Church"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ]
[ icon "delete_forever" ]
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
]
td [] [ str ch.name ]
td [] [ str ch.city; rawText ", "; str ch.st ]
@ -104,8 +105,9 @@ let maintain (churches : Church list) (stats : Map<string, ChurchStats>) ctx vi
]
[ div [ _class "pt-center-text" ] [
br []
a [ _href $"/church/{emptyGuid}/edit"; _title s["Add a New Church"].Value ]
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Church"] ]
a [ _href $"/church/{emptyGuid}/edit"; _title s["Add a New Church"].Value ] [
icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Church"]
]
br []
br []
]

View File

@ -113,6 +113,12 @@ let flatGuid (x : Guid) = x.ToString "N"
/// An empty GUID string (used for "add" actions)
let emptyGuid = flatGuid Guid.Empty
/// Create an HTML onsubmit event handler
let _onsubmit = attr "onsubmit"
/// A "rel='noopener'" attribute
let _relNoOpener = _rel "noopener"
/// The name this function used to have when the view engine was part of Giraffe
let renderHtmlNode = RenderView.AsString.htmlNode
@ -143,3 +149,15 @@ module TimeZones =
let name tzId (s : IStringLocalizer) =
try s[xref[tzId]]
with :? KeyNotFoundException -> LocalizedString (tzId, tzId)
open Giraffe.ViewEngine.Htmx
/// Known htmx targets
module Target =
/// htmx links target the body element
let body = _hxTarget "body"
/// htmx links target the #pt-body element
let content = _hxTarget "#pt-body"

View File

@ -50,7 +50,6 @@ let index vi =
let l = I18N.forView "Home/Index"
use sw = new StringWriter ()
let raw = rawLocText sw
[ p [] [
raw l["Welcome to <strong>{0}</strong>!", s["PrayerTracker"]]
space
@ -128,7 +127,6 @@ let privacyPolicy vi =
let l = I18N.forView "Home/PrivacyPolicy"
use sw = new StringWriter ()
let raw = rawLocText sw
[ p [ _class "pt-right-text" ] [ small [] [ em [] [ raw l["(as of July 31, 2018)"] ] ] ]
p [] [
raw l["The nature of the service is one where privacy is a must."]
@ -150,7 +148,7 @@ let privacyPolicy vi =
rawText " &ndash; "
raw l["{0} stores the text of prayer requests.", s["PrayerTracker"]]
space
raw l["It also stores names and e-mail addreses of small group members, and plain-text passwords for small groups with password-protected lists."]
raw l["It also stores names and e-mail addresses of small group members, and plain-text passwords for small groups with password-protected lists."]
]
]
h3 [] [ raw l["How Your Data Is Accessed / Secured"] ]

View File

@ -3,27 +3,13 @@ module PrayerTracker.Views.Layout
open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open PrayerTracker
open PrayerTracker.ViewModels
open System
open System.Globalization
/// Get the two-character language code for the current request
let langCode () = if CultureInfo.CurrentCulture.Name.StartsWith "es" then "es" else "en"
/// Known htmx targets
module Target =
/// htmx links target the body element
let body = _hxTarget "body"
/// htmx links target the #pt-body element
let content = _hxTarget "#pt-body"
/// Navigation items
module Navigation =
@ -31,11 +17,12 @@ module Navigation =
let top m =
let s = I18N.localizer.Force ()
let menuSpacer = rawText "&nbsp; "
let _dropdown = _class "dropbtn"
let leftLinks = [
match m.User with
| Some u ->
li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Requests"].Value; _title s["Requests"].Value; _roleButton ] [
a [ _dropdown; _ariaLabel s["Requests"].Value; _title s["Requests"].Value; _roleButton ] [
icon "question_answer"; space; locStr s["Requests"]; space; icon "keyboard_arrow_down"
]
div [ _class "dropdown-content"; _roleMenuBar ] [
@ -48,7 +35,7 @@ module Navigation =
]
]
li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Group"].Value; _title s["Group"].Value; _roleButton ] [
a [ _dropdown; _ariaLabel s["Group"].Value; _title s["Group"].Value; _roleButton ] [
icon "group"; space; locStr s["Group"]; space; icon "keyboard_arrow_down"
]
div [ _class "dropdown-content"; _roleMenuBar ] [
@ -65,7 +52,7 @@ module Navigation =
]
if u.isAdmin then
li [ _class "dropdown" ] [
a [ _class "dropbtn"
a [ _dropdown
_ariaLabel s["Administration"].Value
_title s["Administration"].Value
_roleButton ] [
@ -89,7 +76,7 @@ module Navigation =
]
| None ->
li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Log On"].Value; _title s["Log On"].Value; _roleButton ] [
a [ _dropdown; _ariaLabel s["Log On"].Value; _title s["Log On"].Value; _roleButton ] [
icon "security"; space; locStr s["Log On"]; space; icon "keyboard_arrow_down"
]
div [ _class "dropdown-content"; _roleMenuBar ] [
@ -111,7 +98,7 @@ module Navigation =
_ariaLabel s["Help"].Value
_title s["View Help"].Value
_target "_blank"
_rel "noopener" ] [
_relNoOpener ] [
icon "help"; space; locStr s["Help"]
]
]
@ -130,10 +117,7 @@ module Navigation =
]
| None -> ()
li [] [
a [ _href "/log-off"
_ariaLabel s["Log Off"].Value
_title s["Log Off"].Value
_hxTarget "body" ] [
a [ _href "/log-off"; _ariaLabel s["Log Off"].Value; _title s["Log Off"].Value; Target.body ] [
icon "power_settings_new"; space; locStr s["Log Off"]
]
]
@ -222,10 +206,11 @@ let private htmlHead m pageTitle =
yield! commonHead
for cssFile in m.Style do
link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ]
for jsFile in m.Script do
script [ _src $"/js/{jsFile}.js" ] []
]
open Giraffe.ViewEngine.Htmx
/// Render a link to the help page for the current page
let private helpLink link =
let s = I18N.localizer.Force ()
@ -241,7 +226,7 @@ let private helpLink link =
/// Render the page title, and optionally a help link
let private renderPageTitle m pageTitle =
h2 [ _id "pt-page-title" ] [
match m.HelpLink with Some link -> Help.fullLink (langCode ()) link |> helpLink | None -> ()
match m.HelpLink with Some link -> PrayerTracker.Utils.Help.fullLink (langCode ()) link |> helpLink | None -> ()
locStr pageTitle
]
@ -268,6 +253,9 @@ let private messages m =
]
])
open System
/// Render the <footer> at the bottom of the page
let private htmlFooter m =
let s = I18N.localizer.Force ()
@ -282,7 +270,7 @@ let private htmlFooter m =
a [ _href "https://github.com/bit-badger/PrayerTracker"
_title s["View source code and get technical support"].Value
_target "_blank"
_rel "noopener" ] [
_relNoOpener ] [
locStr s["Source & Support"]
]
]
@ -300,32 +288,56 @@ let private htmlFooter m =
script [ _src "/js/app.js" ] []
]
/// The standard layout for PrayerTracker
let standard m pageTitle (content : XmlNode) =
let s = I18N.localizer.Force ()
let ttl = s[pageTitle]
html [ _lang (langCode ()) ] [
htmlHead m ttl
body [ _hxBoost ] [
Navigation.top m
div [ _id "pt-body" ] [
Navigation.identity m
renderPageTitle m ttl
yield! messages m
/// The content portion of the PrayerTracker layout
let private contentSection viewInfo title (content : XmlNode) = [
Navigation.identity viewInfo
renderPageTitle viewInfo title
yield! messages viewInfo
content
htmlFooter m
htmlFooter viewInfo
for jsFile in viewInfo.Script do
script [ _src $"/js/{jsFile}.js" ] []
]
/// The HTML head element for partial responses
let private partialHead pgTitle =
let s = I18N.localizer.Force ()
head [] [
meta [ _charset "UTF-8" ]
title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker"] ]
]
open Giraffe.Htmx.Common
/// The body of the PrayerTracker layout
let private pageLayout viewInfo title content =
body [ _hxBoost ] [
Navigation.top viewInfo
div [ _id "pt-body"; Target.content; _hxSwap $"{HxSwap.InnerHtml} show:window:top" ]
(contentSection viewInfo title content)
]
/// The standard layout(s) for PrayerTracker
let standard viewInfo pageTitle content =
let s = I18N.localizer.Force ()
let pgTitle = s[pageTitle]
html [ _lang (langCode ()) ] [
match viewInfo.Layout with
| FullPage ->
htmlHead viewInfo pgTitle
pageLayout viewInfo pgTitle content
| PartialPage ->
partialHead pgTitle
pageLayout viewInfo pgTitle content
| ContentOnly ->
partialHead pgTitle
body [] (contentSection viewInfo pgTitle content)
]
/// A layout with nothing but a title and content
let bare pageTitle content =
let s = I18N.localizer.Force ()
let ttl = s[pageTitle]
html [ _lang (langCode ()) ] [
head [] [
meta [ _charset "UTF-8" ]
title [] [ locStr ttl; titleSep; locStr s["PrayerTracker"] ]
]
partialHead s[pageTitle]
body [] [ content ]
]

View File

@ -14,7 +14,7 @@ open PrayerTracker.ViewModels
let edit (m : EditRequest) today ctx vi =
let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Request" else "Edit Request"
[ form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns" ] [
[ form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx
input [ _type "hidden"; _name (nameof m.RequestId); _value (flatGuid m.RequestId) ]
div [ _class "pt-field-row" ] [
@ -28,8 +28,8 @@ let edit (m : EditRequest) today ctx vi =
div [ _class "pt-field" ] [
label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ]
input [ _type "text"
_name (nameof m.Requestor)
_id "requestor"
_name (nameof m.Requestor)
_value (defaultArg m.Requestor "") ]
]
if m.IsNew then
@ -41,7 +41,10 @@ let edit (m : EditRequest) today ctx vi =
div [ _class "pt-field" ] [
div [ _class "pt-checkbox-field" ] [
br []
input [ _type "checkbox"; _name (nameof m.SkipDateUpdate); _id "skipDateUpdate"; _value "True" ]
input [ _type "checkbox"
_name (nameof m.SkipDateUpdate)
_id "skipDateUpdate"
_value "True" ]
label [ _for "skipDateUpdate" ] [ locStr s["Check to not update the date"] ]
br []
small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ]
@ -139,7 +142,9 @@ let lists (groups : SmallGroup list) vi =
if grp.preferences.isPublic then
a [ _href $"/prayer-requests/{grpId}/list"; _title s["View"].Value ] [ icon "list" ]
else
a [ _href $"/small-group/log-on/{grpId}"; _title s["Log On"].Value ] [ icon "verified_user" ]
a [ _href $"/small-group/log-on/{grpId}"; _title s["Log On"].Value ] [
icon "verified_user"
]
|> List.singleton
|> td []
td [] [ str grp.church.name ]
@ -159,14 +164,13 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
use sw = new StringWriter ()
let raw = rawLocText sw
let now = m.SmallGroup.localDateNow (ctx.GetService<IClock> ())
let prefs = m.SmallGroup.preferences
let types = ReferenceList.requestTypeList s |> Map.ofList
let updReq (req : PrayerRequest) =
if req.updateRequired now m.SmallGroup.preferences.daysToExpire m.SmallGroup.preferences.longTermUpdateWeeks then
"pt-request-update"
else ""
if req.updateRequired now prefs.daysToExpire prefs.longTermUpdateWeeks then "pt-request-update" else ""
|> _class
let reqExp (req : PrayerRequest) =
_class (if req.isExpired now m.SmallGroup.preferences.daysToExpire then "pt-request-expired" else "")
_class (if req.isExpired now prefs.daysToExpire then "pt-request-expired" else "")
/// Iterate the sequence once, before we render, so we can get the count of it at the top of the table
let requests =
m.Requests
@ -184,19 +188,24 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
|> String.concat ""
tr [] [
td [] [
a [ _href $"/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ]
[ icon "edit" ]
if req.isExpired now m.SmallGroup.preferences.daysToExpire then
a [ _href $"/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] [
icon "edit"
]
if req.isExpired now prefs.daysToExpire then
a [ _href $"/prayer-request/{reqId}/restore"
_title l["Restore This Inactive Request"].Value ]
[ icon "visibility" ]
_title l["Restore This Inactive Request"].Value ] [
icon "visibility"
]
else
a [ _href $"/prayer-request/{reqId}/expire"
_title l["Expire This Request Immediately"].Value ]
[ icon "visibility_off" ]
a [ _href delAction; _title l["Delete This Request"].Value;
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ]
[ icon "delete_forever" ]
_title l["Expire This Request Immediately"].Value ] [
icon "visibility_off"
]
a [ _href delAction
_title l["Delete This Request"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
]
td [ updReq req ] [
str (req.updatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture))
@ -212,19 +221,22 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
|> List.ofSeq
[ div [ _class "pt-center-text" ] [
br []
a [ _href $"/prayer-request/{emptyGuid}/edit"; _title s["Add a New Request"].Value ]
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Request"] ]
a [ _href $"/prayer-request/{emptyGuid}/edit"; _title s["Add a New Request"].Value ] [
icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Request"]
]
rawText " &nbsp; &nbsp; &nbsp; "
a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ]
[ icon "list"; rawText " &nbsp;"; locStr s["View Prayer Request List"] ]
a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ] [
icon "list"; rawText " &nbsp;"; locStr s["View Prayer Request List"]
]
match m.SearchTerm with
| Some _ ->
rawText " &nbsp; &nbsp; &nbsp; "
a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ]
[ icon "highlight_off"; rawText " &nbsp;"; raw l["Clear Search Criteria"] ]
a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ] [
icon "highlight_off"; rawText " &nbsp;"; raw l["Clear Search Criteria"]
]
| None -> ()
]
form [ _action "/prayer-requests"; _method "get"; _class "pt-center-text pt-search-form" ] [
form [ _action "/prayer-requests"; _method "get"; _class "pt-center-text pt-search-form"; Target.content ] [
input [ _type "text"
_name "search"
_placeholder l["Search requests..."].Value
@ -267,19 +279,20 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
let search = [ match m.SearchTerm with Some s -> "search", s | None -> () ]
let pg = defaultArg m.PageNbr 1
let url =
match m.OnlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/prayer-requests%s"
match m.OnlyActive with Some true | None -> "" | _ -> "/inactive"
|> sprintf "/prayer-requests%s"
match pg with
| 1 -> ()
| _ ->
// button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; locStr text ]
let withPage = match pg with 2 -> search | _ -> ("page", string (pg - 1)) :: search
a [ _href (makeUrl url withPage) ]
[ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ]
a [ _href (makeUrl url withPage) ] [ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ]
rawText " &nbsp; &nbsp; "
match requests.Length = m.SmallGroup.preferences.pageSize with
| true ->
a [ _href (makeUrl url (("page", string (pg + 1)) :: search)) ]
[ raw l["Next Page"]; space; icon "keyboard_arrow_right" ]
a [ _href (makeUrl url (("page", string (pg + 1)) :: search)) ] [
raw l["Next Page"]; space; icon "keyboard_arrow_right"
]
| false -> ()
]
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
@ -319,8 +332,10 @@ let view m vi =
br []
a [ _class "pt-icon-link"
_href $"/prayer-requests/print/{dtString}"
_title s["View Printable"].Value
] [ icon "print"; rawText " &nbsp;"; locStr s["View Printable"] ]
_target "_blank"
_title s["View Printable"].Value ] [
icon "print"; rawText " &nbsp;"; locStr s["View Printable"]
]
if m.CanEmail then
spacer
if m.Date.DayOfWeek <> DayOfWeek.Sunday then

View File

@ -147,7 +147,7 @@
<data name="If you utilize the “{0}” box on sign in, a second cookie is stored, and transmitted to establish a session; this cookie is removed by clicking the “{1}” link." xml:space="preserve">
<value>Si utilizas el cuadro "{0}" al iniciar sesión, se almacena una segunda cookie y se transmite para establecer una sesión; esta cookie se elimina haciendo clic en el enlace "{1}".</value>
</data>
<data name="It also stores names and e-mail addreses of small group members, and plain-text passwords for small groups with password-protected lists." xml:space="preserve">
<data name="It also stores names and e-mail addresses of small group members, and plain-text passwords for small groups with password-protected lists." xml:space="preserve">
<value>También almacena nombres y direcciones de correo electrónico de miembros de grupos pequeños, y contraseñas de texto sin formato para grupos pequeños con listas protegidas por contraseña.</value>
</data>
<data name="On the server, all data is stored in a controlled-access database." xml:space="preserve">

View File

@ -1,9 +1,7 @@
module PrayerTracker.Views.SmallGroup
open System.IO
open Giraffe.ViewEngine
open Microsoft.Extensions.Localization
open PrayerTracker
open PrayerTracker.Entities
open PrayerTracker.ViewModels
@ -12,7 +10,7 @@ let announcement isAdmin ctx vi =
let s = I18N.localizer.Force ()
let m = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None }
let reqTypes = ReferenceList.requestTypeList s
[ form [ _action "/small-group/announcement/send"; _method "post"; _class "pt-center-columns" ] [
[ form [ _action "/small-group/announcement/send"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx
div [ _class "pt-field-row" ] [
div [ _class "pt-field pt-editor" ] [
@ -74,7 +72,7 @@ let announcementSent (m : Announcement) vi =
let edit (m : EditSmallGroup) (churches : Church list) ctx vi =
let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Group" else "Edit Group"
form [ _action "/small-group/save"; _method "post"; _class "pt-center-columns" ] [
form [ _action "/small-group/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx
input [ _type "hidden"; _name (nameof m.SmallGroupId); _value (flatGuid m.SmallGroupId) ]
div [ _class "pt-field-row" ] [
@ -104,7 +102,7 @@ let edit (m : EditSmallGroup) (churches : Church list) ctx vi =
let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi =
let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Group Member" else "Edit Group Member"
form [ _action "/small-group/member/save"; _method "post"; _class "pt-center-columns" ] [
form [ _action "/small-group/member/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
style [ _scoped ] [ rawText "#name { width: 15rem; } #email { width: 20rem; }" ]
csrfToken ctx
input [ _type "hidden"; _name (nameof m.MemberId); _value (flatGuid m.MemberId) ]
@ -137,15 +135,14 @@ let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi
let logOn (groups : SmallGroup list) grpId ctx vi =
let s = I18N.localizer.Force ()
let m = { SmallGroupId = System.Guid.Empty; Password = ""; RememberMe = None }
[ form [ _action "/small-group/log-on/submit"; _method "post"; _class "pt-center-columns" ] [
[ form [ _action "/small-group/log-on/submit"; _method "post"; _class "pt-center-columns"; Target.body ] [
csrfToken ctx
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ]
seq {
match groups.Length with
| 0 -> "", s["There are no classes with passwords defined"].Value
| _ ->
if groups.Length = 0 then "", s["There are no classes with passwords defined"].Value
else
"", selectDefault s["Select Group"].Value
yield!
groups
@ -158,8 +155,8 @@ let logOn (groups : SmallGroup list) grpId ctx vi =
input [ _type "password"
_name (nameof m.Password)
_id "password"
_required;
_placeholder (s["Case-Sensitive"].Value.ToLower ()) ]
_placeholder (s["Case-Sensitive"].Value.ToLower ())
_required ]
]
]
div [ _class "pt-checkbox-field" ] [
@ -200,12 +197,12 @@ let maintain (groups : SmallGroup list) ctx vi =
$"""{s["Small Group"].Value.ToLower ()} ({g.name})""" ].Value
tr [] [
td [] [
a [ _href $"/small-group/{grpId}/edit"; _title s["Edit This Group"].Value ]
[ icon "edit" ]
a [ _href $"/small-group/{grpId}/edit"; _title s["Edit This Group"].Value ] [ icon "edit" ]
a [ _href delAction
_title s["Delete This Group"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ]
[ icon "delete_forever" ]
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
]
td [] [ str g.name ]
td [] [ str g.church.name ]
@ -216,9 +213,7 @@ let maintain (groups : SmallGroup list) ctx vi =
[ div [ _class "pt-center-text" ] [
br []
a [ _href $"/small-group/{emptyGuid}/edit"; _title s["Add a New Group"].Value ] [
icon "add_circle"
rawText " &nbsp;"
locStr s["Add a New Group"]
icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group"]
]
br []
br []
@ -256,12 +251,14 @@ let members (members : Member list) (emailTyps : Map<string, LocalizedString>) c
.Value.Replace("?", $" ({mbr.memberName})?")
tr [] [
td [] [
a [ _href $"/small-group/member/{mbrId}/edit"; _title s["Edit This Group Member"].Value ]
[ icon "edit" ]
a [ _href $"/small-group/member/{mbrId}/edit"; _title s["Edit This Group Member"].Value ] [
icon "edit"
]
a [ _href delAction
_title s["Delete This Group Member"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ]
[ icon "delete_forever" ]
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
]
td [] [ str mbr.memberName ]
td [] [ str mbr.email ]
@ -271,8 +268,9 @@ let members (members : Member list) (emailTyps : Map<string, LocalizedString>) c
]
[ div [ _class"pt-center-text" ] [
br []
a [ _href $"/small-group/member/{emptyGuid}/edit"; _title s["Add a New Group Member"].Value ]
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group Member"] ]
a [ _href $"/small-group/member/{emptyGuid}/edit"; _title s["Add a New Group Member"].Value ] [
icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group Member"]
]
br []
br []
]
@ -284,31 +282,26 @@ let members (members : Member list) (emailTyps : Map<string, LocalizedString>) c
|> Layout.standard vi "Maintain Group Members"
open Giraffe.ViewEngine.Accessibility
/// View for the small group overview page
let overview m vi =
let s = I18N.localizer.Force ()
let linkSpacer = rawText "&nbsp; "
let types = ReferenceList.requestTypeList s |> dict
article [ _class "pt-overview" ] [
section [] [
header [ _role "heading" ] [
iconSized 72 "bookmark_border"
locStr s["Quick Actions"]
]
section [ _ariaLabel "Quick actions" ] [
header [ _roleHeading ] [ iconSized 72 "bookmark_border"; locStr s["Quick Actions"] ]
div [] [
a [ _href "/prayer-requests/view" ]
[ icon "list"; linkSpacer; locStr s["View Prayer Request List"] ]
a [ _href "/prayer-requests/view" ] [ icon "list"; linkSpacer; locStr s["View Prayer Request List"] ]
hr []
a [ _href "/small-group/announcement" ] [ icon "send"; linkSpacer; locStr s["Send Announcement"] ]
hr []
a [ _href "/small-group/preferences" ] [ icon "build"; linkSpacer; locStr s["Change Preferences"] ]
]
]
section [] [
header [ _role "heading" ] [
iconSized 72 "question_answer"
locStr s["Prayer Requests"]
]
section [ _ariaLabel "Prayer requests" ] [
header [ _roleHeading ] [ iconSized 72 "question_answer"; locStr s["Prayer Requests"] ]
div [] [
p [ _class "pt-center-text" ] [
strong [] [ str (m.TotalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ]
@ -325,17 +318,12 @@ let overview m vi =
locStr s["Total Requests"]
hr []
a [ _href "/prayer-requests/maintain" ] [
icon "compare_arrows"
linkSpacer
locStr s["Maintain Prayer Requests"]
icon "compare_arrows"; linkSpacer; locStr s["Maintain Prayer Requests"]
]
]
]
section [] [
header [ _role "heading" ] [
iconSized 72 "people_outline"
locStr s["Group Members"]
]
section [ _ariaLabel "Small group members" ] [
header [ _roleHeading ] [ iconSized 72 "people_outline"; locStr s["Group Members"] ]
div [ _class "pt-center-text" ] [
strong [] [ str (m.TotalMembers.ToString "N0"); space; locStr s["Members"] ]
hr []
@ -348,15 +336,19 @@ let overview m vi =
|> Layout.standard vi "Small Group Overview"
open System.IO
open PrayerTracker
/// View for the small group preferences page
let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
let s = I18N.localizer.Force ()
let l = I18N.forView "SmallGroup/Preferences"
use sw = new StringWriter ()
let raw = rawLocText sw
[ form [ _action "/small-group/preferences/save"; _method "post"; _class "pt-center-columns" ] [
style [ _scoped ]
[ rawText "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; } #emailFromAddress { width: 20rem; } #fonts { width: 40rem; } @media screen and (max-width: 40rem) { #fonts { width: 100%; } }" ]
[ style [ _scoped ] [
rawText "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; } #emailFromAddress { width: 20rem; } #fonts { width: 40rem; } @media screen and (max-width: 40rem) { #fonts { width: 100%; } }"
]
form [ _action "/small-group/preferences/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx
fieldset [] [
legend [] [ strong [] [ icon "date_range"; rawText " &nbsp;"; locStr s["Dates"] ] ]
@ -367,11 +359,12 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
input [ _type "number"
_name (nameof m.ExpireDays)
_id "expireDays"
_value (string m.ExpireDays)
_min "1"; _max "30"
_required
_autofocus
_value (string m.ExpireDays) ]
space; str (s["Days"].Value.ToLower ())
_autofocus ]
space
str (s["Days"].Value.ToLower ())
]
]
div [ _class "pt-field" ] [
@ -381,8 +374,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.DaysToKeepNew)
_id "daysToKeepNew"
_min "1"; _max "30"
_required
_value (string m.DaysToKeepNew) ]
_value (string m.DaysToKeepNew)
_required ]
space; str (s["Days"].Value.ToLower ())
]
]
@ -393,8 +386,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.LongTermUpdateWeeks)
_id "longTermUpdateWeeks"
_min "1"; _max "30"
_required
_value (string m.LongTermUpdateWeeks) ]
_value (string m.LongTermUpdateWeeks)
_required ]
space; str (s["Weeks"].Value.ToLower ())
]
]
@ -416,16 +409,16 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
input [ _type "text"
_name (nameof m.EmailFromName)
_id "emailFromName"
_required
_value m.EmailFromName ]
_value m.EmailFromName
_required ]
]
div [ _class "pt-field" ] [
label [ _for "emailFromAddress" ] [ locStr s["From Address"] ]
input [ _type "email"
_name (nameof m.EmailFromAddress)
_id "emailFromAddress"
_required
_value m.EmailFromAddress ]
_value m.EmailFromAddress
_required ]
]
]
div [ _class "pt-field-row" ] [
@ -450,8 +443,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
span [] [
radio (nameof m.LineColorType) "lineColorType_Name" "Name" m.LineColorType
label [ _for "lineColorType_Name" ] [ locStr s["Named Color"] ]
namedColorList (nameof m.LineColor) m.LineColor
[ _id "lineColor_Select"
namedColorList (nameof m.LineColor) m.LineColor [
_id "lineColor_Select"
if m.LineColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio (nameof m.LineColorType) "lineColorType_RGB" "RGB" m.LineColorType
@ -470,8 +463,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
span [] [
radio (nameof m.HeadingColorType) "headingColorType_Name" "Name" m.HeadingColorType
label [ _for "headingColorType_Name" ] [ locStr s["Named Color"] ]
namedColorList (nameof m.HeadingColor) m.HeadingColor
[ _id "headingColor_Select"
namedColorList (nameof m.HeadingColor) m.HeadingColor [
_id "headingColor_Select"
if m.HeadingColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio (nameof m.HeadingColorType) "headingColorType_RGB" "RGB" m.HeadingColorType
@ -498,8 +491,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.HeadingFontSize)
_id "headingFontSize"
_min "8"; _max "24"
_required
_value (string m.HeadingFontSize) ]
_value (string m.HeadingFontSize)
_required ]
]
div [ _class "pt-field" ] [
label [ _for "listFontSize" ] [ locStr s["List Text Size"] ]
@ -507,8 +500,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.ListFontSize)
_id "listFontSize"
_min "8"; _max "24"
_required
_value (string m.ListFontSize) ]
_value (string m.ListFontSize)
_required ]
]
]
]
@ -527,7 +520,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
div [ _class "pt-field" ] [
label [] [ locStr s["Request List Visibility"] ]
span [] [
radio (nameof m.Visibility) "viz_Public" (string RequestVisibility.``public``) (string m.Visibility)
radio (nameof m.Visibility) "viz_Public" (string RequestVisibility.``public``)
(string m.Visibility)
label [ _for "viz_Public" ] [ locStr s["Public"] ]
rawText " &nbsp;"
radio (nameof m.Visibility) "viz_Private" (string RequestVisibility.``private``)
@ -556,8 +550,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.PageSize)
_id "pageSize"
_min "10"; _max "255"
_required
_value (string m.PageSize) ]
_value (string m.PageSize)
_required ]
]
div [ _class "pt-field" ] [
label [ _for (nameof m.AsOfDate) ] [ locStr s["“As of” Date Display"] ]

View File

@ -1,14 +1,13 @@
module PrayerTracker.Views.User
open Giraffe.ViewEngine
open PrayerTracker.Entities
open PrayerTracker.ViewModels
/// View for the group assignment page
let assignGroups m groups curGroups ctx vi =
let s = I18N.localizer.Force ()
let pageTitle = sprintf "%s %A" m.UserName s["Assign Groups"]
form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns" ] [
form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx
input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ]
input [ _type "hidden"; _name (nameof m.UserName); _value m.UserName ]
@ -48,10 +47,11 @@ let changePassword ctx vi =
[ p [ _class "pt-center-text" ] [
locStr s["To change your password, enter your current password in the specified box below, then enter your new password twice."]
]
style [ _scoped ] [ rawText "#oldPassword, #newPassword, #newPasswordConfirm { width: 10rem; } "]
form [ _action "/user/password/change"
_method "post"
_onsubmit $"""return PT.compareValidation('newPassword','newPasswordConfirm','%A{s["The passwords do not match"]}')""" ] [
style [ _scoped ] [ rawText "#oldPassword, #newPassword, #newPasswordConfirm { width: 10rem; } "]
_onsubmit $"""return PT.compareValidation('newPassword','newPasswordConfirm','%A{s["The passwords do not match"]}')"""
Target.content ] [
csrfToken ctx
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
@ -84,10 +84,13 @@ let edit (m : EditUser) ctx vi =
let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New User" else "Edit User"
let pwPlaceholder = s[if m.IsNew then "" else "No change"].Value
[ form [ _action "/user/edit/save"; _method "post"; _class "pt-center-columns"
_onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')""" ] [
style [ _scoped ]
[ style [ _scoped ]
[ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #email { width: 20rem; } " ]
form [ _action "/user/edit/save"
_method "post"
_class "pt-center-columns"
_onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')"""
Target.content ] [
csrfToken ctx
input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ]
div [ _class "pt-field-row" ] [
@ -141,7 +144,7 @@ let edit (m : EditUser) ctx vi =
/// View for the user log on page
let logOn (m : UserLogOn) groups ctx vi =
let s = I18N.localizer.Force ()
form [ _action "/user/log-on"; _method "post"; _class "pt-center-columns" ] [
form [ _action "/user/log-on"; _method "post"; _class "pt-center-columns"; Target.body ] [
style [ _scoped ] [ rawText "#email { width: 20rem; }" ]
csrfToken ctx
input [ _type "hidden"; _name (nameof m.RedirectUrl); _value (defaultArg m.RedirectUrl "") ]
@ -155,17 +158,14 @@ let logOn (m : UserLogOn) groups ctx vi =
input [ _type "password"
_name (nameof m.Password)
_id "password"
_required;
_placeholder (sprintf "(%s)" (s["Case-Sensitive"].Value.ToLower ())) ]
_placeholder $"""({s["Case-Sensitive"].Value.ToLower ()})"""
_required ]
]
]
div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [
label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ]
seq {
"", selectDefault s["Select Group"].Value
yield! groups
}
seq { "", selectDefault s["Select Group"].Value; yield! groups }
|> selectList (nameof m.SmallGroupId) "" [ _required ]
]
]
@ -173,7 +173,7 @@ let logOn (m : UserLogOn) groups ctx vi =
input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ]
label [ _for "rememberMe" ] [ locStr s["Remember Me"] ]
br []
small [] [ em [] [ rawText "("; str (s["Requires Cookies"].Value.ToLower ()); rawText ")" ] ]
small [] [ em [] [ str $"""({s["Requires Cookies"].Value.ToLower ()})""" ] ]
]
div [ _class "pt-field-row" ] [ submit [] "account_circle" s["Log On"] ]
]
@ -182,6 +182,8 @@ let logOn (m : UserLogOn) groups ctx vi =
|> Layout.standard vi "User Log On"
open PrayerTracker.Entities
/// View for the user maintenance page
let maintain (users : User list) ctx vi =
let s = I18N.localizer.Force ()
@ -206,12 +208,14 @@ let maintain (users : User list) ctx vi =
tr [] [
td [] [
a [ _href $"/user/{userId}/edit"; _title s["Edit This User"].Value ] [ icon "edit" ]
a [ _href $"/user/{userId}/small-groups"; _title s["Assign Groups to This User"].Value ]
[ icon "group" ]
a [ _href $"/user/{userId}/small-groups"; _title s["Assign Groups to This User"].Value ] [
icon "group"
]
a [ _href delAction
_title s["Delete This User"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ]
[ icon "delete_forever" ]
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
]
td [] [ str user.fullName ]
td [ _class "pt-center-text" ] [
@ -222,8 +226,9 @@ let maintain (users : User list) ctx vi =
]
[ div [ _class "pt-center-text" ] [
br []
a [ _href $"/user/{emptyGuid}/edit"; _title s["Add a New User"].Value ]
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New User"] ]
a [ _href $"/user/{emptyGuid}/edit"; _title s["Add a New User"].Value ] [
icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New User"]
]
br []
br []
]

View File

@ -103,6 +103,18 @@ module UserMessage =
Description = None
}
/// The template with which the content will be rendered
type LayoutType =
/// A full page load
| FullPage
/// A response that will provide a new body tag
| PartialPage
/// A response that will replace the page content
| ContentOnly
open System
@ -132,6 +144,9 @@ type AppViewInfo =
/// The currently logged on small group, if there is one
Group : SmallGroup option
/// The layout with which the content will be rendered
Layout : LayoutType
}
/// Support for the AppViewInfo type
@ -147,6 +162,7 @@ module AppViewInfo =
RequestStart = DateTime.Now.Ticks
User = None
Group = None
Layout = FullPage
}

View File

@ -18,7 +18,7 @@ let private findStats (db : AppDbContext) churchId = task {
/// POST /church/[church-id]/delete
let delete churchId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task {
let delete churchId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.db.TryChurchById churchId with
| Some church ->
let! _, stats = findStats ctx.db churchId
@ -66,7 +66,7 @@ let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
/// POST /church/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task {
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditChurch> () with
| Ok m ->
let! church =

View File

@ -2,20 +2,7 @@
[<AutoOpen>]
module PrayerTracker.Handlers.CommonFunctions
open System
open System.Net
open System.Reflection
open System.Threading.Tasks
open Giraffe
open Microsoft.AspNetCore.Antiforgery
open Microsoft.AspNetCore.Html
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Http.Extensions
open Microsoft.AspNetCore.Mvc.Rendering
open Microsoft.Extensions.Localization
open PrayerTracker
open PrayerTracker.Cookies
open PrayerTracker.ViewModels
/// Create a select list from an enumeration
let toSelectList<'T> valFunc textFunc withDefault emptyText (items : 'T seq) =
@ -38,7 +25,7 @@ let toSelectListWithDefault<'T> valFunc textFunc (items : 'T seq) =
/// The version of PrayerTracker
let appVersion =
let v = Assembly.GetExecutingAssembly().GetName().Version
let v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version
#if (DEBUG)
$"v{v}"
#else
@ -53,6 +40,10 @@ let appVersion =
|> String.concat ""
#endif
open Microsoft.AspNetCore.Http
open PrayerTracker
/// The currently signed-in user (will raise if none exists)
let currentUser (ctx : HttpContext) =
match ctx.Session.user with Some u -> u | None -> nullArg "User"
@ -61,6 +52,12 @@ let currentUser (ctx : HttpContext) =
let currentGroup (ctx : HttpContext) =
match ctx.Session.smallGroup with Some g -> g | None -> nullArg "SmallGroup"
open System
open Giraffe
open PrayerTracker.Cookies
open PrayerTracker.ViewModels
/// Create the common view information heading
let viewInfo (ctx : HttpContext) startTicks =
let msg =
@ -84,12 +81,18 @@ let viewInfo (ctx : HttpContext) startTicks =
CookieOptions (Expires = Nullable<DateTimeOffset> (DateTimeOffset (DateTime timeout.Until)),
HttpOnly = true))
| None -> ()
let layout =
match ctx.TryGetRequestHeader "X-Target" with
| Some hdr when hdr = "#pt-body" -> ContentOnly
| Some _ -> PartialPage
| None -> FullPage
{ AppViewInfo.fresh with
Version = appVersion
Messages = msg
RequestStart = startTicks
User = ctx.Session.user
Group = ctx.Session.smallGroup
Layout = layout
}
/// The view is the last parameter, so it can be composed
@ -107,23 +110,24 @@ let fourOhFour next (ctx : HttpContext) =
ctx.SetStatusCode 404
text "Not Found" next ctx
/// Handler to validate CSRF prevention token
let validateCSRF : HttpHandler = fun next ctx -> task {
match! (ctx.GetService<IAntiforgery> ()).IsRequestValidAsync ctx with
let validateCsrf : HttpHandler = fun next ctx -> task {
match! (ctx.GetService<Microsoft.AspNetCore.Antiforgery.IAntiforgery> ()).IsRequestValidAsync ctx with
| true -> return! next ctx
| false ->
return! (clearResponse >=> setStatusCode 400 >=> text "Quit hacking...") (fun _ -> Task.FromResult None) ctx
| false -> return! (clearResponse >=> setStatusCode 400 >=> text "Quit hacking...") earlyReturn ctx
}
/// Add a message to the session
let addUserMessage (ctx : HttpContext) msg =
ctx.Session.messages <- msg :: ctx.Session.messages
open Microsoft.AspNetCore.Html
open Microsoft.Extensions.Localization
/// Convert a localized string to an HTML string
let htmlLocString (x : LocalizedString) =
(WebUtility.HtmlEncode >> HtmlString) x.Value
(System.Net.WebUtility.HtmlEncode >> HtmlString) x.Value
let htmlString (x : LocalizedString) =
HtmlString x.Value
@ -157,6 +161,8 @@ type AccessLevel =
| Public
open Microsoft.AspNetCore.Http.Extensions
/// Require the given access role (also refreshes "Remember Me" user and group logons)
let requireAccess level : HttpHandler =

View File

@ -94,7 +94,7 @@ let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
/// POST /prayer-request/[request-id]/delete
let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task {
let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! findRequest ctx reqId with
| Ok req ->
let s = Views.I18N.localizer.Force ()
@ -215,7 +215,7 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas
/// POST /prayer-request/save
let save : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task {
let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditRequest> () with
| Ok m ->
let! req =

View File

@ -26,6 +26,7 @@
<ItemGroup>
<PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
<PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup>

View File

@ -27,7 +27,7 @@ let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
/// POST /small-group/[group-id]/delete
let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task {
let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
let s = Views.I18N.localizer.Force ()
match! ctx.db.TryGroupById groupId with
| Some grp ->
@ -44,7 +44,7 @@ let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=>
/// POST /small-group/member/[member-id]/delete
let deleteMember memberId : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task {
let deleteMember memberId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
let s = Views.I18N.localizer.Force ()
match! ctx.db.TryMemberById memberId with
| Some mbr when mbr.smallGroupId = (currentGroup ctx).smallGroupId ->
@ -113,7 +113,7 @@ let logOn (groupId : SmallGroupId option) : HttpHandler = requireAccess [ Access
/// POST /small-group/log-on/submit
let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSRF >=> fun next ctx -> task {
let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<GroupLogOn> () with
| Ok m ->
let s = Views.I18N.localizer.Force ()
@ -193,7 +193,7 @@ let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task
/// POST /small-group/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task {
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditSmallGroup> () with
| Ok m ->
let s = Views.I18N.localizer.Force ()
@ -218,7 +218,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
/// POST /small-group/member/save
let saveMember : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task {
let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditMember> () with
| Ok m ->
let grp = currentGroup ctx
@ -246,7 +246,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun n
/// POST /small-group/preferences/save
let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task {
let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditPreferences> () with
| Ok m ->
// Since the class is stored in the session, we'll use an intermediate instance to persist it; once that works,
@ -268,7 +268,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=>
/// POST /small-group/announcement/send
let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task {
let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
let startTicks = DateTime.Now.Ticks
match! ctx.TryBindFormAsync<Announcement> () with
| Ok m ->

View File

@ -44,7 +44,7 @@ let private findUserByPassword m (db : AppDbContext) = task {
/// POST /user/password/change
let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task {
let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<ChangePassword> () with
| Ok m ->
let s = Views.I18N.localizer.Force ()
@ -81,7 +81,7 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> f
/// POST /user/[user-id]/delete
let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task {
let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.db.TryUserById userId with
| Some user ->
ctx.db.RemoveEntry user
@ -94,7 +94,7 @@ let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> f
/// POST /user/log-on
let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSRF >=> fun next ctx -> task {
let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<UserLogOn> () with
| Ok m ->
let s = Views.I18N.localizer.Force ()
@ -192,7 +192,7 @@ let password : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
/// POST /user/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task {
let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditUser> () with
| Ok m ->
let! user =
@ -235,7 +235,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
/// POST /user/small-groups/save
let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task {
let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<AssignGroups> () with
| Ok m ->
let s = Views.I18N.localizer.Force ()