Version 3.1 #71

Merged
danieljsummers merged 9 commits from v3.1 into main 2022-07-30 21:02:58 +00:00
6 changed files with 281 additions and 258 deletions
Showing only changes of commit 9b85ac2412 - Show all commits

View File

@ -1,42 +1,11 @@
open MyPrayerJournal.Domain open MyPrayerJournal.Domain
open NodaTime open NodaTime
/// Request is the identifying record for a prayer request
[<CLIMutable; NoComparison; NoEquality>]
type OldRequest =
{ /// The ID of the request
id : RequestId
/// The time this request was initially entered
enteredOn : Instant
/// The ID of the user to whom this request belongs ("sub" from the JWT)
userId : UserId
/// The time at which this request should reappear in the user's journal by manual user choice
snoozedUntil : Instant
/// The time at which this request should reappear in the user's journal by recurrence
showAfter : Instant
/// The type of recurrence for this request
recurType : string
/// How many of the recurrence intervals should occur between appearances in the journal
recurCount : int16
/// The history entries for this request
history : History array
/// The notes for this request
notes : Note array
}
/// The old definition of the history entry /// The old definition of the history entry
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type OldHistory = type OldHistory =
{ /// The time when this history entry was made { /// The time when this history entry was made
asOf : Instant asOf : int64
/// The status for this history entry /// The status for this history entry
status : RequestAction status : RequestAction
/// The text of the update, if applicable /// The text of the update, if applicable
@ -47,12 +16,43 @@ type OldHistory =
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type OldNote = type OldNote =
{ /// The time when this note was made { /// The time when this note was made
asOf : Instant asOf : int64
/// The text of the notes /// The text of the notes
notes : string notes : string
} }
/// Request is the identifying record for a prayer request
[<CLIMutable; NoComparison; NoEquality>]
type OldRequest =
{ /// The ID of the request
id : RequestId
/// The time this request was initially entered
enteredOn : int64
/// The ID of the user to whom this request belongs ("sub" from the JWT)
userId : UserId
/// The time at which this request should reappear in the user's journal by manual user choice
snoozedUntil : int64
/// The time at which this request should reappear in the user's journal by recurrence
showAfter : int64
/// The type of recurrence for this request
recurType : string
/// How many of the recurrence intervals should occur between appearances in the journal
recurCount : int16
/// The history entries for this request
history : OldHistory[]
/// The notes for this request
notes : OldNote[]
}
open LiteDB open LiteDB
open MyPrayerJournal.Data open MyPrayerJournal.Data
@ -68,27 +68,41 @@ let mapRecurrence old =
| "Weeks" -> Weeks old.recurCount | "Weeks" -> Weeks old.recurCount
| _ -> Immediate | _ -> Immediate
/// Convert an old history entry to the new form
let convertHistory (old : OldHistory) =
{ AsOf = Instant.FromUnixTimeMilliseconds old.asOf
Status = old.status
Text = old.text
}
/// Convert an old note to the new form
let convertNote (old : OldNote) =
{ AsOf = Instant.FromUnixTimeMilliseconds old.asOf
Notes = old.notes
}
/// Map the old request to the new request /// Map the old request to the new request
let convert old = let convert old =
{ id = old.id { Id = old.id
enteredOn = old.enteredOn EnteredOn = Instant.FromUnixTimeMilliseconds old.enteredOn
userId = old.userId UserId = old.userId
snoozedUntil = old.snoozedUntil SnoozedUntil = Instant.FromUnixTimeMilliseconds old.snoozedUntil
showAfter = old.showAfter ShowAfter = Instant.FromUnixTimeMilliseconds old.showAfter
recurrence = mapRecurrence old Recurrence = mapRecurrence old
history = Array.toList old.history History = old.history |> Array.map convertHistory |> List.ofArray
notes = Array.toList old.notes Notes = old.notes |> Array.map convertNote |> List.ofArray
} }
/// Remove the old request, add the converted one (removes recurType / recurCount fields) /// Remove the old request, add the converted one (removes recurType / recurCount fields)
let replace (req : Request) = let replace (req : Request) =
db.requests.Delete(Mapping.RequestId.toBson req.id) |> ignore db.requests.Delete (Mapping.RequestId.toBson req.Id) |> ignore
db.requests.Insert(req) |> ignore db.requests.Insert req |> ignore
db.Checkpoint() db.Checkpoint ()
db.GetCollection<OldRequest>("request").FindAll() db.GetCollection<OldRequest>("request").FindAll ()
|> Seq.map convert |> Seq.map convert
|> Seq.iter replace |> List.ofSeq
|> List.iter replace
// For more information see https://aka.ms/fsharp-console-apps // For more information see https://aka.ms/fsharp-console-apps
printfn "Done" printfn "Done"

