Version 8 #43
@ -2,6 +2,7 @@
|
||||
module PrayerTracker.DataAccess
|
||||
|
||||
open System.Linq
|
||||
open NodaTime
|
||||
open PrayerTracker.Entities
|
||||
|
||||
[<AutoOpen>]
|
||||
@ -24,7 +25,6 @@ module private Helpers =
|
||||
if pageNbr > 0 then q.Skip((pageNbr - 1) * pageSize).Take pageSize else q
|
||||
|
||||
|
||||
open System
|
||||
open Microsoft.EntityFrameworkCore
|
||||
open Microsoft.FSharpLu
|
||||
|
||||
@ -90,12 +90,14 @@ type AppDbContext with
|
||||
|
||||
/// Get all (or active) requests for a small group as of now or the specified date
|
||||
member this.AllRequestsForSmallGroup (grp : SmallGroup) clock listDate activeOnly pageNbr = backgroundTask {
|
||||
let theDate = match listDate with Some dt -> dt | _ -> grp.LocalDateNow clock
|
||||
let theDate = match listDate with Some dt -> dt | _ -> SmallGroup.localDateNow clock grp
|
||||
let query =
|
||||
this.PrayerRequests.Where(fun req -> req.SmallGroupId = grp.Id)
|
||||
|> function
|
||||
| q when activeOnly ->
|
||||
let asOf = DateTime (theDate.AddDays(-(float grp.Preferences.DaysToExpire)).Date.Ticks, DateTimeKind.Utc)
|
||||
let asOf =
|
||||
(theDate.AtStartOfDayInZone(SmallGroup.timeZone grp) - Duration.FromDays grp.Preferences.DaysToExpire)
|
||||
.ToInstant ()
|
||||
q.Where(fun req ->
|
||||
( req.UpdatedDate > asOf
|
||||
|| req.Expiration = Manual
|
||||
|
@ -1,7 +1,5 @@
|
||||
namespace PrayerTracker.Entities
|
||||
|
||||
// fsharplint:disable RecordFieldNames MemberNames
|
||||
|
||||
(*-- SUPPORT TYPES --*)
|
||||
|
||||
/// How as-of dates should (or should not) be displayed with requests
|
||||
@ -633,10 +631,10 @@ and [<CLIMutable; NoComparison; NoEquality>] PrayerRequest =
|
||||
SmallGroupId : SmallGroupId
|
||||
|
||||
/// The date/time on which this request was entered
|
||||
EnteredDate : DateTime
|
||||
EnteredDate : Instant
|
||||
|
||||
/// The date/time this request was last updated
|
||||
UpdatedDate : DateTime
|
||||
UpdatedDate : Instant
|
||||
|
||||
/// The name of the requestor or subject, or title of announcement
|
||||
Requestor : string option
|
||||
@ -664,8 +662,8 @@ with
|
||||
RequestType = CurrentRequest
|
||||
UserId = UserId Guid.Empty
|
||||
SmallGroupId = SmallGroupId Guid.Empty
|
||||
EnteredDate = DateTime.MinValue
|
||||
UpdatedDate = DateTime.MinValue
|
||||
EnteredDate = Instant.MinValue
|
||||
UpdatedDate = Instant.MinValue
|
||||
Requestor = None
|
||||
Text = ""
|
||||
NotifyChaplain = false
|
||||
@ -674,20 +672,6 @@ with
|
||||
Expiration = Automatic
|
||||
}
|
||||
|
||||
/// Is this request expired?
|
||||
member this.IsExpired (curr : DateTime) expDays =
|
||||
match this.Expiration, this.RequestType with
|
||||
| Forced, _ -> true
|
||||
| Manual, _
|
||||
| Automatic, LongTermRequest
|
||||
| Automatic, Expecting -> false
|
||||
| Automatic, _ -> curr.AddDays(-(float expDays)).Date > this.UpdatedDate.Date // Automatic expiration
|
||||
|
||||
/// Is an update required for this long-term request?
|
||||
member this.UpdateRequired curr expDays updWeeks =
|
||||
if this.IsExpired curr expDays then false
|
||||
else curr.AddDays(-(float (updWeeks * 7))).Date > this.UpdatedDate.Date
|
||||
|
||||
/// Configure EF for this entity
|
||||
static member internal ConfigureEF (mb : ModelBuilder) =
|
||||
mb.Entity<PrayerRequest> (fun it ->
|
||||
@ -759,19 +743,6 @@ with
|
||||
Users = ResizeArray<UserSmallGroup> ()
|
||||
}
|
||||
|
||||
/// Get the local date for this group
|
||||
member this.LocalTimeNow (clock : IClock) =
|
||||
if isNull clock then nullArg (nameof clock)
|
||||
let tzId = TimeZoneId.toString this.Preferences.TimeZoneId
|
||||
let tz =
|
||||
if DateTimeZoneProviders.Tzdb.Ids.Contains tzId then DateTimeZoneProviders.Tzdb[tzId]
|
||||
else DateTimeZone.Utc
|
||||
clock.GetCurrentInstant().InZone(tz).ToDateTimeUnspecified ()
|
||||
|
||||
/// Get the local date for this group
|
||||
member this.LocalDateNow clock =
|
||||
(this.LocalTimeNow clock).Date
|
||||
|
||||
/// Configure EF for this entity
|
||||
static member internal ConfigureEF (mb : ModelBuilder) =
|
||||
mb.Entity<SmallGroup> (fun it ->
|
||||
@ -855,7 +826,7 @@ and [<CLIMutable; NoComparison; NoEquality>] User =
|
||||
Salt : Guid option
|
||||
|
||||
/// The last time the user was seen (set whenever the user is loaded into a session)
|
||||
LastSeen : DateTime option
|
||||
LastSeen : Instant option
|
||||
|
||||
/// The small groups which this user is authorized
|
||||
SmallGroups : ResizeArray<UserSmallGroup>
|
||||
@ -900,7 +871,7 @@ with
|
||||
mb.Model.FindEntityType(typeof<User>).FindProperty(nameof User.empty.Salt)
|
||||
.SetValueConverter (OptionConverter<Guid> ())
|
||||
mb.Model.FindEntityType(typeof<User>).FindProperty(nameof User.empty.LastSeen)
|
||||
.SetValueConverter (OptionConverter<DateTime> ())
|
||||
.SetValueConverter (OptionConverter<Instant> ())
|
||||
|
||||
|
||||
/// Cross-reference between user and small group
|
||||
@ -947,4 +918,44 @@ with
|
||||
.SetValueConverter (Converters.UserIdConverter ())
|
||||
mb.Model.FindEntityType(typeof<UserSmallGroup>).FindProperty(nameof UserSmallGroup.empty.SmallGroupId)
|
||||
.SetValueConverter (Converters.SmallGroupIdConverter ())
|
||||
|
||||
|
||||
|
||||
/// Support functions for small groups
|
||||
module SmallGroup =
|
||||
|
||||
/// The DateTimeZone for the time zone ID for this small group
|
||||
let timeZone group =
|
||||
let tzId = TimeZoneId.toString group.Preferences.TimeZoneId
|
||||
if DateTimeZoneProviders.Tzdb.Ids.Contains tzId then DateTimeZoneProviders.Tzdb[tzId]
|
||||
else DateTimeZone.Utc
|
||||
|
||||
/// Get the local date/time for this group
|
||||
let localTimeNow (clock : IClock) group =
|
||||
if isNull clock then nullArg (nameof clock)
|
||||
clock.GetCurrentInstant().InZone(timeZone group).LocalDateTime
|
||||
|
||||
/// Get the local date for this group
|
||||
let localDateNow clock group =
|
||||
(localTimeNow clock group).Date
|
||||
|
||||
|
||||
|
||||
/// Support functions for prayer requests
|
||||
module PrayerRequest =
|
||||
|
||||
/// Is this request expired?
|
||||
let isExpired (asOf : LocalDate) group req =
|
||||
match req.Expiration, req.RequestType with
|
||||
| Forced, _ -> true
|
||||
| Manual, _
|
||||
| Automatic, LongTermRequest
|
||||
| Automatic, Expecting -> false
|
||||
| Automatic, _ ->
|
||||
// Automatic expiration
|
||||
asOf.PlusDays -group.Preferences.DaysToExpire > req.UpdatedDate.InZone(SmallGroup.timeZone group).Date
|
||||
|
||||
/// Is an update required for this long-term request?
|
||||
let updateRequired asOf group req =
|
||||
if isExpired asOf group req then false
|
||||
else asOf.PlusWeeks -group.Preferences.LongTermUpdateWeeks
|
||||
>= req.UpdatedDate.InZone(SmallGroup.timeZone group).Date
|
||||
|
@ -17,7 +17,7 @@
|
||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.FSharpLu" Version="0.11.7" />
|
||||
<PackageReference Include="NodaTime" Version="3.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -255,13 +255,13 @@ let private messages viewInfo =
|
||||
|> List.singleton
|
||||
|
||||
|
||||
open System
|
||||
open NodaTime
|
||||
|
||||
/// Render the <footer> at the bottom of the page
|
||||
let private htmlFooter viewInfo =
|
||||
let s = I18N.localizer.Force ()
|
||||
let imgText = $"""%O{s["PrayerTracker"]} %O{s["from Bit Badger Solutions"]}"""
|
||||
let resultTime = TimeSpan(DateTime.Now.Ticks - viewInfo.RequestStart).TotalSeconds
|
||||
let resultTime = (SystemClock.Instance.GetCurrentInstant () - viewInfo.RequestStart).TotalSeconds
|
||||
footer [ _class "pt-footer" ] [
|
||||
div [ _id "pt-legal" ] [
|
||||
a [ _href "/legal/privacy-policy" ] [ locStr s["Privacy Policy"] ]
|
||||
|
@ -154,23 +154,22 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo =
|
||||
let l = I18N.forView "Requests/Maintain"
|
||||
use sw = new StringWriter ()
|
||||
let raw = rawLocText sw
|
||||
let now = model.SmallGroup.LocalDateNow (ctx.GetService<IClock> ())
|
||||
let prefs = model.SmallGroup.Preferences
|
||||
let group = model.SmallGroup
|
||||
let now = SmallGroup.localDateNow (ctx.GetService<IClock> ()) group
|
||||
let types = ReferenceList.requestTypeList s |> Map.ofList
|
||||
let updReq (req : PrayerRequest) =
|
||||
if req.UpdateRequired now prefs.DaysToExpire prefs.LongTermUpdateWeeks then "cell pt-request-update" else "cell"
|
||||
|> _class
|
||||
let reqExp (req : PrayerRequest) =
|
||||
_class (if req.IsExpired now prefs.DaysToExpire then "cell pt-request-expired" else "cell")
|
||||
let vi = AppViewInfo.withScopedStyles [ "#requestList { grid-template-columns: repeat(5, auto); }" ] viewInfo
|
||||
let vi = AppViewInfo.withScopedStyles [ "#requestList { grid-template-columns: repeat(5, auto); }" ] viewInfo
|
||||
/// Iterate the sequence once, before we render, so we can get the count of it at the top of the table
|
||||
let requests =
|
||||
model.Requests
|
||||
|> List.map (fun req ->
|
||||
let reqId = shortGuid req.Id.Value
|
||||
let reqText = htmlToPlainText req.Text
|
||||
let delAction = $"/prayer-request/{reqId}/delete"
|
||||
let delPrompt =
|
||||
let updateClass =
|
||||
_class (if PrayerRequest.updateRequired now group req then "cell pt-request-update" else "cell")
|
||||
let isExpired = PrayerRequest.isExpired now group req
|
||||
let expiredClass = _class (if isExpired then "cell pt-request-expired" else "cell")
|
||||
let reqId = shortGuid req.Id.Value
|
||||
let reqText = htmlToPlainText req.Text
|
||||
let delAction = $"/prayer-request/{reqId}/delete"
|
||||
let delPrompt =
|
||||
[ s["Are you sure you want to delete this {0}? This action cannot be undone.",
|
||||
s["Prayer Request"].Value.ToLower() ].Value
|
||||
"\\n"
|
||||
@ -183,7 +182,7 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo =
|
||||
a [ _href $"/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] [
|
||||
iconSized 18 "edit"
|
||||
]
|
||||
if req.IsExpired now prefs.DaysToExpire then
|
||||
if isExpired then
|
||||
a [ _href $"/prayer-request/{reqId}/restore"
|
||||
_title l["Restore This Inactive Request"].Value ] [
|
||||
iconSized 18 "visibility"
|
||||
@ -200,11 +199,11 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo =
|
||||
iconSized 18 "delete_forever"
|
||||
]
|
||||
]
|
||||
div [ updReq req ] [
|
||||
div [ updateClass ] [
|
||||
str (req.UpdatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture))
|
||||
]
|
||||
div [ _class "cell" ] [ locStr types[req.RequestType] ]
|
||||
div [ reqExp req ] [ str (match req.Requestor with Some r -> r | None -> " ") ]
|
||||
div [ expiredClass ] [ str (match req.Requestor with Some r -> r | None -> " ") ]
|
||||
div [ _class "cell" ] [
|
||||
match reqText.Length with
|
||||
| len when len < 60 -> rawText reqText
|
||||
@ -316,7 +315,7 @@ let view model viewInfo =
|
||||
let s = I18N.localizer.Force ()
|
||||
let pageTitle = $"""{s["Prayer Requests"].Value} • {model.SmallGroup.Name}"""
|
||||
let spacer = rawText " "
|
||||
let dtString = model.Date.ToString "yyyy-MM-dd"
|
||||
let dtString = model.Date.ToString ("yyyy-MM-dd", null) // TODO: this should be invariant
|
||||
[ div [ _class "pt-center-text" ] [
|
||||
br []
|
||||
a [ _class "pt-icon-link"
|
||||
@ -327,12 +326,12 @@ let view model viewInfo =
|
||||
]
|
||||
if model.CanEmail then
|
||||
spacer
|
||||
if model.Date.DayOfWeek <> DayOfWeek.Sunday then
|
||||
let rec findSunday (date : DateTime) =
|
||||
if date.DayOfWeek = DayOfWeek.Sunday then date else findSunday (date.AddDays 1.)
|
||||
if model.Date.DayOfWeek <> IsoDayOfWeek.Sunday then
|
||||
let rec findSunday (date : LocalDate) =
|
||||
if date.DayOfWeek = IsoDayOfWeek.Sunday then date else findSunday (date.PlusDays 1)
|
||||
let sunday = findSunday model.Date
|
||||
a [ _class "pt-icon-link"
|
||||
_href $"""/prayer-requests/view/{sunday.ToString "yyyy-MM-dd"}"""
|
||||
_href $"""/prayer-requests/view/{sunday.ToString ("yyyy-MM-dd", null)}""" // TODO: make invariant
|
||||
_title s["List for Next Sunday"].Value ] [
|
||||
icon "update"; rawText " "; locStr s["List for Next Sunday"]
|
||||
]
|
||||
|
@ -217,7 +217,10 @@ let maintain (users : User list) ctx viewInfo =
|
||||
]
|
||||
div [ _class "cell" ] [ str user.Name ]
|
||||
div [ _class "cell" ] [
|
||||
str (match user.LastSeen with Some dt -> dt.ToString s["MMMM d, yyyy"] | None -> "--")
|
||||
match user.LastSeen with
|
||||
| Some dt -> dt.ToString (s["MMMM d, yyyy"].Value, null)
|
||||
| None -> "--"
|
||||
|> str
|
||||
]
|
||||
div [ _class "cell pt-center-text" ] [
|
||||
if user.IsAdmin then strong [] [ locStr s["Yes"] ] else locStr s["No"]
|
||||
|
@ -116,7 +116,7 @@ type LayoutType =
|
||||
| ContentOnly
|
||||
|
||||
|
||||
open System
|
||||
open NodaTime
|
||||
|
||||
/// View model required by the layout template, given as first parameter for all pages in PrayerTracker
|
||||
[<NoComparison; NoEquality>]
|
||||
@ -134,7 +134,7 @@ type AppViewInfo =
|
||||
Version : string
|
||||
|
||||
/// The ticks when the request started
|
||||
RequestStart : int64
|
||||
RequestStart : Instant
|
||||
|
||||
/// The currently logged on user, if there is one
|
||||
User : User option
|
||||
@ -151,7 +151,6 @@ type AppViewInfo =
|
||||
/// A JavaScript function to run on page load
|
||||
OnLoadScript : string option
|
||||
}
|
||||
// TODO: add onload script option to this, modify layout to add it
|
||||
|
||||
/// Support for the AppViewInfo type
|
||||
module AppViewInfo =
|
||||
@ -162,7 +161,7 @@ module AppViewInfo =
|
||||
HelpLink = None
|
||||
Messages = []
|
||||
Version = ""
|
||||
RequestStart = DateTime.Now.Ticks
|
||||
RequestStart = Instant.MinValue
|
||||
User = None
|
||||
Group = None
|
||||
Layout = FullPage
|
||||
@ -467,7 +466,7 @@ type EditRequest =
|
||||
RequestType : string
|
||||
|
||||
/// The date of the request
|
||||
EnteredDate : DateTime option
|
||||
EnteredDate : string option
|
||||
|
||||
/// Whether to update the date or not
|
||||
SkipDateUpdate : bool option
|
||||
@ -724,6 +723,7 @@ module UserLogOn =
|
||||
}
|
||||
|
||||
|
||||
open System
|
||||
open Giraffe.ViewEngine
|
||||
|
||||
/// This represents a list of requests
|
||||
@ -732,7 +732,7 @@ type RequestList =
|
||||
Requests : PrayerRequest list
|
||||
|
||||
/// The date for which this list is being generated
|
||||
Date : DateTime
|
||||
Date : LocalDate
|
||||
|
||||
/// The small group to which this list belongs
|
||||
SmallGroup : SmallGroup
|
||||
@ -767,30 +767,31 @@ with
|
||||
|
||||
/// Is this request new?
|
||||
member this.IsNew (req : PrayerRequest) =
|
||||
(this.Date - req.UpdatedDate).Days <= this.SmallGroup.Preferences.DaysToKeepNew
|
||||
let reqDate = req.UpdatedDate.InZone(SmallGroup.timeZone this.SmallGroup).Date
|
||||
Period.Between(this.Date, reqDate, PeriodUnits.Days).Days <= this.SmallGroup.Preferences.DaysToKeepNew
|
||||
|
||||
/// Generate this list as HTML
|
||||
member this.AsHtml (s : IStringLocalizer) =
|
||||
let prefs = this.SmallGroup.Preferences
|
||||
let asOfSize = Math.Round (float prefs.TextFontSize * 0.8, 2)
|
||||
let p = this.SmallGroup.Preferences
|
||||
let asOfSize = Math.Round (float p.TextFontSize * 0.8, 2)
|
||||
[ if this.ShowHeader then
|
||||
div [ _style $"text-align:center;font-family:{prefs.Fonts}" ] [
|
||||
span [ _style $"font-size:%i{prefs.HeadingFontSize}pt;" ] [
|
||||
div [ _style $"text-align:center;font-family:{p.Fonts}" ] [
|
||||
span [ _style $"font-size:%i{p.HeadingFontSize}pt;" ] [
|
||||
strong [] [ str s["Prayer Requests"].Value ]
|
||||
]
|
||||
br []
|
||||
span [ _style $"font-size:%i{prefs.TextFontSize}pt;" ] [
|
||||
span [ _style $"font-size:%i{p.TextFontSize}pt;" ] [
|
||||
strong [] [ str this.SmallGroup.Name ]
|
||||
br []
|
||||
str (this.Date.ToString s["MMMM d, yyyy"].Value)
|
||||
str (this.Date.ToString (s["MMMM d, yyyy"].Value, null))
|
||||
]
|
||||
]
|
||||
br []
|
||||
for _, name, reqs in this.RequestsByType s do
|
||||
div [ _style "padding-left:10px;padding-bottom:.5em;" ] [
|
||||
table [ _style $"font-family:{prefs.Fonts};page-break-inside:avoid;" ] [
|
||||
table [ _style $"font-family:{p.Fonts};page-break-inside:avoid;" ] [
|
||||
tr [] [
|
||||
td [ _style $"font-size:%i{prefs.HeadingFontSize}pt;color:{prefs.HeadingColor};padding:3px 0;border-top:solid 3px {prefs.LineColor};border-bottom:solid 3px {prefs.LineColor};font-weight:bold;" ] [
|
||||
td [ _style $"font-size:%i{p.HeadingFontSize}pt;color:{p.HeadingColor};padding:3px 0;border-top:solid 3px {p.LineColor};border-bottom:solid 3px {p.LineColor};font-weight:bold;" ] [
|
||||
rawText " "; str name.Value; rawText " "
|
||||
]
|
||||
]
|
||||
@ -799,7 +800,7 @@ with
|
||||
reqs
|
||||
|> List.map (fun req ->
|
||||
let bullet = if this.IsNew req then "circle" else "disc"
|
||||
li [ _style $"list-style-type:{bullet};font-family:{prefs.Fonts};font-size:%i{prefs.TextFontSize}pt;padding-bottom:.25em;" ] [
|
||||
li [ _style $"list-style-type:{bullet};font-family:{p.Fonts};font-size:%i{p.TextFontSize}pt;padding-bottom:.25em;" ] [
|
||||
match req.Requestor with
|
||||
| Some r when r <> "" ->
|
||||
strong [] [ str r ]
|
||||
@ -807,14 +808,14 @@ with
|
||||
| Some _ -> ()
|
||||
| None -> ()
|
||||
rawText req.Text
|
||||
match prefs.AsOfDateDisplay with
|
||||
match p.AsOfDateDisplay with
|
||||
| NoDisplay -> ()
|
||||
| ShortDate
|
||||
| LongDate ->
|
||||
let dt =
|
||||
match prefs.AsOfDateDisplay with
|
||||
| ShortDate -> req.UpdatedDate.ToShortDateString ()
|
||||
| LongDate -> req.UpdatedDate.ToLongDateString ()
|
||||
match p.AsOfDateDisplay with
|
||||
| ShortDate -> req.UpdatedDate.ToString ("d", null)
|
||||
| LongDate -> req.UpdatedDate.ToString ("D", null)
|
||||
| _ -> ""
|
||||
i [ _style $"font-size:%.2f{asOfSize}pt" ] [
|
||||
rawText " ("; str s["as of"].Value; str " "; str dt; rawText ")"
|
||||
@ -830,7 +831,7 @@ with
|
||||
seq {
|
||||
this.SmallGroup.Name
|
||||
s["Prayer Requests"].Value
|
||||
this.Date.ToString s["MMMM d, yyyy"].Value
|
||||
this.Date.ToString (s["MMMM d, yyyy"].Value, null)
|
||||
" "
|
||||
for _, name, reqs in this.RequestsByType s do
|
||||
let dashes = String.replicate (name.Value.Length + 4) "-"
|
||||
@ -845,8 +846,8 @@ with
|
||||
| _ ->
|
||||
let dt =
|
||||
match this.SmallGroup.Preferences.AsOfDateDisplay with
|
||||
| ShortDate -> req.UpdatedDate.ToShortDateString ()
|
||||
| LongDate -> req.UpdatedDate.ToLongDateString ()
|
||||
| ShortDate -> req.UpdatedDate.ToString ("d", null)
|
||||
| LongDate -> req.UpdatedDate.ToString ("D", null)
|
||||
| _ -> ""
|
||||
$""" ({s["as of"].Value} {dt})"""
|
||||
|> sprintf " %s %s%s%s" bullet requestor (htmlToPlainText req.Text)
|
||||
|
@ -1,17 +1,17 @@
|
||||
namespace PrayerTracker
|
||||
|
||||
open System
|
||||
open Microsoft.AspNetCore.Http
|
||||
|
||||
/// Middleware to add the starting ticks for the request
|
||||
type RequestStartMiddleware (next : RequestDelegate) =
|
||||
|
||||
member this.InvokeAsync (ctx : HttpContext) = task {
|
||||
ctx.Items[Key.startTime] <- DateTime.Now.Ticks
|
||||
ctx.Items[Key.startTime] <- ctx.Now
|
||||
return! next.Invoke ctx
|
||||
}
|
||||
|
||||
|
||||
open System
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.Hosting
|
||||
|
||||
@ -71,9 +71,13 @@ module Configure =
|
||||
let _ = svc.AddSingleton<IClock> SystemClock.Instance
|
||||
|
||||
let config = svc.BuildServiceProvider().GetRequiredService<IConfiguration> ()
|
||||
//NpgsqlConnection.GlobalTypeMapper.
|
||||
let _ =
|
||||
svc.AddDbContext<AppDbContext> (
|
||||
(fun options -> options.UseNpgsql (config.GetConnectionString "PrayerTracker") |> ignore),
|
||||
(fun options ->
|
||||
options.UseNpgsql (
|
||||
config.GetConnectionString "PrayerTracker", fun o -> o.UseNodaTime () |> ignore)
|
||||
|> ignore),
|
||||
ServiceLifetime.Scoped, ServiceLifetime.Singleton)
|
||||
()
|
||||
|
||||
|
@ -44,6 +44,7 @@ let appVersion =
|
||||
open Giraffe
|
||||
open Giraffe.Htmx
|
||||
open Microsoft.AspNetCore.Http
|
||||
open NodaTime
|
||||
open PrayerTracker
|
||||
open PrayerTracker.ViewModels
|
||||
|
||||
@ -63,7 +64,7 @@ let viewInfo (ctx : HttpContext) =
|
||||
{ AppViewInfo.fresh with
|
||||
Version = appVersion
|
||||
Messages = msg
|
||||
RequestStart = ctx.Items[Key.startTime] :?> int64
|
||||
RequestStart = ctx.Items[Key.startTime] :?> Instant
|
||||
User = ctx.Session.CurrentUser
|
||||
Group = ctx.Session.CurrentGroup
|
||||
Layout = layout
|
||||
|
@ -1,7 +1,6 @@
|
||||
[<AutoOpen>]
|
||||
module PrayerTracker.Extensions
|
||||
|
||||
open System
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.FSharpLu
|
||||
open Newtonsoft.Json
|
||||
@ -76,6 +75,9 @@ type HttpContext with
|
||||
/// The system clock (via DI)
|
||||
member this.Clock = this.GetService<IClock> ()
|
||||
|
||||
/// The current instant
|
||||
member this.Now = this.Clock.GetCurrentInstant ()
|
||||
|
||||
/// The currently logged on small group (sets the value in the session if it is missing)
|
||||
member this.CurrentGroup () = task {
|
||||
match this.Session.CurrentGroup with
|
||||
@ -101,7 +103,7 @@ type HttpContext with
|
||||
match! this.Db.TryUserById userId with
|
||||
| Some user ->
|
||||
// Set last seen for user
|
||||
this.Db.UpdateEntry { user with LastSeen = Some DateTime.UtcNow }
|
||||
this.Db.UpdateEntry { user with LastSeen = Some this.Now }
|
||||
let! _ = this.Db.SaveChangesAsync ()
|
||||
this.Session.CurrentUser <- Some user
|
||||
return Some user
|
||||
|
@ -20,7 +20,7 @@ let private findRequest (ctx : HttpContext) reqId = task {
|
||||
/// Generate a list of requests for the given date
|
||||
let private generateRequestList (ctx : HttpContext) date = task {
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
let listDate = match date with Some d -> d | None -> group.LocalDateNow ctx.Clock
|
||||
let listDate = match date with Some d -> d | None -> SmallGroup.localDateNow ctx.Clock group
|
||||
let! reqs = ctx.Db.AllRequestsForSmallGroup group ctx.Clock (Some listDate) true 0
|
||||
return
|
||||
{ Requests = reqs
|
||||
@ -32,29 +32,31 @@ let private generateRequestList (ctx : HttpContext) date = task {
|
||||
}
|
||||
}
|
||||
|
||||
open System
|
||||
open NodaTime.Text
|
||||
|
||||
/// Parse a string into a date (optionally, of course)
|
||||
let private parseListDate (date : string option) =
|
||||
match date with
|
||||
| Some dt -> match DateTime.TryParse dt with true, d -> Some d | false, _ -> None
|
||||
| Some dt -> match LocalDatePattern.Iso.Parse dt with it when it.Success -> Some it.Value | _ -> None
|
||||
| None -> None
|
||||
|
||||
open System
|
||||
|
||||
/// GET /prayer-request/[request-id]/edit
|
||||
let edit reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
let now = group.LocalDateNow ctx.Clock
|
||||
let now = SmallGroup.localDateNow ctx.Clock group
|
||||
let requestId = PrayerRequestId reqId
|
||||
if requestId.Value = Guid.Empty then
|
||||
return!
|
||||
{ viewInfo ctx with HelpLink = Some Help.editRequest }
|
||||
|> Views.PrayerRequest.edit EditRequest.empty (now.ToString "yyyy-MM-dd") ctx
|
||||
|> Views.PrayerRequest.edit EditRequest.empty (now.ToString ("R", null)) ctx
|
||||
|> renderHtml next ctx
|
||||
else
|
||||
match! findRequest ctx requestId with
|
||||
| Ok req ->
|
||||
let s = Views.I18N.localizer.Force ()
|
||||
if req.IsExpired now group.Preferences.DaysToExpire then
|
||||
if PrayerRequest.isExpired now group req then
|
||||
{ UserMessage.warning with
|
||||
Text = htmlLocString s["This request is expired."]
|
||||
Description =
|
||||
@ -128,7 +130,7 @@ let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun ne
|
||||
viewInfo ctx
|
||||
|> Views.PrayerRequest.list
|
||||
{ Requests = reqs
|
||||
Date = group.LocalDateNow ctx.Clock
|
||||
Date = SmallGroup.localDateNow ctx.Clock group
|
||||
SmallGroup = group
|
||||
ShowHeader = true
|
||||
CanEmail = Option.isSome ctx.User.UserId
|
||||
@ -199,7 +201,7 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas
|
||||
match! findRequest ctx requestId with
|
||||
| Ok req ->
|
||||
let s = Views.I18N.localizer.Force ()
|
||||
ctx.Db.UpdateEntry { req with Expiration = Automatic; UpdatedDate = DateTime.Now }
|
||||
ctx.Db.UpdateEntry { req with Expiration = Automatic; UpdatedDate = ctx.Now }
|
||||
let! _ = ctx.Db.SaveChangesAsync ()
|
||||
addInfo ctx s["Successfully {0} prayer request", s["Restored"].Value.ToLower ()]
|
||||
return! redirectTo false "/prayer-requests" next ctx
|
||||
@ -226,10 +228,13 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct
|
||||
Expiration = Expiration.fromCode model.Expiration
|
||||
}
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
let now = group.LocalDateNow ctx.Clock
|
||||
let now = SmallGroup.localDateNow ctx.Clock group
|
||||
match model.IsNew with
|
||||
| true ->
|
||||
let dt = defaultArg model.EnteredDate now
|
||||
let dt =
|
||||
(defaultArg (parseListDate model.EnteredDate) now)
|
||||
.AtStartOfDayInZone(SmallGroup.timeZone group)
|
||||
.ToInstant()
|
||||
{ upd8 with
|
||||
SmallGroupId = group.Id
|
||||
UserId = ctx.User.UserId.Value
|
||||
@ -237,7 +242,7 @@ let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ct
|
||||
UpdatedDate = dt
|
||||
}
|
||||
| false when defaultArg model.SkipDateUpdate false -> upd8
|
||||
| false -> { upd8 with UpdatedDate = now }
|
||||
| false -> { upd8 with UpdatedDate = ctx.Now }
|
||||
|> if model.IsNew then ctx.Db.AddEntry else ctx.Db.UpdateEntry
|
||||
let! _ = ctx.Db.SaveChangesAsync ()
|
||||
let s = Views.I18N.localizer.Force ()
|
||||
|
@ -27,8 +27,9 @@
|
||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" />
|
||||
<PackageReference Include="NeoSmart.Caching.Sqlite" Version="6.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="6.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -261,14 +261,14 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
|
||||
match! ctx.TryBindFormAsync<Announcement> () with
|
||||
| Ok model ->
|
||||
let group = ctx.Session.CurrentGroup.Value
|
||||
let prefs = group.Preferences
|
||||
let pref = group.Preferences
|
||||
let usr = ctx.Session.CurrentUser.Value
|
||||
let now = group.LocalTimeNow ctx.Clock
|
||||
let now = SmallGroup.localTimeNow ctx.Clock group
|
||||
let s = Views.I18N.localizer.Force ()
|
||||
// Reformat the text to use the class's font stylings
|
||||
let requestText = ckEditorToText model.Text
|
||||
let htmlText =
|
||||
p [ _style $"font-family:{prefs.Fonts};font-size:%d{prefs.TextFontSize}pt;" ] [ rawText requestText ]
|
||||
p [ _style $"font-family:{pref.Fonts};font-size:%d{pref.TextFontSize}pt;" ] [ rawText requestText ]
|
||||
|> renderHtmlNode
|
||||
let plainText = (htmlToPlainText >> wordWrap 74) htmlText
|
||||
// Send the e-mails
|
||||
@ -282,7 +282,7 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
|
||||
Recipients = recipients
|
||||
Group = group
|
||||
Subject = s["Announcement for {0} - {1:MMMM d, yyyy} {2}", group.Name, now.Date,
|
||||
(now.ToString "h:mm tt").ToLower ()].Value
|
||||
(now.ToString ("h:mm tt", null)).ToLower ()].Value
|
||||
HtmlBody = htmlText
|
||||
PlainTextBody = plainText
|
||||
Strings = s
|
||||
@ -293,14 +293,15 @@ let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=>
|
||||
| _, None -> ()
|
||||
| _, Some x when not x -> ()
|
||||
| _, _ ->
|
||||
let zone = SmallGroup.timeZone group
|
||||
{ PrayerRequest.empty with
|
||||
Id = (Guid.NewGuid >> PrayerRequestId) ()
|
||||
SmallGroupId = group.Id
|
||||
UserId = usr.Id
|
||||
RequestType = (Option.get >> PrayerRequestType.fromCode) model.RequestType
|
||||
Text = requestText
|
||||
EnteredDate = now
|
||||
UpdatedDate = now
|
||||
EnteredDate = now.Date.AtStartOfDayInZone(zone).ToInstant()
|
||||
UpdatedDate = now.InZoneLeniently(zone).ToInstant()
|
||||
}
|
||||
|> ctx.Db.AddEntry
|
||||
let! _ = ctx.Db.SaveChangesAsync ()
|
||||
|
@ -145,7 +145,7 @@ let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsr
|
||||
AuthenticationProperties (
|
||||
IssuedUtc = DateTimeOffset.UtcNow,
|
||||
IsPersistent = defaultArg model.RememberMe false))
|
||||
ctx.Db.UpdateEntry { user with LastSeen = Some DateTime.UtcNow }
|
||||
ctx.Db.UpdateEntry { user with LastSeen = Some ctx.Now }
|
||||
let! _ = ctx.Db.SaveChangesAsync ()
|
||||
addHtmlInfo ctx s["Log On Successful • Welcome to {0}", s["PrayerTracker"]]
|
||||
return! redirectTo false (sanitizeUrl model.RedirectUrl "/small-group") next ctx
|
||||
|
@ -94,7 +94,7 @@ ALTER TABLE pt."User" RENAME COLUMN "PasswordHash" TO password_hash;
|
||||
ALTER TABLE pt."User" RENAME COLUMN "Salt" TO salt;
|
||||
ALTER TABLE pt."User" RENAME CONSTRAINT "PK_User" TO pk_pt_user;
|
||||
ALTER TABLE pt."User" RENAME TO pt_user;
|
||||
ALTER TABLE pt.pt_user ADD COLUMN last_seen timestamp;
|
||||
ALTER TABLE pt.pt_user ADD COLUMN last_seen timestamptz;
|
||||
|
||||
-- User / Small Group
|
||||
ALTER TABLE pt."User_SmallGroup" RENAME COLUMN "UserId" TO user_id;
|
||||
@ -105,3 +105,8 @@ ALTER TABLE pt."User_SmallGroup" RENAME CONSTRAINT "FK_User_SmallGroup_SmallGrou
|
||||
ALTER TABLE pt."User_SmallGroup" RENAME TO user_small_group;
|
||||
|
||||
ALTER INDEX pt."IX_User_SmallGroup_SmallGroupId" RENAME TO ix_user_small_group_small_group_id;
|
||||
|
||||
-- #41 - change to timestamptz
|
||||
SET TimeZone = 'UTC';
|
||||
ALTER TABLE pt.prayer_request ALTER COLUMN entered_date TYPE timestamptz;
|
||||
ALTER TABLE pt.prayer_request ALTER COLUMN updated_date TYPE timestamptz;
|
||||
|
Loading…
Reference in New Issue
Block a user