Version 3.1 #71
@ -1,8 +1,8 @@
 | 
				
			|||||||
module MyPrayerJournal.Data
 | 
					module MyPrayerJournal.Data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
open LiteDB
 | 
					open LiteDB
 | 
				
			||||||
 | 
					open MyPrayerJournal
 | 
				
			||||||
open NodaTime
 | 
					open NodaTime
 | 
				
			||||||
open System
 | 
					 | 
				
			||||||
open System.Threading.Tasks
 | 
					open System.Threading.Tasks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// fsharplint:disable MemberNames
 | 
					// fsharplint:disable MemberNames
 | 
				
			||||||
@ -36,7 +36,12 @@ module Mapping =
 | 
				
			|||||||
  module Option =
 | 
					  module Option =
 | 
				
			||||||
    let stringFromBson (value : BsonValue) = match value.AsString with "" -> None | x -> Some x
 | 
					    let stringFromBson (value : BsonValue) = match value.AsString with "" -> None | x -> Some x
 | 
				
			||||||
    let stringToBson (value : string option) : BsonValue = match value with Some txt -> txt | None -> ""
 | 
					    let stringToBson (value : string option) : BsonValue = match value with Some txt -> txt | None -> ""
 | 
				
			||||||
    
 | 
					  
 | 
				
			||||||
 | 
					  /// Mapping for Recurrence
 | 
				
			||||||
 | 
					  module Recurrence =
 | 
				
			||||||
 | 
					    let fromBson (value : BsonValue) = Recurrence.ofString value
 | 
				
			||||||
 | 
					    let toBson (value : Recurrence) : BsonValue = Recurrence.toString value
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  /// Mapping for RequestAction
 | 
					  /// Mapping for RequestAction
 | 
				
			||||||
  module RequestAction =
 | 
					  module RequestAction =
 | 
				
			||||||
    let fromBson (value : BsonValue) = RequestAction.ofString value.AsString
 | 
					    let fromBson (value : BsonValue) = RequestAction.ofString value.AsString
 | 
				
			||||||