View File

@ -4,6 +4,7 @@ open LiteDB
open MyPrayerJournal open MyPrayerJournal
open NodaTime open NodaTime
open System.Threading.Tasks open System.Threading.Tasks
open NodaTime.Text
// fsharplint:disable MemberNames // fsharplint:disable MemberNames
@ -29,10 +30,13 @@ module Extensions =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Mapping = module Mapping =
/// A NodaTime instant pattern to use for parsing instants from the database
let instantPattern = InstantPattern.CreateWithInvariantCulture "g"
/// Mapping for NodaTime's Instant type /// Mapping for NodaTime's Instant type
module Instant = module Instant =
let fromBson (value : BsonValue) = Instant.FromUnixTimeMilliseconds value.AsInt64 let fromBson (value : BsonValue) = (instantPattern.Parse value.AsString).Value
let toBson (value : Instant) : BsonValue = value.ToUnixTimeMilliseconds () let toBson (value : Instant) : BsonValue = value.ToString ("g", null)
/// Mapping for option types /// Mapping for option types
module Option = module Option =
@ -73,7 +77,7 @@ module Startup =
/// Ensure the database is set up /// Ensure the database is set up
let ensureDb (db : LiteDatabase) = let ensureDb (db : LiteDatabase) =
db.requests.EnsureIndex (fun it -> it.userId) |> ignore db.requests.EnsureIndex (fun it -> it.UserId) |> ignore
Mapping.register () Mapping.register ()
@ -100,20 +104,20 @@ module private Helpers =
/// Retrieve a request, including its history and notes, by its ID and user ID /// Retrieve a request, including its history and notes, by its ID and user ID
let tryFullRequestById reqId userId (db : LiteDatabase) = backgroundTask { let tryFullRequestById reqId userId (db : LiteDatabase) = backgroundTask {
let! req = db.requests.Find (Query.EQ ("_id", RequestId.toString reqId)) |> firstAsync let! req = db.requests.Find (Query.EQ ("_id", RequestId.toString reqId)) |> firstAsync
return match box req with null -> None | _ when req.userId = userId -> Some req | _ -> None return match box req with null -> None | _ when req.UserId = userId -> Some req | _ -> None
} }
/// Add a history entry /// Add a history entry
let addHistory reqId userId hist db = backgroundTask { let addHistory reqId userId hist db = backgroundTask {
match! tryFullRequestById reqId userId db with match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with history = hist :: req.history } | Some req -> do! doUpdate db { req with History = hist :: req.History }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
} }
/// Add a note /// Add a note
let addNote reqId userId note db = backgroundTask { let addNote reqId userId note db = backgroundTask {
match! tryFullRequestById reqId userId db with match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with notes = note :: req.notes } | Some req -> do! doUpdate db { req with Notes = note :: req.Notes }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
} }
@ -129,8 +133,8 @@ let answeredRequests userId (db : LiteDatabase) = backgroundTask {
return return
reqs reqs
|> Seq.map JournalRequest.ofRequestFull |> Seq.map JournalRequest.ofRequestFull
|> Seq.filter (fun it -> it.lastStatus = Answered) |> Seq.filter (fun it -> it.LastStatus = Answered)
|> Seq.sortByDescending (fun it -> it.asOf) |> Seq.sortByDescending (fun it -> it.AsOf)
|> List.ofSeq |> List.ofSeq
} }
@ -140,26 +144,26 @@ let journalByUserId userId (db : LiteDatabase) = backgroundTask {
return return
jrnl jrnl
|> Seq.map JournalRequest.ofRequestLite |> Seq.map JournalRequest.ofRequestLite
|> Seq.filter (fun it -> it.lastStatus <> Answered) |> Seq.filter (fun it -> it.LastStatus <> Answered)
|> Seq.sortBy (fun it -> it.asOf) |> Seq.sortBy (fun it -> it.AsOf)
|> List.ofSeq |> List.ofSeq
} }
/// Does the user have any snoozed requests? /// Does the user have any snoozed requests?
let hasSnoozed userId now (db : LiteDatabase) = backgroundTask { let hasSnoozed userId now (db : LiteDatabase) = backgroundTask {
let! jrnl = journalByUserId userId db let! jrnl = journalByUserId userId db
return jrnl |> List.exists (fun r -> r.snoozedUntil > now) return jrnl |> List.exists (fun r -> r.SnoozedUntil > now)
} }
/// Retrieve a request by its ID and user ID (without notes and history) /// Retrieve a request by its ID and user ID (without notes and history)
let tryRequestById reqId userId db = backgroundTask { let tryRequestById reqId userId db = backgroundTask {
let! req = tryFullRequestById reqId userId db let! req = tryFullRequestById reqId userId db
return req |> Option.map (fun r -> { r with history = []; notes = [] }) return req |> Option.map (fun r -> { r with History = []; Notes = [] })
} }
/// Retrieve notes for a request by its ID and user ID /// Retrieve notes for a request by its ID and user ID
let notesById reqId userId (db : LiteDatabase) = backgroundTask { let notesById reqId userId (db : LiteDatabase) = backgroundTask {
match! tryFullRequestById reqId userId db with | Some req -> return req.notes | None -> return [] match! tryFullRequestById reqId userId db with | Some req -> return req.Notes | None -> return []
} }
/// Retrieve a journal request by its ID and user ID /// Retrieve a journal request by its ID and user ID
@ -171,20 +175,20 @@ let tryJournalById reqId userId (db : LiteDatabase) = backgroundTask {
/// Update the recurrence for a request /// Update the recurrence for a request
let updateRecurrence reqId userId recurType db = backgroundTask { let updateRecurrence reqId userId recurType db = backgroundTask {
match! tryFullRequestById reqId userId db with match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with recurrence = recurType } | Some req -> do! doUpdate db { req with Recurrence = recurType }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
} }
/// Update a snoozed request /// Update a snoozed request
let updateSnoozed reqId userId until db = backgroundTask { let updateSnoozed reqId userId until db = backgroundTask {
match! tryFullRequestById reqId userId db with match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with snoozedUntil = until; showAfter = until } | Some req -> do! doUpdate db { req with SnoozedUntil = until; ShowAfter = until }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
} }
/// Update the "show after" timestamp for a request /// Update the "show after" timestamp for a request
let updateShowAfter reqId userId showAfter db = backgroundTask { let updateShowAfter reqId userId showAfter db = backgroundTask {
match! tryFullRequestById reqId userId db with match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with showAfter = showAfter } | Some req -> do! doUpdate db { req with ShowAfter = showAfter }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
} }

View File

@ -2,8 +2,6 @@
[<AutoOpen>] [<AutoOpen>]
module MyPrayerJournal.Domain module MyPrayerJournal.Domain
// fsharplint:disable RecordFieldNames
open System open System
open Cuid open Cuid
open NodaTime open NodaTime
@ -33,12 +31,16 @@ module UserId =
/// How frequently a request should reappear after it is marked "Prayed" /// How frequently a request should reappear after it is marked "Prayed"
type Recurrence = type Recurrence =
/// A request should reappear immediately at the bottom of the list /// A request should reappear immediately at the bottom of the list
| Immediate | Immediate
/// A request should reappear in the given number of hours /// A request should reappear in the given number of hours
| Hours of int16 | Hours of int16
/// A request should reappear in the given number of days /// A request should reappear in the given number of days
| Days of int16 | Days of int16
/// A request should reappear in the given number of weeks (7-day increments) /// A request should reappear in the given number of weeks (7-day increments)
| Weeks of int16 | Weeks of int16
@ -86,142 +88,6 @@ type RequestAction =
| Updated | Updated
| Answered | Answered
/// History is a record of action taken on a prayer request, including updates to its text
[<CLIMutable; NoComparison; NoEquality>]
type History =
{ /// The time when this history entry was made
asOf : Instant
/// The status for this history entry
status : RequestAction
/// The text of the update, if applicable
text : string option
}
/// Note is a note regarding a prayer request that does not result in an update to its text
[<CLIMutable; NoComparison; NoEquality>]
type Note =
{ /// The time when this note was made
asOf : Instant
/// The text of the notes
notes : string
}
/// Request is the identifying record for a prayer request
[<CLIMutable; NoComparison; NoEquality>]
type Request =
{ /// The ID of the request
id : RequestId
/// The time this request was initially entered
enteredOn : Instant
/// The ID of the user to whom this request belongs ("sub" from the JWT)
userId : UserId
/// The time at which this request should reappear in the user's journal by manual user choice
snoozedUntil : Instant
/// The time at which this request should reappear in the user's journal by recurrence
showAfter : Instant
/// The recurrence for this request
recurrence : Recurrence
/// The history entries for this request
history : History list
/// The notes for this request
notes : Note list
}
/// Functions to support requests
module Request =
/// An empty request
let empty =
{ id = Cuid.generate () |> RequestId
enteredOn = Instant.MinValue
userId = UserId ""
snoozedUntil = Instant.MinValue
showAfter = Instant.MinValue
recurrence = Immediate
history = []
notes = []
}
/// JournalRequest is the form of a prayer request returned for the request journal display. It also contains
/// properties that may be filled for history and notes.
[<NoComparison; NoEquality>]
type JournalRequest =
{ /// The ID of the request (just the CUID part)
requestId : RequestId
/// The ID of the user to whom the request belongs
userId : UserId
/// The current text of the request
text : string
/// The last time action was taken on the request
asOf : Instant
/// The last status for the request
lastStatus : RequestAction
/// The time that this request should reappear in the user's journal
snoozedUntil : Instant
/// The time after which this request should reappear in the user's journal by configured recurrence
showAfter : Instant
/// The recurrence for this request
recurrence : Recurrence
/// History entries for the request
history : History list
/// Note entries for the request
notes : Note list
}
/// Functions to manipulate journal requests
module JournalRequest =
/// Convert a request to the form used for the journal (precomputed values, no notes or history)
let ofRequestLite (req : Request) =
let hist = req.history |> List.sortByDescending (fun it -> it.asOf) |> List.tryHead
{ requestId = req.id
userId = req.userId
text = req.history
|> List.filter (fun it -> Option.isSome it.text)
|> List.sortByDescending (fun it -> it.asOf)
|> List.tryHead
|> Option.map (fun h -> Option.get h.text)
|> Option.defaultValue ""
asOf = match hist with Some h -> h.asOf | None -> Instant.MinValue
lastStatus = match hist with Some h -> h.status | None -> Created
snoozedUntil = req.snoozedUntil
showAfter = req.showAfter
recurrence = req.recurrence
history = []
notes = []
}
/// Same as `ofRequestLite`, but with notes and history
let ofRequestFull req =
{ ofRequestLite req with
history = req.history
notes = req.notes
}
/// Functions to manipulate request actions /// Functions to manipulate request actions
module RequestAction = module RequestAction =
@ -242,11 +108,150 @@ module RequestAction =
| "Answered" -> Answered | "Answered" -> Answered
| it -> invalidOp $"Bad request action {it}" | it -> invalidOp $"Bad request action {it}"
/// History is a record of action taken on a prayer request, including updates to its text
[<CLIMutable; NoComparison; NoEquality>]
type History =
{ /// The time when this history entry was made
AsOf : Instant
/// The status for this history entry
Status : RequestAction
/// The text of the update, if applicable
Text : string option
}
/// Functions to manipulate history entries
module History =
/// Determine if a history's status is `Created` /// Determine if a history's status is `Created`
let isCreated hist = hist.status = Created let isCreated hist = hist.Status = Created
/// Determine if a history's status is `Prayed` /// Determine if a history's status is `Prayed`
let isPrayed hist = hist.status = Prayed let isPrayed hist = hist.Status = Prayed
/// Determine if a history's status is `Answered` /// Determine if a history's status is `Answered`
let isAnswered hist = hist.status = Answered let isAnswered hist = hist.Status = Answered
/// Note is a note regarding a prayer request that does not result in an update to its text
[<CLIMutable; NoComparison; NoEquality>]
type Note =
{ /// The time when this note was made
AsOf : Instant
/// The text of the notes
Notes : string
}
/// Request is the identifying record for a prayer request
[<CLIMutable; NoComparison; NoEquality>]
type Request =
{ /// The ID of the request
Id : RequestId
/// The time this request was initially entered
EnteredOn : Instant
/// The ID of the user to whom this request belongs ("sub" from the JWT)
UserId : UserId
/// The time at which this request should reappear in the user's journal by manual user choice
SnoozedUntil : Instant
/// The time at which this request should reappear in the user's journal by recurrence
ShowAfter : Instant
/// The recurrence for this request
Recurrence : Recurrence
/// The history entries for this request
History : History list
/// The notes for this request
Notes : Note list
}
/// Functions to support requests
module Request =
/// An empty request
let empty =
{ Id = Cuid.generate () |> RequestId
EnteredOn = Instant.MinValue
UserId = UserId ""
SnoozedUntil = Instant.MinValue
ShowAfter = Instant.MinValue
Recurrence = Immediate
History = []
Notes = []
}
/// JournalRequest is the form of a prayer request returned for the request journal display. It also contains
/// properties that may be filled for history and notes.
[<NoComparison; NoEquality>]
type JournalRequest =
{ /// The ID of the request (just the CUID part)
RequestId : RequestId
/// The ID of the user to whom the request belongs
UserId : UserId
/// The current text of the request
Text : string
/// The last time action was taken on the request
AsOf : Instant
/// The last status for the request
LastStatus : RequestAction
/// The time that this request should reappear in the user's journal
SnoozedUntil : Instant
/// The time after which this request should reappear in the user's journal by configured recurrence
ShowAfter : Instant
/// The recurrence for this request
Recurrence : Recurrence
/// History entries for the request
History : History list
/// Note entries for the request
Notes : Note list
}
/// Functions to manipulate journal requests
module JournalRequest =
/// Convert a request to the form used for the journal (precomputed values, no notes or history)
let ofRequestLite (req : Request) =
let hist = req.History |> List.sortByDescending (fun it -> it.AsOf) |> List.tryHead
{ RequestId = req.Id
UserId = req.UserId
Text = req.History
|> List.filter (fun it -> Option.isSome it.Text)
|> List.sortByDescending (fun it -> it.AsOf)
|> List.tryHead
|> Option.map (fun h -> Option.get h.Text)
|> Option.defaultValue ""
AsOf = match hist with Some h -> h.AsOf | None -> Instant.MinValue
LastStatus = match hist with Some h -> h.Status | None -> Created
SnoozedUntil = req.SnoozedUntil
ShowAfter = req.ShowAfter
Recurrence = req.Recurrence
History = []
Notes = []
}
/// Same as `ofRequestLite`, but with notes and history
let ofRequestFull req =
{ ofRequestLite req with
History = req.History
Notes = req.Notes
}