@ -52,65 +57,10 @@ module Mapping =
 | 
				
			|||||||
    let fromBson (value : BsonValue) = UserId value.AsString
 | 
					    let fromBson (value : BsonValue) = UserId value.AsString
 | 
				
			||||||
    let toBson (value : UserId) : BsonValue = UserId.toString value
 | 
					    let toBson (value : UserId) : BsonValue = UserId.toString value
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
  /// Map a history entry to BSON
 | 
					 | 
				
			||||||
  let historyToBson (hist : History) : BsonValue =
 | 
					 | 
				
			||||||
    let doc = BsonDocument ()
 | 
					 | 
				
			||||||
    doc["asOf"]   <- hist.asOf.ToUnixTimeMilliseconds ()
 | 
					 | 
				
			||||||
    doc["status"] <- RequestAction.toString hist.status
 | 
					 | 
				
			||||||
    doc["text"]   <- match hist.text with Some t -> t | None -> ""
 | 
					 | 
				
			||||||
    upcast doc
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Map a BSON document to a history entry
 | 
					 | 
				
			||||||
  let historyFromBson (doc : BsonValue) =
 | 
					 | 
				
			||||||
    { asOf   = Instant.FromUnixTimeMilliseconds doc["asOf"].AsInt64
 | 
					 | 
				
			||||||
      status = RequestAction.ofString doc["status"].AsString
 | 
					 | 
				
			||||||
      text   = match doc["text"].AsString with "" -> None | txt -> Some txt
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Map a note entry to BSON
 | 
					 | 
				
			||||||
  let noteToBson (note : Note) : BsonValue =
 | 
					 | 
				
			||||||
    let doc = BsonDocument ()
 | 
					 | 
				
			||||||
    doc["asOf"]  <- note.asOf.ToUnixTimeMilliseconds ()
 | 
					 | 
				
			||||||
    doc["notes"] <- note.notes
 | 
					 | 
				
			||||||
    upcast doc
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Map a BSON document to a note entry
 | 
					 | 
				
			||||||
  let noteFromBson (doc : BsonValue) =
 | 
					 | 
				
			||||||
    { asOf  = Instant.FromUnixTimeMilliseconds doc["asOf"].AsInt64
 | 
					 | 
				
			||||||
      notes = doc["notes"].AsString
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Map a request to its BSON representation
 | 
					 | 
				
			||||||
  let requestToBson req : BsonValue =
 | 
					 | 
				
			||||||
    let doc = BsonDocument ()
 | 
					 | 
				
			||||||
    doc["_id"]          <- RequestId.toString req.id
 | 
					 | 
				
			||||||
    doc["enteredOn"]    <- req.enteredOn.ToUnixTimeMilliseconds ()
 | 
					 | 
				
			||||||
    doc["userId"]       <- UserId.toString req.userId
 | 
					 | 
				
			||||||
    doc["snoozedUntil"] <- req.snoozedUntil.ToUnixTimeMilliseconds ()
 | 
					 | 
				
			||||||
    doc["showAfter"]    <- req.showAfter.ToUnixTimeMilliseconds ()
 | 
					 | 
				
			||||||
    doc["recurType"]    <- Recurrence.toString req.recurType
 | 
					 | 
				
			||||||
    doc["recurCount"]   <- BsonValue req.recurCount
 | 
					 | 
				
			||||||
    doc["history"]      <- BsonArray (req.history |> List.map historyToBson |> Seq.ofList)
 | 
					 | 
				
			||||||
    doc["notes"]        <- BsonArray (req.notes   |> List.map noteToBson    |> Seq.ofList)
 | 
					 | 
				
			||||||
    upcast doc
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  /// Map a BSON document to a request
 | 
					 | 
				
			||||||
  let requestFromBson (doc : BsonValue) =
 | 
					 | 
				
			||||||
    { id           = RequestId.ofString doc["_id"].AsString
 | 
					 | 
				
			||||||
      enteredOn    = Instant.FromUnixTimeMilliseconds doc["enteredOn"].AsInt64
 | 
					 | 
				
			||||||
      userId       = UserId doc["userId"].AsString
 | 
					 | 
				
			||||||
      snoozedUntil = Instant.FromUnixTimeMilliseconds doc["snoozedUntil"].AsInt64
 | 
					 | 
				
			||||||
      showAfter    = Instant.FromUnixTimeMilliseconds doc["showAfter"].AsInt64
 | 
					 | 
				
			||||||
      recurType    = Recurrence.ofString doc["recurType"].AsString
 | 
					 | 
				
			||||||
      recurCount   = int16 doc["recurCount"].AsInt32
 | 
					 | 
				
			||||||
      history      = doc["history"].AsArray |> Seq.map historyFromBson |> List.ofSeq
 | 
					 | 
				
			||||||
      notes        = doc["notes"].AsArray   |> Seq.map noteFromBson    |> List.ofSeq
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  /// Set up the mapping
 | 
					  /// Set up the mapping
 | 
				
			||||||
  let register () = 
 | 
					  let register () = 
 | 
				
			||||||
    BsonMapper.Global.RegisterType<Request>(requestToBson, requestFromBson)
 | 
					 | 
				
			||||||
    BsonMapper.Global.RegisterType<Instant>(Instant.toBson, Instant.fromBson)
 | 
					    BsonMapper.Global.RegisterType<Instant>(Instant.toBson, Instant.fromBson)
 | 
				
			||||||
 | 
					    BsonMapper.Global.RegisterType<Recurrence>(Recurrence.toBson, Recurrence.fromBson)
 | 
				
			||||||
    BsonMapper.Global.RegisterType<RequestAction>(RequestAction.toBson, RequestAction.fromBson)
 | 
					    BsonMapper.Global.RegisterType<RequestAction>(RequestAction.toBson, RequestAction.fromBson)
 | 
				
			||||||
    BsonMapper.Global.RegisterType<RequestId>(RequestId.toBson, RequestId.fromBson)
 | 
					    BsonMapper.Global.RegisterType<RequestId>(RequestId.toBson, RequestId.fromBson)
 | 
				
			||||||
    BsonMapper.Global.RegisterType<string option>(Option.stringToBson, Option.stringFromBson)
 | 
					    BsonMapper.Global.RegisterType<string option>(Option.stringToBson, Option.stringFromBson)
 | 
				
			||||||
@ -217,9 +167,9 @@ let tryJournalById reqId userId (db : LiteDatabase) = backgroundTask {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
/// Update the recurrence for a request
 | 
					/// Update the recurrence for a request
 | 
				
			||||||
let updateRecurrence reqId userId recurType recurCount 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 recurType = recurType; recurCount = recurCount }
 | 
					  | Some req -> do! doUpdate db { req with recurrence = recurType }
 | 
				
			||||||
  | None     -> invalidOp $"{RequestId.toString reqId} not found"
 | 
					  | None     -> invalidOp $"{RequestId.toString reqId} not found"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
[<AutoOpen>]
 | 
					/// The data model for myPrayerJournal
 | 
				
			||||||
/// The data model for myPrayerJournal
 | 
					[<AutoOpen>]
 | 
				
			||||||
module MyPrayerJournal.Domain
 | 
					module MyPrayerJournal.Domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// fsharplint:disable RecordFieldNames
 | 
					// fsharplint:disable RecordFieldNames
 | 
				
			||||||
@ -34,7 +34,7 @@ module UserId =
 | 
				
			|||||||
type Recurrence =
 | 
					type Recurrence =
 | 
				
			||||||
  | Immediate
 | 
					  | Immediate
 | 
				
			||||||
  | Hours of int16
 | 
					  | Hours of int16
 | 
				
			||||||
  | Days of int16
 | 
					  | Days  of int16
 | 
				
			||||||
  | Weeks of int16
 | 
					  | Weeks of int16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Functions to manipulate recurrences
 | 
					/// Functions to manipulate recurrences
 | 
				
			||||||
@ -111,10 +111,8 @@ type Request = {
 | 
				
			|||||||
  snoozedUntil : Instant
 | 
					  snoozedUntil : Instant
 | 
				
			||||||
  /// The time at which this request should reappear in the user's journal by recurrence
 | 
					  /// The time at which this request should reappear in the user's journal by recurrence
 | 
				
			||||||
  showAfter    : Instant
 | 
					  showAfter    : Instant
 | 
				
			||||||
  /// The type of recurrence for this request
 | 
					  /// The recurrence for this request
 | 
				
			||||||
  recurType    : Recurrence
 | 
					  recurrence   : Recurrence
 | 
				
			||||||
  /// How many of the recurrence intervals should occur between appearances in the journal
 | 
					 | 
				
			||||||
  recurCount   : int16
 | 
					 | 
				
			||||||
  /// The history entries for this request
 | 
					  /// The history entries for this request
 | 
				
			||||||
  history      : History list
 | 
					  history      : History list
 | 
				
			||||||
  /// The notes for this request
 | 
					  /// The notes for this request
 | 
				
			||||||
@ -128,8 +126,7 @@ with
 | 
				
			|||||||
      userId       = UserId ""
 | 
					      userId       = UserId ""
 | 
				
			||||||
      snoozedUntil = Instant.MinValue
 | 
					      snoozedUntil = Instant.MinValue
 | 
				
			||||||
      showAfter    = Instant.MinValue
 | 
					      showAfter    = Instant.MinValue
 | 
				
			||||||
      recurType    = Immediate
 | 
					      recurrence   = Immediate
 | 
				
			||||||
      recurCount   = 0s
 | 
					 | 
				
			||||||
      history      = []
 | 
					      history      = []
 | 
				
			||||||
      notes        = []
 | 
					      notes        = []
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -152,10 +149,8 @@ type JournalRequest = {
 | 
				
			|||||||
  snoozedUntil : Instant
 | 
					  snoozedUntil : Instant
 | 
				
			||||||
  /// The time after which this request should reappear in the user's journal by configured recurrence
 | 
					  /// The time after which this request should reappear in the user's journal by configured recurrence
 | 
				
			||||||
  showAfter    : Instant
 | 
					  showAfter    : Instant
 | 
				
			||||||
  /// The type of recurrence for this request
 | 
					  /// The recurrence for this request
 | 
				
			||||||
  recurType    : Recurrence
 | 
					  recurrence   : Recurrence
 | 
				
			||||||
  /// How many of the recurrence intervals should occur between appearances in the journal
 | 
					 | 
				
			||||||
  recurCount   : int16
 | 
					 | 
				
			||||||
  /// History entries for the request
 | 
					  /// History entries for the request
 | 
				
			||||||
  history      : History list
 | 
					  history      : History list
 | 
				
			||||||
  /// Note entries for the request
 | 
					  /// Note entries for the request
 | 
				
			||||||
@ -180,8 +175,7 @@ module JournalRequest =
 | 
				
			|||||||
      lastStatus   = match hist with Some h -> h.status | None -> Created
 | 
					      lastStatus   = match hist with Some h -> h.status | None -> Created
 | 
				
			||||||
      snoozedUntil = req.snoozedUntil
 | 
					      snoozedUntil = req.snoozedUntil
 | 
				
			||||||
      showAfter    = req.showAfter
 | 
					      showAfter    = req.showAfter
 | 
				
			||||||
      recurType    = req.recurType
 | 
					      recurrence   = req.recurrence
 | 
				
			||||||
      recurCount   = req.recurCount
 | 
					 | 
				
			||||||
      history      = []
 | 
					      history      = []
 | 
				
			||||||
      notes        = []
 | 
					      notes        = []
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
				
			|||||||
@ -37,10 +37,10 @@ module Error =
 | 
				
			|||||||
    log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
 | 
					    log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
 | 
				
			||||||
    clearResponse
 | 
					    clearResponse
 | 
				
			||||||
    >=> setStatusCode 500
 | 
					    >=> setStatusCode 500
 | 
				
			||||||
    >=> setHttpHeader "X-Toast" (sprintf "error|||%s: %s" (ex.GetType().Name) ex.Message)
 | 
					    >=> setHttpHeader "X-Toast" $"error|||{ex.GetType().Name}: {ex.Message}"
 | 
				
			||||||
    >=> text ex.Message
 | 
					    >=> text ex.Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized reponse
 | 
					  /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response
 | 
				
			||||||
  let notAuthorized : HttpHandler =
 | 
					  let notAuthorized : HttpHandler =
 | 
				
			||||||
    fun next ctx ->
 | 
					    fun next ctx ->
 | 
				
			||||||
      (next, ctx)
 | 
					      (next, ctx)
 | 
				
			||||||
@ -97,7 +97,7 @@ module private Helpers =
 | 
				
			|||||||
  /// Return a 201 CREATED response with the location header set for the created resource
 | 
					  /// Return a 201 CREATED response with the location header set for the created resource
 | 
				
			||||||
  let createdAt url : HttpHandler =
 | 
					  let createdAt url : HttpHandler =
 | 
				
			||||||
    fun next ctx ->
 | 
					    fun next ctx ->
 | 
				
			||||||
      (sprintf "%s://%s%s" ctx.Request.Scheme ctx.Request.Host.Value url |> setHttpHeader HeaderNames.Location
 | 
					      ($"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{url}" |> setHttpHeader HeaderNames.Location
 | 
				
			||||||
       >=> created) next ctx
 | 
					       >=> created) next ctx
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  /// Return a 303 SEE OTHER response (forces a GET on the redirected URL)
 | 
					  /// Return a 303 SEE OTHER response (forces a GET on the redirected URL)
 | 
				
			||||||
@ -107,7 +107,7 @@ module private Helpers =
 | 
				
			|||||||
  /// Render a component result
 | 
					  /// Render a component result
 | 
				
			||||||
  let renderComponent nodes : HttpHandler =
 | 
					  let renderComponent nodes : HttpHandler =
 | 
				
			||||||
    noResponseCaching
 | 
					    noResponseCaching
 | 
				
			||||||
    >=> fun next ctx -> backgroundTask {
 | 
					    >=> fun _ ctx -> backgroundTask {
 | 
				
			||||||
      return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
 | 
					      return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,7 +131,7 @@ module private Helpers =
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /// Composable handler to write a view to the output
 | 
					  /// Composable handler to write a view to the output
 | 
				
			||||||
  let writeView view : HttpHandler =
 | 
					  let writeView view : HttpHandler =
 | 
				
			||||||
    fun next ctx -> backgroundTask {
 | 
					    fun _ ctx -> backgroundTask {
 | 
				
			||||||
      return! ctx.WriteHtmlViewAsync view
 | 
					      return! ctx.WriteHtmlViewAsync view
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -139,7 +139,7 @@ module private Helpers =
 | 
				
			|||||||
  module Messages =
 | 
					  module Messages =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// The messages being held
 | 
					    /// The messages being held
 | 
				
			||||||
    let mutable private messages : Map<string, (string * string)> = Map.empty
 | 
					    let mutable private messages : Map<string, string * string> = Map.empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Locked update to prevent updates by multiple threads
 | 
					    /// Locked update to prevent updates by multiple threads
 | 
				
			||||||
    let private upd8 = obj ()
 | 
					    let private upd8 = obj ()
 | 
				
			||||||
@ -150,7 +150,7 @@ module private Helpers =
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /// Add a success message header to the response
 | 
					    /// Add a success message header to the response
 | 
				
			||||||
    let pushSuccess ctx message url =
 | 
					    let pushSuccess ctx message url =
 | 
				
			||||||
      push ctx (sprintf "success|||%s" message) url
 | 
					      push ctx $"success|||{message}" url
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Pop the messages for the given user
 | 
					    /// Pop the messages for the given user
 | 
				
			||||||
    let pop userId = lock upd8 (fun () ->
 | 
					    let pop userId = lock upd8 (fun () ->
 | 
				
			||||||
@ -289,7 +289,7 @@ module Journal =
 | 
				
			|||||||
        |> Option.map (fun c -> c.Value)
 | 
					        |> Option.map (fun c -> c.Value)
 | 
				
			||||||
        |> Option.defaultValue "Your"
 | 
					        |> Option.defaultValue "Your"
 | 
				
			||||||
      let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's"
 | 
					      let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's"
 | 
				
			||||||
      return! partial (sprintf "%s Prayer Journal" title) (Views.Journal.journal usr) next ctx
 | 
					      return! partial $"{title} Prayer Journal" (Views.Journal.journal usr) next ctx
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -343,9 +343,9 @@ module Request =
 | 
				
			|||||||
          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.recurType with
 | 
					            match Recurrence.duration req.recurrence with
 | 
				
			||||||
            | 0L       -> Instant.MinValue
 | 
					            | 0L       -> Instant.MinValue
 | 
				
			||||||
            | duration -> now.Plus (Duration.FromSeconds (duration * int64 req.recurCount))
 | 
					            | duration -> now.Plus (Duration.FromSeconds duration)
 | 
				
			||||||
          do! Data.updateShowAfter reqId usrId nextShow db
 | 
					          do! Data.updateShowAfter reqId usrId nextShow db
 | 
				
			||||||
          do! db.saveChanges ()
 | 
					          do! db.saveChanges ()
 | 
				
			||||||
          return! (withSuccessMessage "Request marked as prayed" >=> Components.journalItems) next ctx
 | 
					          return! (withSuccessMessage "Request marked as prayed" >=> Components.journalItems) next ctx
 | 
				
			||||||
@ -465,27 +465,25 @@ module Request =
 | 
				
			|||||||
      | None -> return! Error.notFound next ctx
 | 
					      | None -> return! Error.notFound next ctx
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Derive a recurrence and interval from its primitive representation in the form
 | 
					  /// Derive a recurrence from its representation in the form
 | 
				
			||||||
  let private parseRecurrence (form : Models.Request) =
 | 
					  let private parseRecurrence (form : Models.Request) =
 | 
				
			||||||
    (Recurrence.ofString (match form.recurInterval with Some x -> x | _ -> "Immediate"),
 | 
					    match form.recurInterval with Some x -> $"{defaultArg form.recurCount 0s} {x}" | None -> "Immediate"
 | 
				
			||||||
     defaultArg form.recurCount (int16 0))
 | 
					    |> Recurrence.ofString
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // POST /request
 | 
					  // POST /request
 | 
				
			||||||
  let add : HttpHandler =
 | 
					  let add : HttpHandler =
 | 
				
			||||||
    requiresAuthentication Error.notAuthorized
 | 
					    requiresAuthentication Error.notAuthorized
 | 
				
			||||||
    >=> fun next ctx -> backgroundTask {
 | 
					    >=> fun next ctx -> backgroundTask {
 | 
				
			||||||
      let! form             = ctx.BindModelAsync<Models.Request> ()
 | 
					      let! form  = ctx.BindModelAsync<Models.Request> ()
 | 
				
			||||||
      let  db               = db ctx
 | 
					      let  db    = db ctx
 | 
				
			||||||
      let  usrId            = userId ctx
 | 
					      let  usrId = userId ctx
 | 
				
			||||||
      let  now              = now ctx
 | 
					      let  now   = now ctx
 | 
				
			||||||
      let (recur, interval) = parseRecurrence form
 | 
					 | 
				
			||||||
      let  req   =
 | 
					      let  req   =
 | 
				
			||||||
        { Request.empty with
 | 
					        { Request.empty with
 | 
				
			||||||
            userId     = usrId
 | 
					            userId     = usrId
 | 
				
			||||||
            enteredOn  = now
 | 
					            enteredOn  = now
 | 
				
			||||||
            showAfter  = Instant.MinValue
 | 
					            showAfter  = Instant.MinValue
 | 
				
			||||||
            recurType  = recur
 | 
					            recurrence = parseRecurrence form
 | 
				
			||||||
            recurCount = interval
 | 
					 | 
				
			||||||
            history    = [
 | 
					            history    = [
 | 
				
			||||||
              { asOf   = now
 | 
					              { asOf   = now
 | 
				
			||||||
                status = Created
 | 
					                status = Created
 | 
				
			||||||
@ -509,11 +507,11 @@ module Request =
 | 
				
			|||||||
      match! Data.tryJournalById (RequestId.ofString form.requestId) usrId db with
 | 
					      match! Data.tryJournalById (RequestId.ofString form.requestId) usrId db with
 | 
				
			||||||
      | Some req ->
 | 
					      | Some req ->
 | 
				
			||||||
          // update recurrence if changed
 | 
					          // update recurrence if changed
 | 
				
			||||||
          let (recur, interval) = parseRecurrence form
 | 
					          let recur = parseRecurrence form
 | 
				
			||||||
          match recur = req.recurType && interval = req.recurCount with
 | 
					          match recur = req.recurrence with
 | 
				
			||||||
          | true  -> ()
 | 
					          | true  -> ()
 | 
				
			||||||
          | false ->
 | 
					          | false ->
 | 
				
			||||||
              do! Data.updateRecurrence req.requestId usrId recur interval 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
 | 
				
			||||||
              | _         -> ()
 | 
					              | _         -> ()
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
  <PropertyGroup>
 | 
					  <PropertyGroup>
 | 
				
			||||||
    <TargetFramework>net6.0</TargetFramework>
 | 
					    <TargetFramework>net6.0</TargetFramework>
 | 
				
			||||||
    <Version>3.0.0.0</Version>
 | 
					    <Version>3.0.0.0</Version>
 | 
				
			||||||
 | 
					    <NoWarn>3391</NoWarn>
 | 
				
			||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <Compile Include="Domain.fs" />
 | 
					    <Compile Include="Domain.fs" />
 | 
				
			||||||
@ -19,8 +20,8 @@
 | 
				
			|||||||
    <PackageReference Include="FSharp.SystemTextJson" Version="0.17.4" />
 | 
					    <PackageReference Include="FSharp.SystemTextJson" Version="0.17.4" />
 | 
				
			||||||
    <PackageReference Include="FunctionalCuid" Version="1.0.0" />
 | 
					    <PackageReference Include="FunctionalCuid" Version="1.0.0" />
 | 
				
			||||||
    <PackageReference Include="Giraffe" Version="5.0.0" />
 | 
					    <PackageReference Include="Giraffe" Version="5.0.0" />
 | 
				
			||||||
    <PackageReference Include="Giraffe.Htmx" Version="0.9.2" />
 | 
					    <PackageReference Include="Giraffe.Htmx" Version="1.6.1" />
 | 
				
			||||||
    <PackageReference Include="Giraffe.ViewEngine.Htmx" Version="0.9.2" />
 | 
					    <PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.6.1" />
 | 
				
			||||||
    <PackageReference Include="LiteDB" Version="5.0.11" />
 | 
					    <PackageReference Include="LiteDB" Version="5.0.11" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
 | 
					    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
 | 
				
			||||||
    <PackageReference Include="NodaTime" Version="3.0.9" />
 | 
					    <PackageReference Include="NodaTime" Version="3.0.9" />
 | 
				
			||||||
 | 
				
			|||||||
@ -84,17 +84,17 @@ module Configure =
 | 
				
			|||||||
          opts.OnAppendCookie        <- fun ctx -> sameSite ctx.CookieOptions
 | 
					          opts.OnAppendCookie        <- fun ctx -> sameSite ctx.CookieOptions
 | 
				
			||||||
          opts.OnDeleteCookie        <- fun ctx -> sameSite ctx.CookieOptions)
 | 
					          opts.OnDeleteCookie        <- fun ctx -> sameSite ctx.CookieOptions)
 | 
				
			||||||
      .AddAuthentication(
 | 
					      .AddAuthentication(
 | 
				
			||||||
        /// Use HTTP "Bearer" authentication with JWTs
 | 
					        // Use HTTP "Bearer" authentication with JWTs
 | 
				
			||||||
        fun opts ->
 | 
					        fun opts ->
 | 
				
			||||||
          opts.DefaultAuthenticateScheme <- CookieAuthenticationDefaults.AuthenticationScheme
 | 
					          opts.DefaultAuthenticateScheme <- CookieAuthenticationDefaults.AuthenticationScheme
 | 
				
			||||||
          opts.DefaultSignInScheme       <- CookieAuthenticationDefaults.AuthenticationScheme
 | 
					          opts.DefaultSignInScheme       <- CookieAuthenticationDefaults.AuthenticationScheme
 | 
				
			||||||
          opts.DefaultChallengeScheme    <- CookieAuthenticationDefaults.AuthenticationScheme)
 | 
					          opts.DefaultChallengeScheme    <- CookieAuthenticationDefaults.AuthenticationScheme)
 | 
				
			||||||
      .AddCookie()
 | 
					      .AddCookie()
 | 
				
			||||||
      .AddOpenIdConnect("Auth0",
 | 
					      .AddOpenIdConnect("Auth0",
 | 
				
			||||||
        /// Configure OIDC with Auth0 options from configuration
 | 
					        // Configure OIDC with Auth0 options from configuration
 | 
				
			||||||
        fun opts ->
 | 
					        fun opts ->
 | 
				
			||||||
          let cfg = bldr.Configuration.GetSection "Auth0"
 | 
					          let cfg = bldr.Configuration.GetSection "Auth0"
 | 
				
			||||||
          opts.Authority    <- sprintf "https://%s/" cfg["Domain"]
 | 
					          opts.Authority    <- $"""https://{cfg["Domain"]}/"""
 | 
				
			||||||
          opts.ClientId     <- cfg["Id"]
 | 
					          opts.ClientId     <- cfg["Id"]
 | 
				
			||||||
          opts.ClientSecret <- cfg["Secret"]
 | 
					          opts.ClientSecret <- cfg["Secret"]
 | 
				
			||||||
          opts.ResponseType <- OpenIdConnectResponseType.Code
 | 
					          opts.ResponseType <- OpenIdConnectResponseType.Code
 | 
				
			||||||
@ -118,11 +118,10 @@ module Configure =
 | 
				
			|||||||
                    | true ->
 | 
					                    | true ->
 | 
				
			||||||
                        // transform to absolute
 | 
					                        // transform to absolute
 | 
				
			||||||
                        let request = ctx.Request
 | 
					                        let request = ctx.Request
 | 
				
			||||||
                        sprintf "%s://%s%s%s" request.Scheme request.Host.Value request.PathBase.Value redirUri
 | 
					                        $"{request.Scheme}://{request.Host.Value}{request.PathBase.Value}{redirUri}"
 | 
				
			||||||
                    | false -> redirUri
 | 
					                    | false -> redirUri
 | 
				
			||||||
                  Uri.EscapeDataString finalRedirUri |> sprintf "&returnTo=%s"
 | 
					                  Uri.EscapeDataString $"&returnTo={finalRedirUri}"
 | 
				
			||||||
            sprintf "https://%s/v2/logout?client_id=%s%s" cfg["Domain"] cfg["Id"] returnTo
 | 
					            ctx.Response.Redirect $"""https://{cfg["Domain"]}/v2/logout?client_id={cfg["Id"]}{returnTo}"""
 | 
				
			||||||
            |> ctx.Response.Redirect
 | 
					 | 
				
			||||||
            ctx.HandleResponse ()
 | 
					            ctx.HandleResponse ()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Task.CompletedTask
 | 
					            Task.CompletedTask
 | 
				
			||||||
@ -159,7 +158,7 @@ module Configure =
 | 
				
			|||||||
      .UseRouting()
 | 
					      .UseRouting()
 | 
				
			||||||
      .UseAuthentication()
 | 
					      .UseAuthentication()
 | 
				
			||||||
      .UseGiraffeErrorHandler(Handlers.Error.error)
 | 
					      .UseGiraffeErrorHandler(Handlers.Error.error)
 | 
				
			||||||
      .UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes |> ignore)
 | 
					      .UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes)
 | 
				
			||||||
    |> ignore
 | 
					    |> ignore
 | 
				
			||||||
    app
 | 
					    app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -110,11 +110,7 @@ let htmlFoot =
 | 
				
			|||||||
          ]
 | 
					          ]
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    script [
 | 
					    Htmx.Script.minified
 | 
				
			||||||
      _src         "https://unpkg.com/htmx.org@1.5.0"
 | 
					 | 
				
			||||||
      _integrity   "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI"
 | 
					 | 
				
			||||||
      _crossorigin "anonymous"
 | 
					 | 
				
			||||||
      ] []
 | 
					 | 
				
			||||||
    script [] [
 | 
					    script [] [
 | 
				
			||||||
      rawText "if (!htmx) document.write('<script src=\"/script/htmx-1.5.0.min.js\"><\/script>')"
 | 
					      rawText "if (!htmx) document.write('<script src=\"/script/htmx-1.5.0.min.js\"><\/script>')"
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,6 @@ open Giraffe.ViewEngine
 | 
				
			|||||||
open Giraffe.ViewEngine.Htmx
 | 
					open Giraffe.ViewEngine.Htmx
 | 
				
			||||||
open MyPrayerJournal
 | 
					open MyPrayerJournal
 | 
				
			||||||
open NodaTime
 | 
					open NodaTime
 | 
				
			||||||
open System
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a request within the list
 | 
					/// Create a request within the list
 | 
				
			||||||
let reqListItem now req =
 | 
					let reqListItem now req =
 | 
				
			||||||
@ -142,6 +141,14 @@ let edit (req : JournalRequest) returnTo isNew =
 | 
				
			|||||||
    | "active"          -> "/requests/active"
 | 
					    | "active"          -> "/requests/active"
 | 
				
			||||||
    | "snoozed"         -> "/requests/snoozed"
 | 
					    | "snoozed"         -> "/requests/snoozed"
 | 
				
			||||||
    | _ (* "journal" *) -> "/journal"
 | 
					    | _ (* "journal" *) -> "/journal"
 | 
				
			||||||
 | 
					  let recurCount =
 | 
				
			||||||
 | 
					    match req.recurrence with
 | 
				
			||||||
 | 
					    | Immediate -> None
 | 
				
			||||||
 | 
					    | Hours   h -> Some h
 | 
				
			||||||
 | 
					    | Days    d -> Some d
 | 
				
			||||||
 | 
					    | Weeks   w -> Some w
 | 
				
			||||||
 | 
					    |> Option.map string
 | 
				
			||||||
 | 
					    |> Option.defaultValue ""
 | 
				
			||||||
  article [ _class "container" ] [
 | 
					  article [ _class "container" ] [
 | 
				
			||||||
    h2 [ _class "pb-3" ] [ (match isNew with true -> "Add" | false -> "Edit") |> strf "%s Prayer Request" ]
 | 
					    h2 [ _class "pb-3" ] [ (match isNew with true -> "Add" | false -> "Edit") |> strf "%s Prayer Request" ]
 | 
				
			||||||
    form [
 | 
					    form [
 | 
				
			||||||
@ -202,7 +209,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.recurType with Immediate -> _checked | _ -> ()
 | 
					                match req.recurrence with Immediate -> _checked | _ -> ()
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
              label [ _for "rI" ] [ str "Immediately" ]
 | 
					              label [ _for "rI" ] [ str "Immediately" ]
 | 
				
			||||||
              ]
 | 
					              ]
 | 
				
			||||||
@ -214,7 +221,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.recurType with Immediate -> () | _ -> _checked
 | 
					                match req.recurrence with Immediate -> () | _ -> _checked
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
              label [ _for "rO" ] [ rawText "Every…" ]
 | 
					              label [ _for "rO" ] [ rawText "Every…" ]
 | 
				
			||||||
              ]
 | 
					              ]
 | 
				
			||||||
@ -225,10 +232,10 @@ let edit (req : JournalRequest) returnTo isNew =
 | 
				
			|||||||
                _id          "recurCount"
 | 
					                _id          "recurCount"
 | 
				
			||||||
                _name        "recurCount"
 | 
					                _name        "recurCount"
 | 
				
			||||||
                _placeholder "0"
 | 
					                _placeholder "0"
 | 
				
			||||||
                _value       (string req.recurCount)
 | 
					                _value       recurCount
 | 
				
			||||||
                _style       "width:6rem;"
 | 
					                _style       "width:6rem;"
 | 
				
			||||||
                _required
 | 
					                _required
 | 
				
			||||||
                match req.recurType with Immediate -> _disabled | _ -> ()
 | 
					                match req.recurrence with Immediate -> _disabled | _ -> ()
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
              label [ _for "recurCount" ] [ str "Count" ]
 | 
					              label [ _for "recurCount" ] [ str "Count" ]
 | 
				
			||||||
              ]
 | 
					              ]
 | 
				
			||||||
@ -239,11 +246,11 @@ let edit (req : JournalRequest) returnTo isNew =
 | 
				
			|||||||
                _name     "recurInterval"
 | 
					                _name     "recurInterval"
 | 
				
			||||||
                _style    "width:6rem;"
 | 
					                _style    "width:6rem;"
 | 
				
			||||||
                _required
 | 
					                _required
 | 
				
			||||||
                match req.recurType with Immediate -> _disabled | _ -> ()
 | 
					                match req.recurrence with Immediate -> _disabled | _ -> ()
 | 
				
			||||||
                ] [
 | 
					                ] [
 | 
				
			||||||
                option [ _value "Hours"; match req.recurType with Hours -> _selected | _ -> () ] [ str "hours" ]
 | 
					                option [ _value "Hours"; match req.recurrence with Hours _ -> _selected | _ -> () ] [ str "hours" ]
 | 
				
			||||||
                option [ _value "Days";  match req.recurType with Days  -> _selected | _ -> () ] [ str "days" ]
 | 
					                option [ _value "Days";  match req.recurrence with Days  _ -> _selected | _ -> () ] [ str "days" ]
 | 
				
			||||||
                option [ _value "Weeks"; match req.recurType with Weeks -> _selected | _ -> () ] [ str "weeks" ]
 | 
					                option [ _value "Weeks"; match req.recurrence with Weeks _ -> _selected | _ -> () ] [ str "weeks" ]
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
              label [ _form "recurInterval" ] [ str "Interval" ]
 | 
					              label [ _form "recurInterval" ] [ str "Interval" ]
 | 
				
			||||||
              ]
 | 
					              ]
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								src/MyPrayerJournal/wwwroot/script/htmx-1.6.1.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/MyPrayerJournal/wwwroot/script/htmx-1.6.1.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user