View File

@ -237,7 +237,7 @@ module Components =
let journalItems : HttpHandler = requiresAuthentication Error.notAuthorized >=> fun next ctx -> backgroundTask { let journalItems : HttpHandler = requiresAuthentication Error.notAuthorized >=> fun next ctx -> backgroundTask {
let now = now ctx let now = now ctx
let! jrnl = Data.journalByUserId (userId ctx) (db ctx) let! jrnl = Data.journalByUserId (userId ctx) (db ctx)
let shown = jrnl |> List.filter (fun it -> now > it.snoozedUntil && now > it.showAfter) let shown = jrnl |> List.filter (fun it -> now > it.SnoozedUntil && now > it.ShowAfter)
return! renderComponent [ Views.Journal.journalItems now shown ] next ctx return! renderComponent [ Views.Journal.journalItems now shown ] next ctx
} }
@ -332,9 +332,9 @@ module Request =
match! Data.tryRequestById reqId usrId db with match! Data.tryRequestById reqId usrId db with
| Some req -> | Some req ->
let now = now ctx let now = now ctx
do! Data.addHistory reqId usrId { asOf = now; status = Prayed; text = None } db do! Data.addHistory reqId usrId { AsOf = now; Status = Prayed; Text = None } db
let nextShow = let nextShow =
match Recurrence.duration req.recurrence with match Recurrence.duration req.Recurrence with
| 0L -> Instant.MinValue | 0L -> Instant.MinValue
| duration -> now.Plus (Duration.FromSeconds duration) | duration -> now.Plus (Duration.FromSeconds duration)
do! Data.updateShowAfter reqId usrId nextShow db do! Data.updateShowAfter reqId usrId nextShow db
@ -351,7 +351,7 @@ module Request =
match! Data.tryRequestById reqId usrId db with match! Data.tryRequestById reqId usrId db with
| Some _ -> | Some _ ->
let! notes = ctx.BindFormAsync<Models.NoteEntry> () let! notes = ctx.BindFormAsync<Models.NoteEntry> ()
do! Data.addNote reqId usrId { asOf = now ctx; notes = notes.notes } db do! Data.addNote reqId usrId { AsOf = now ctx; Notes = notes.notes } db
do! db.saveChanges () do! db.saveChanges ()
return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
@ -367,7 +367,7 @@ module Request =
let snoozed : HttpHandler = requiresAuthentication Error.notAuthorized >=> fun next ctx -> backgroundTask { let snoozed : HttpHandler = requiresAuthentication Error.notAuthorized >=> fun next ctx -> backgroundTask {
let! reqs = Data.journalByUserId (userId ctx) (db ctx) let! reqs = Data.journalByUserId (userId ctx) (db ctx)
let now = now ctx let now = now ctx
let snoozed = reqs |> List.filter (fun it -> it.snoozedUntil > now) let snoozed = reqs |> List.filter (fun it -> it.SnoozedUntil > now)
return! partial "Active Requests" (Views.Request.snoozed now snoozed) next ctx return! partial "Active Requests" (Views.Request.snoozed now snoozed) next ctx
} }
@ -444,14 +444,14 @@ module Request =
let now = now ctx let now = now ctx
let req = let req =
{ Request.empty with { Request.empty with
userId = usrId UserId = usrId
enteredOn = now EnteredOn = now
showAfter = Instant.MinValue ShowAfter = Instant.MinValue
recurrence = parseRecurrence form Recurrence = parseRecurrence form
history = [ History = [
{ asOf = now { AsOf = now
status = Created Status = Created
text = Some form.requestText Text = Some form.requestText
} }
] ]
} }
@ -470,18 +470,18 @@ module Request =
| Some req -> | Some req ->
// update recurrence if changed // update recurrence if changed
let recur = parseRecurrence form let recur = parseRecurrence form
match recur = req.recurrence with match recur = req.Recurrence with
| true -> () | true -> ()
| false -> | false ->
do! Data.updateRecurrence req.requestId usrId recur db do! Data.updateRecurrence req.RequestId usrId recur db
match recur with match recur with
| Immediate -> do! Data.updateShowAfter req.requestId usrId Instant.MinValue db | Immediate -> do! Data.updateShowAfter req.RequestId usrId Instant.MinValue db
| _ -> () | _ -> ()
// append history // append history
let upd8Text = form.requestText.Trim () let upd8Text = form.requestText.Trim ()
let text = match upd8Text = req.text with true -> None | false -> Some upd8Text let text = match upd8Text = req.Text with true -> None | false -> Some upd8Text
do! Data.addHistory req.requestId usrId do! Data.addHistory req.RequestId usrId
{ asOf = now ctx; status = (Option.get >> RequestAction.ofString) form.status; text = text } db { AsOf = now ctx; Status = (Option.get >> RequestAction.ofString) form.status; Text = text } db
do! db.saveChanges () do! db.saveChanges ()
let nextUrl = let nextUrl =
match form.returnTo with match form.returnTo with

View File

@ -8,7 +8,7 @@ open MyPrayerJournal
/// Display a card for this prayer request /// Display a card for this prayer request
let journalCard now req = let journalCard now req =
let reqId = RequestId.toString req.requestId let reqId = RequestId.toString req.RequestId
let spacer = span [] [ rawText "&nbsp;" ] let spacer = span [] [ rawText "&nbsp;" ]
div [ _class "col" ] [ div [ _class "col" ] [
div [ _class "card h-100" ] [ div [ _class "card h-100" ] [
@ -45,10 +45,10 @@ let journalCard now req =
] ]
] ]
div [ _class "card-body" ] [ div [ _class "card-body" ] [
p [ _class "request-text" ] [ str req.text ] p [ _class "request-text" ] [ str req.Text ]
] ]
div [ _class "card-footer text-end text-muted px-1 py-0" ] [ div [ _class "card-footer text-end text-muted px-1 py-0" ] [
em [] [ str "last activity "; relativeDate req.asOf now ] em [] [ str "last activity "; relativeDate req.AsOf now ]
] ]
] ]
] ]

View File

@ -8,10 +8,10 @@ open NodaTime
/// Create a request within the list /// Create a request within the list
let reqListItem now req = let reqListItem now req =
let reqId = RequestId.toString req.requestId let reqId = RequestId.toString req.RequestId
let isAnswered = req.lastStatus = Answered let isAnswered = req.LastStatus = Answered
let isSnoozed = req.snoozedUntil > now let isSnoozed = req.SnoozedUntil > now
let isPending = (not isSnoozed) && req.showAfter > now let isPending = (not isSnoozed) && req.ShowAfter > now
let btnClass = _class "btn btn-light mx-2" let btnClass = _class "btn btn-light mx-2"
let restoreBtn (link : string) title = let restoreBtn (link : string) title =
button [ btnClass; _hxPatch $"/request/{reqId}/{link}"; _title title ] [ icon "restore" ] button [ btnClass; _hxPatch $"/request/{reqId}/{link}"; _title title ] [ icon "restore" ]
@ -23,13 +23,13 @@ let reqListItem now req =
if isSnoozed then restoreBtn "cancel-snooze" "Cancel Snooze" if isSnoozed then restoreBtn "cancel-snooze" "Cancel Snooze"
elif isPending then restoreBtn "show" "Show Now" elif isPending then restoreBtn "show" "Show Now"
p [ _class "request-text mb-0" ] [ p [ _class "request-text mb-0" ] [
str req.text str req.Text
if isSnoozed || isPending || isAnswered then if isSnoozed || isPending || isAnswered then
br [] br []
small [ _class "text-muted" ] [ small [ _class "text-muted" ] [
if isSnoozed then [ str "Snooze expires "; relativeDate req.snoozedUntil now ] if isSnoozed then [ str "Snooze expires "; relativeDate req.SnoozedUntil now ]
elif isPending then [ str "Request appears next "; relativeDate req.showAfter now ] elif isPending then [ str "Request appears next "; relativeDate req.ShowAfter now ]
else (* isAnswered *) [ str "Answered "; relativeDate req.asOf now ] else (* isAnswered *) [ str "Answered "; relativeDate req.AsOf now ]
|> em [] |> em []
] ]
] ]
@ -74,27 +74,27 @@ let snoozed now reqs =
let full (clock : IClock) (req : Request) = let full (clock : IClock) (req : Request) =
let now = clock.GetCurrentInstant () let now = clock.GetCurrentInstant ()
let answered = let answered =
req.history req.History
|> List.filter RequestAction.isAnswered |> List.filter History.isAnswered
|> List.tryHead |> List.tryHead
|> Option.map (fun x -> x.asOf) |> Option.map (fun x -> x.AsOf)
let prayed = (req.history |> List.filter RequestAction.isPrayed |> List.length).ToString "N0" let prayed = (req.History |> List.filter History.isPrayed |> List.length).ToString "N0"
let daysOpen = let daysOpen =
let asOf = defaultArg answered now let asOf = defaultArg answered now
((asOf - (req.history |> List.filter RequestAction.isCreated |> List.head).asOf).TotalDays |> int).ToString "N0" ((asOf - (req.History |> List.filter History.isCreated |> List.head).AsOf).TotalDays |> int).ToString "N0"
let lastText = let lastText =
req.history req.History
|> List.filter (fun h -> Option.isSome h.text) |> List.filter (fun h -> Option.isSome h.Text)
|> List.sortByDescending (fun h -> h.asOf) |> List.sortByDescending (fun h -> h.AsOf)
|> List.map (fun h -> Option.get h.text) |> List.map (fun h -> Option.get h.Text)
|> List.head |> List.head
// The history log including notes (and excluding the final entry for answered requests) // The history log including notes (and excluding the final entry for answered requests)
let log = let log =
let toDisp (h : History) = {| asOf = h.asOf; text = h.text; status = RequestAction.toString h.status |} let toDisp (h : History) = {| asOf = h.AsOf; text = h.Text; status = RequestAction.toString h.Status |}
let all = let all =
req.notes req.Notes
|> List.map (fun n -> {| asOf = n.asOf; text = Some n.notes; status = "Notes" |}) |> List.map (fun n -> {| asOf = n.AsOf; text = Some n.Notes; status = "Notes" |})
|> List.append (req.history |> List.map toDisp) |> List.append (req.History |> List.map toDisp)
|> List.sortByDescending (fun it -> it.asOf) |> List.sortByDescending (fun it -> it.asOf)
// Skip the first entry for answered requests; that info is already displayed // Skip the first entry for answered requests; that info is already displayed
match answered with Some _ -> all |> List.skip 1 | None -> all match answered with Some _ -> all |> List.skip 1 | None -> all
@ -139,7 +139,7 @@ let edit (req : JournalRequest) returnTo isNew =
| "snoozed" -> "/requests/snoozed" | "snoozed" -> "/requests/snoozed"
| _ (* "journal" *) -> "/journal" | _ (* "journal" *) -> "/journal"
let recurCount = let recurCount =
match req.recurrence with match req.Recurrence with
| Immediate -> None | Immediate -> None
| Hours h -> Some h | Hours h -> Some h
| Days d -> Some d | Days d -> Some d
@ -154,7 +154,7 @@ let edit (req : JournalRequest) returnTo isNew =
"/request" |> match isNew with true -> _hxPost | false -> _hxPatch ] [ "/request" |> match isNew with true -> _hxPost | false -> _hxPatch ] [
input [ _type "hidden" input [ _type "hidden"
_name "requestId" _name "requestId"
_value (match isNew with true -> "new" | false -> RequestId.toString req.requestId) ] _value (match isNew with true -> "new" | false -> RequestId.toString req.RequestId) ]
input [ _type "hidden"; _name "returnTo"; _value returnTo ] input [ _type "hidden"; _name "returnTo"; _value returnTo ]
div [ _class "form-floating pb-3" ] [ div [ _class "form-floating pb-3" ] [
textarea [ _id "requestText" textarea [ _id "requestText"
@ -162,7 +162,7 @@ let edit (req : JournalRequest) returnTo isNew =
_class "form-control" _class "form-control"
_style "min-height: 8rem;" _style "min-height: 8rem;"
_placeholder "Enter the text of the request" _placeholder "Enter the text of the request"
_autofocus; _required ] [ str req.text ] _autofocus; _required ] [ str req.Text ]
label [ _for "requestText" ] [ str "Prayer Request" ] label [ _for "requestText" ] [ str "Prayer Request" ]
] ]
br [] br []
@ -202,7 +202,7 @@ let edit (req : JournalRequest) returnTo isNew =
_name "recurType" _name "recurType"
_value "Immediate" _value "Immediate"
_onclick "mpj.edit.toggleRecurrence(event)" _onclick "mpj.edit.toggleRecurrence(event)"
match req.recurrence with Immediate -> _checked | _ -> () ] match req.Recurrence with Immediate -> _checked | _ -> () ]
label [ _for "rI" ] [ str "Immediately" ] label [ _for "rI" ] [ str "Immediately" ]
] ]
div [ _class "form-check mx-2"] [ div [ _class "form-check mx-2"] [
@ -212,7 +212,7 @@ let edit (req : JournalRequest) returnTo isNew =
_name "recurType" _name "recurType"
_value "Other" _value "Other"
_onclick "mpj.edit.toggleRecurrence(event)" _onclick "mpj.edit.toggleRecurrence(event)"
match req.recurrence with Immediate -> () | _ -> _checked ] match req.Recurrence with Immediate -> () | _ -> _checked ]
label [ _for "rO" ] [ rawText "Every&hellip;" ] label [ _for "rO" ] [ rawText "Every&hellip;" ]
] ]
div [ _class "form-floating mx-2"] [ div [ _class "form-floating mx-2"] [
@ -224,7 +224,7 @@ let edit (req : JournalRequest) returnTo isNew =
_value recurCount _value recurCount
_style "width:6rem;" _style "width:6rem;"
_required _required
match req.recurrence with Immediate -> _disabled | _ -> () ] match req.Recurrence with Immediate -> _disabled | _ -> () ]
label [ _for "recurCount" ] [ str "Count" ] label [ _for "recurCount" ] [ str "Count" ]
] ]
div [ _class "form-floating mx-2" ] [ div [ _class "form-floating mx-2" ] [
@ -233,14 +233,14 @@ let edit (req : JournalRequest) returnTo isNew =
_name "recurInterval" _name "recurInterval"
_style "width:6rem;" _style "width:6rem;"
_required _required
match req.recurrence with Immediate -> _disabled | _ -> () ] [ match req.Recurrence with Immediate -> _disabled | _ -> () ] [
option [ _value "Hours"; match req.recurrence with Hours _ -> _selected | _ -> () ] [ option [ _value "Hours"; match req.Recurrence with Hours _ -> _selected | _ -> () ] [
str "hours" str "hours"
] ]
option [ _value "Days"; match req.recurrence with Days _ -> _selected | _ -> () ] [ option [ _value "Days"; match req.Recurrence with Days _ -> _selected | _ -> () ] [
str "days" str "days"
] ]
option [ _value "Weeks"; match req.recurrence with Weeks _ -> _selected | _ -> () ] [ option [ _value "Weeks"; match req.Recurrence with Weeks _ -> _selected | _ -> () ] [
str "weeks" str "weeks"
] ]
] ]
@ -259,7 +259,7 @@ let edit (req : JournalRequest) returnTo isNew =
/// Display a list of notes for a request /// Display a list of notes for a request
let notes now notes = let notes now notes =
let toItem (note : Note) = let toItem (note : Note) =
p [] [ small [ _class "text-muted" ] [ relativeDate note.asOf now ]; br []; str note.notes ] p [] [ small [ _class "text-muted" ] [ relativeDate note.AsOf now ]; br []; str note.Notes ]
[ p [ _class "text-center" ] [ strong [] [ str "Prior Notes for This Request" ] ] [ p [ _class "text-center" ] [ strong [] [ str "Prior Notes for This Request" ] ]
match notes with match notes with
| [] -> p [ _class "text-center text-muted" ] [ str "There are no prior notes for this request" ] | [] -> p [ _class "text-center text-muted" ] [ str "There are no prior notes for this request" ]