Search, Paging, and "As of" Date #10
| @ -98,11 +98,11 @@ type AppDbContext with | ||||
|       | query when activeOnly -> | ||||
|           let asOf = theDate.AddDays(-(float grp.preferences.daysToExpire)).Date | ||||
|           query.Where(fun pr -> | ||||
|               (pr.updatedDate > asOf | ||||
|                   || pr.doNotExpire | ||||
|                   || RequestType.Recurring = pr.requestType | ||||
|                   || RequestType.Expecting = pr.requestType) | ||||
|               && not pr.isManuallyExpired) | ||||
|               (    pr.updatedDate > asOf | ||||
|                 || pr.expiration  = Manual | ||||
|                 || pr.requestType = LongTermRequest | ||||
|                 || pr.requestType = Expecting) | ||||
|               && pr.expiration <> Forced) | ||||
|       | query -> query | ||||
|       |> reqSort grp.preferences.requestSort | ||||
|       |> function | ||||
|  | ||||
| @ -6,35 +6,6 @@ open NodaTime | ||||
| open System | ||||
| open System.Collections.Generic | ||||
| 
 | ||||
| (*-- CONSTANTS --*) | ||||
| 
 | ||||
| /// Constants to use for the e-mail type parameter | ||||
| [<RequireQualifiedAccess>] | ||||
| module EmailType = | ||||
|   /// HTML e-mail | ||||
|   [<Literal>] | ||||
|   let Html        = "Html" | ||||
|   /// Plain Text e-mail | ||||
|   [<Literal>] | ||||
|   let PlainText   = "PlainText" | ||||
|   /// E-mail with the list as an attached PDF | ||||
|   [<Literal>] | ||||
|   let AttachedPdf = "AttachedPdf" | ||||
| 
 | ||||
| /// These values match those in the RequestType document store | ||||
| [<RequireQualifiedAccess>] | ||||
| module RequestType = | ||||
|   /// Current Requests (follow expiration rules) | ||||
|   let Current      = "Current" | ||||
|   /// Long-Term / Recurring Requests (do not automatically expire) | ||||
|   let Recurring    = "Recurring" | ||||
|   /// Praise Reports (follow expiration rules) | ||||
|   let Praise       = "Praise" | ||||
|   /// Expectant Mothers (do not automatically expire) | ||||
|   let Expecting    = "Expecting" | ||||
|   /// Announcements (follow expiration rules) | ||||
|   let Announcement = "Announcement" | ||||
| 
 | ||||
| (*-- SUPPORT TYPES --*) | ||||
| 
 | ||||
| /// How as-of dates should (or should not) be displayed with requests | ||||
| @ -61,6 +32,82 @@ with | ||||
|     | LongDate -> "L" | ||||
| 
 | ||||
| 
 | ||||
| /// Acceptable e-mail formats | ||||
| type EmailFormat = | ||||
|   /// HTML e-mail | ||||
|   | HtmlFormat | ||||
|   /// Plain-text e-mail | ||||
|   | PlainTextFormat | ||||
| with | ||||
|   /// Convert to a DU case from a single-character string | ||||
|   static member fromCode code = | ||||
|     match code with | ||||
|     | "H" -> HtmlFormat | ||||
|     | "P" -> PlainTextFormat | ||||
|     | _ -> invalidArg "code" (sprintf "Unknown code %s" code) | ||||
|   /// Convert this DU case to a single-character string | ||||
|   member this.code = | ||||
|     match this with | ||||
|     | HtmlFormat -> "H" | ||||
|     | PlainTextFormat -> "P" | ||||
| 
 | ||||
| 
 | ||||
| /// Expiration for requests | ||||
| type Expiration = | ||||
|   /// Follow the rules for normal expiration | ||||
|   | Automatic | ||||
|   /// Do not expire via rules | ||||
|   | Manual | ||||
|   /// Force immediate expiration | ||||
|   | Forced | ||||
| with | ||||
|   /// Convert to a DU case from a single-character string | ||||
|   static member fromCode code = | ||||
|     match code with | ||||
|     | "A" -> Automatic | ||||
|     | "M" -> Manual | ||||
|     | "F" -> Forced | ||||
|     | _ -> invalidArg "code" (sprintf "Unknown code %s" code) | ||||
|   /// Convert this DU case to a single-character string | ||||
|   member this.code = | ||||
|     match this with | ||||
|     | Automatic -> "A" | ||||
|     | Manual -> "M" | ||||
|     | Forced -> "F" | ||||
| 
 | ||||
| 
 | ||||
| /// Types of prayer requests | ||||
| type PrayerRequestType = | ||||
|   /// Current requests | ||||
|   | CurrentRequest | ||||
|   /// Long-term/ongoing request | ||||
|   | LongTermRequest | ||||
|   /// Expectant couples | ||||
|   | Expecting | ||||
|   /// Praise reports | ||||
|   | PraiseReport | ||||
|   /// Announcements | ||||
|   | Announcement | ||||
| with | ||||
|   /// Convert to a DU case from a single-character string | ||||
|   static member fromCode code = | ||||
|     match code with | ||||
|     | "C" -> CurrentRequest | ||||
|     | "L" -> LongTermRequest | ||||
|     | "E" -> Expecting | ||||
|     | "P" -> PraiseReport | ||||
|     | "A" -> Announcement | ||||
|     | _ -> invalidArg "code" (sprintf "Unknown code %s" code) | ||||
|   /// Convert this DU case to a single-character string | ||||
|   member this.code = | ||||
|     match this with | ||||
|     | CurrentRequest -> "C" | ||||
|     | LongTermRequest -> "L" | ||||
|     | Expecting -> "E" | ||||
|     | PraiseReport -> "P" | ||||
|     | Announcement -> "A" | ||||
| 
 | ||||
| 
 | ||||
| /// How requests should be sorted | ||||
| type RequestSort = | ||||
|   /// Sort by date, then by requestor/subject | ||||
| @ -96,6 +143,26 @@ module Converters = | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
|     |> unbox<Expression<Func<string, AsOfDateDisplay>>> | ||||
|    | ||||
|   let private emailFromDU = | ||||
|     <@ Func<EmailFormat, string>(fun (x : EmailFormat) -> x.code) @> | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
|     |> unbox<Expression<Func<EmailFormat, string>>> | ||||
| 
 | ||||
|   let private emailToDU = | ||||
|     <@ Func<string, EmailFormat>(EmailFormat.fromCode) @> | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
|     |> unbox<Expression<Func<string, EmailFormat>>> | ||||
|    | ||||
|   let private expFromDU = | ||||
|     <@ Func<Expiration, string>(fun (x : Expiration) -> x.code) @> | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
|     |> unbox<Expression<Func<Expiration, string>>> | ||||
| 
 | ||||
|   let private expToDU = | ||||
|     <@ Func<string, Expiration>(Expiration.fromCode) @> | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
|     |> unbox<Expression<Func<string, Expiration>>> | ||||
|    | ||||
|   let private sortFromDU = | ||||
|     <@ Func<RequestSort, string>(fun (x : RequestSort) -> x.code) @> | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
| @ -106,10 +173,32 @@ module Converters = | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
|     |> unbox<Expression<Func<string, RequestSort>>> | ||||
|    | ||||
|   let private typFromDU = | ||||
|     <@ Func<PrayerRequestType, string>(fun (x : PrayerRequestType) -> x.code) @> | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
|     |> unbox<Expression<Func<PrayerRequestType, string>>> | ||||
| 
 | ||||
|   let private typToDU = | ||||
|     <@ Func<string, PrayerRequestType>(PrayerRequestType.fromCode) @> | ||||
|     |> LeafExpressionConverter.QuotationToExpression | ||||
|     |> unbox<Expression<Func<string, PrayerRequestType>>> | ||||
|    | ||||
|   /// Conversion between a string and an AsOfDateDisplay DU value | ||||
|   type AsOfDateDisplayConverter () = | ||||
|     inherit ValueConverter<AsOfDateDisplay, string> (asOfFromDU, asOfToDU) | ||||
| 
 | ||||
|   /// Conversion between a string and an EmailFormat DU value | ||||
|   type EmailFormatConverter () = | ||||
|     inherit ValueConverter<EmailFormat, string> (emailFromDU, emailToDU) | ||||
| 
 | ||||
|   /// Conversion between a string and an Expiration DU value | ||||
|   type ExpirationConverter () = | ||||
|     inherit ValueConverter<Expiration, string> (expFromDU, expToDU) | ||||
| 
 | ||||
|   /// Conversion between a string and an AsOfDateDisplay DU value | ||||
|   type PrayerRequestTypeConverter () = | ||||
|     inherit ValueConverter<PrayerRequestType, string> (typFromDU, typToDU) | ||||
| 
 | ||||
|   /// Conversion between a string and a RequestSort DU value | ||||
|   type RequestSortConverter () = | ||||
|     inherit ValueConverter<RequestSort, string> (sortFromDU, sortToDU) | ||||
| @ -227,7 +316,7 @@ and [<CLIMutable; NoComparison; NoEquality>] ListPreferences = | ||||
|     /// The password used for "small group login" (view-only request list) | ||||
|     groupPassword       : string | ||||
|     /// The default e-mail type for this class | ||||
|     defaultEmailType    : string | ||||
|     defaultEmailType    : EmailFormat | ||||
|     /// Whether this class makes its request list public | ||||
|     isPublic            : bool | ||||
|     /// The time zone which this class uses (use tzdata names) | ||||
| @ -255,7 +344,7 @@ and [<CLIMutable; NoComparison; NoEquality>] ListPreferences = | ||||
|         textFontSize        = 12 | ||||
|         requestSort         = SortByDate | ||||
|         groupPassword       = "" | ||||
|         defaultEmailType    = EmailType.Html | ||||
|         defaultEmailType    = HtmlFormat | ||||
|         isPublic            = false | ||||
|         timeZoneId          = "America/Denver" | ||||
|         timeZone            = TimeZone.empty | ||||
| @ -333,7 +422,7 @@ and [<CLIMutable; NoComparison; NoEquality>] ListPreferences = | ||||
|           m.Property(fun e -> e.defaultEmailType) | ||||
|             .HasColumnName("DefaultEmailType") | ||||
|             .IsRequired() | ||||
|             .HasDefaultValue EmailType.Html | ||||
|             .HasDefaultValue HtmlFormat | ||||
|           |> ignore | ||||
|           m.Property(fun e -> e.isPublic) | ||||
|             .HasColumnName("IsPublic") | ||||
| @ -359,6 +448,8 @@ and [<CLIMutable; NoComparison; NoEquality>] ListPreferences = | ||||
|       |> ignore | ||||
|       mb.Model.FindEntityType(typeof<ListPreferences>).FindProperty("requestSort") | ||||
|         .SetValueConverter(Converters.RequestSortConverter ()) | ||||
|       mb.Model.FindEntityType(typeof<ListPreferences>).FindProperty("defaultEmailType") | ||||
|         .SetValueConverter(Converters.EmailFormatConverter ()) | ||||
|       mb.Model.FindEntityType(typeof<ListPreferences>).FindProperty("asOfDateDisplay") | ||||
|         .SetValueConverter(Converters.AsOfDateDisplayConverter ()) | ||||
| 
 | ||||
| @ -374,7 +465,7 @@ and [<CLIMutable; NoComparison; NoEquality>] Member = | ||||
|     /// The e-mail address for the member | ||||
|     email        : string | ||||
|     /// The type of e-mail preferred by this member (see <see cref="EmailTypes"/> constants) | ||||
|     format       : string option | ||||
|     format       : string option // TODO - do I need a custom formatter for this? | ||||
|     /// The small group to which this member belongs | ||||
|     smallGroup   : SmallGroup | ||||
|     } | ||||
| @ -405,64 +496,62 @@ and [<CLIMutable; NoComparison; NoEquality>] Member = | ||||
| /// This represents a single prayer request | ||||
| and [<CLIMutable; NoComparison; NoEquality>] PrayerRequest = | ||||
|   { /// The Id of this request | ||||
|     prayerRequestId   : PrayerRequestId | ||||
|     prayerRequestId : PrayerRequestId | ||||
|     /// The type of the request | ||||
|     requestType       : string | ||||
|     requestType     : PrayerRequestType | ||||
|     /// The user who entered the request | ||||
|     userId            : UserId | ||||
|     userId          : UserId | ||||
|     /// The small group to which this request belongs | ||||
|     smallGroupId      : SmallGroupId | ||||
|     smallGroupId    : SmallGroupId | ||||
|     /// The date/time on which this request was entered | ||||
|     enteredDate       : DateTime | ||||
|     enteredDate     : DateTime | ||||
|     /// The date/time this request was last updated | ||||
|     updatedDate       : DateTime | ||||
|     updatedDate     : DateTime | ||||
|     /// The name of the requestor or subject, or title of announcement | ||||
|     requestor         : string option | ||||
|     requestor       : string option | ||||
|     /// The text of the request | ||||
|     text              : string | ||||
|     /// Whether this request is exempt from standard expiration rules | ||||
|     doNotExpire       : bool | ||||
|     text            : string | ||||
|     /// Whether the chaplain should be notified for this request | ||||
|     notifyChaplain    : bool | ||||
|     /// Whether this request has been expired manually | ||||
|     isManuallyExpired : bool | ||||
|     notifyChaplain  : bool | ||||
|     /// The user who entered this request | ||||
|     user              : User | ||||
|     user            : User | ||||
|     /// The small group to which this request belongs | ||||
|     smallGroup        : SmallGroup | ||||
|     smallGroup      : SmallGroup | ||||
|     /// Is this request expired? | ||||
|     expiration      : Expiration | ||||
|     } | ||||
|   with | ||||
|     /// An empty request | ||||
|     static member empty = | ||||
|       { prayerRequestId   = Guid.Empty | ||||
|         requestType       = RequestType.Current | ||||
|         userId            = Guid.Empty | ||||
|         smallGroupId      = Guid.Empty | ||||
|         enteredDate       = DateTime.MinValue | ||||
|         updatedDate       = DateTime.MinValue | ||||
|         requestor         = None | ||||
|         text              = ""  | ||||
|         doNotExpire       = false | ||||
|         notifyChaplain    = false | ||||
|         isManuallyExpired = false | ||||
|         user              = User.empty | ||||
|         smallGroup        = SmallGroup.empty | ||||
|       { prayerRequestId = Guid.Empty | ||||
|         requestType     = CurrentRequest | ||||
|         userId          = Guid.Empty | ||||
|         smallGroupId    = Guid.Empty | ||||
|         enteredDate     = DateTime.MinValue | ||||
|         updatedDate     = DateTime.MinValue | ||||
|         requestor       = None | ||||
|         text            = ""  | ||||
|         notifyChaplain  = false | ||||
|         user            = User.empty | ||||
|         smallGroup      = SmallGroup.empty | ||||
|         expiration      = Automatic | ||||
|         } | ||||
|     /// Is this request expired? | ||||
|     member this.isExpired (curr : DateTime) expDays = | ||||
|       match this.isManuallyExpired with | ||||
|       | true -> true // Manual expiration | ||||
|       | false -> | ||||
|           let nonExpiringTypes = [ RequestType.Recurring; RequestType.Expecting ] | ||||
|           match this.doNotExpire || List.contains this.requestType nonExpiringTypes with | ||||
|           | true -> false // No expiration | ||||
|           | false -> curr.AddDays(-(float expDays)) > this.updatedDate // Automatic expiration | ||||
|       match this.expiration with | ||||
|       | Forced -> true | ||||
|       | Manual -> false  | ||||
|       | Automatic -> | ||||
|           match this.requestType with | ||||
|           | LongTermRequest | ||||
|           | Expecting -> false | ||||
|           | _ -> curr.AddDays(-(float expDays)) > this.updatedDate // Automatic expiration | ||||
| 
 | ||||
|     /// Is an update required for this long-term request? | ||||
|     member this.updateRequired curr expDays updWeeks = | ||||
|       match this.isExpired curr expDays with | ||||
|       | true -> false | ||||
|       | _ -> curr.AddDays(-(float (updWeeks * 7))) > this.updatedDate | ||||
|       | false -> curr.AddDays(-(float (updWeeks * 7))) > this.updatedDate | ||||
| 
 | ||||
|     /// Configure EF for this entity | ||||
|     static member internal configureEF (mb : ModelBuilder) = | ||||
| @ -477,12 +566,15 @@ and [<CLIMutable; NoComparison; NoEquality>] PrayerRequest = | ||||
|           m.Property(fun e -> e.updatedDate).HasColumnName "UpdatedDate" |> ignore | ||||
|           m.Property(fun e -> e.requestor).HasColumnName "Requestor" |> ignore | ||||
|           m.Property(fun e -> e.text).HasColumnName("Text").IsRequired() |> ignore | ||||
|           m.Property(fun e -> e.doNotExpire).HasColumnName "DoNotExpire" |> ignore | ||||
|           m.Property(fun e -> e.notifyChaplain).HasColumnName "NotifyChaplain" |> ignore | ||||
|           m.Property(fun e -> e.isManuallyExpired).HasColumnName "IsManuallyExpired" |> ignore) | ||||
|           m.Property(fun e -> e.expiration).HasColumnName "Expiration" |> ignore) | ||||
|       |> ignore | ||||
|       mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty("requestType") | ||||
|         .SetValueConverter(Converters.PrayerRequestTypeConverter ()) | ||||
|       mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty("requestor") | ||||
|         .SetValueConverter(OptionConverter<string> ()) | ||||
|       mb.Model.FindEntityType(typeof<PrayerRequest>).FindProperty("expiration") | ||||
|         .SetValueConverter(Converters.ExpirationConverter ()) | ||||
| 
 | ||||
| 
 | ||||
| /// This represents a small group (Sunday School class, Bible study group, etc.) | ||||
|  | ||||
| @ -50,17 +50,16 @@ type MemberTable = | ||||
|     } | ||||
| 
 | ||||
| type PrayerRequestTable = | ||||
|   { prayerRequestId   : OperationBuilder<AddColumnOperation> | ||||
|     doNotExpire       : OperationBuilder<AddColumnOperation> | ||||
|     enteredDate       : OperationBuilder<AddColumnOperation> | ||||
|     isManuallyExpired : OperationBuilder<AddColumnOperation> | ||||
|     notifyChaplain    : OperationBuilder<AddColumnOperation> | ||||
|     requestType       : OperationBuilder<AddColumnOperation> | ||||
|     requestor         : OperationBuilder<AddColumnOperation> | ||||
|     smallGroupId      : OperationBuilder<AddColumnOperation> | ||||
|     text              : OperationBuilder<AddColumnOperation> | ||||
|     updatedDate       : OperationBuilder<AddColumnOperation> | ||||
|     userId            : OperationBuilder<AddColumnOperation> | ||||
|   { prayerRequestId : OperationBuilder<AddColumnOperation> | ||||
|     enteredDate     : OperationBuilder<AddColumnOperation> | ||||
|     expiration      : OperationBuilder<AddColumnOperation> | ||||
|     notifyChaplain  : OperationBuilder<AddColumnOperation> | ||||
|     requestType     : OperationBuilder<AddColumnOperation> | ||||
|     requestor       : OperationBuilder<AddColumnOperation> | ||||
|     smallGroupId    : OperationBuilder<AddColumnOperation> | ||||
|     text            : OperationBuilder<AddColumnOperation> | ||||
|     updatedDate     : OperationBuilder<AddColumnOperation> | ||||
|     userId          : OperationBuilder<AddColumnOperation> | ||||
|     } | ||||
| 
 | ||||
| type SmallGroupTable = | ||||
| @ -104,12 +103,12 @@ type InitialDatabase () = | ||||
|       schema  = "pt", | ||||
|       columns = | ||||
|         (fun table -> | ||||
|             { churchId         = table.Column<Guid>   (name = "ChurchId", nullable = false) | ||||
|               city             = table.Column<string> (name = "City", nullable = false) | ||||
|             { churchId         = table.Column<Guid>   (name = "ChurchId",         nullable = false) | ||||
|               city             = table.Column<string> (name = "City",             nullable = false) | ||||
|               hasInterface     = table.Column<bool>   (name = "HasVirtualPrayerRoomInterface", nullable = false) | ||||
|               interfaceAddress = table.Column<string> (name = "InterfaceAddress", nullable = true) | ||||
|               name             = table.Column<string> (name = "Name", nullable = false) | ||||
|               st               = table.Column<string> (name = "ST", maxLength = Nullable<int> 2, nullable = false) | ||||
|               name             = table.Column<string> (name = "Name",             nullable = false) | ||||
|               st               = table.Column<string> (name = "ST",               nullable = false, maxLength = Nullable<int> 2) | ||||
|               }), | ||||
|       constraints = | ||||
|         fun table -> | ||||
| @ -121,10 +120,10 @@ type InitialDatabase () = | ||||
|       schema  = "pt", | ||||
|       columns = | ||||
|         (fun table -> | ||||
|             { timeZoneId  = table.Column<string> (name = "TimeZoneId", nullable = false) | ||||
|             { timeZoneId  = table.Column<string> (name = "TimeZoneId",  nullable = false) | ||||
|               description = table.Column<string> (name = "Description", nullable = false) | ||||
|               isActive    = table.Column<bool>   (name = "IsActive", nullable = false) | ||||
|               sortOrder   = table.Column<int>    (name = "SortOrder", nullable = false) | ||||
|               isActive    = table.Column<bool>   (name = "IsActive",    nullable = false) | ||||
|               sortOrder   = table.Column<int>    (name = "SortOrder",   nullable = false) | ||||
|               }), | ||||
|       constraints = | ||||
|         fun table -> | ||||
| @ -136,13 +135,13 @@ type InitialDatabase () = | ||||
|       schema  = "pt", | ||||
|       columns = | ||||
|         (fun table -> | ||||
|             { userId       = table.Column<Guid>   (name = "UserId", nullable = false) | ||||
|               emailAddress = table.Column<string> (name = "EmailAddress", nullable = false) | ||||
|               firstName    = table.Column<string> (name = "FirstName", nullable = false) | ||||
|             { userId       = table.Column<Guid>   (name = "UserId",        nullable = false) | ||||
|               emailAddress = table.Column<string> (name = "EmailAddress",  nullable = false) | ||||
|               firstName    = table.Column<string> (name = "FirstName",     nullable = false) | ||||
|               isAdmin      = table.Column<bool>   (name = "IsSystemAdmin", nullable = false) | ||||
|               lastName     = table.Column<string> (name = "LastName", nullable = false) | ||||
|               passwordHash = table.Column<string> (name = "PasswordHash", nullable = false) | ||||
|               salt         = table.Column<Guid>   (name = "Salt", nullable = true) | ||||
|               lastName     = table.Column<string> (name = "LastName",      nullable = false) | ||||
|               passwordHash = table.Column<string> (name = "PasswordHash",  nullable = false) | ||||
|               salt         = table.Column<Guid>   (name = "Salt",          nullable = true) | ||||
|               }), | ||||
|       constraints = | ||||
|         fun table -> | ||||
| @ -155,8 +154,8 @@ type InitialDatabase () = | ||||
|       columns = | ||||
|         (fun table -> | ||||
|             { smallGroupId = table.Column<Guid>   (name = "SmallGroupId", nullable = false) | ||||
|               churchId     = table.Column<Guid>   (name = "ChurchId", nullable = false) | ||||
|               name         = table.Column<string> (name = "Name", nullable = false) | ||||
|               churchId     = table.Column<Guid>   (name = "ChurchId",     nullable = false) | ||||
|               name         = table.Column<string> (name = "Name",         nullable = false) | ||||
|               }), | ||||
|       constraints = | ||||
|         fun table -> | ||||
| @ -221,10 +220,10 @@ type InitialDatabase () = | ||||
|       schema  = "pt", | ||||
|       columns = | ||||
|         (fun table -> | ||||
|             { memberId     = table.Column<Guid>   (name = "MemberId", nullable = false) | ||||
|               email        = table.Column<string> (name = "Email", nullable = false) | ||||
|               format       = table.Column<string> (name = "Format", nullable = true) | ||||
|               memberName   = table.Column<string> (name = "MemberName", nullable = false) | ||||
|             { memberId     = table.Column<Guid>   (name = "MemberId",     nullable = false) | ||||
|               email        = table.Column<string> (name = "Email",        nullable = false) | ||||
|               format       = table.Column<string> (name = "Format",       nullable = true) | ||||
|               memberName   = table.Column<string> (name = "MemberName",   nullable = false) | ||||
|               smallGroupId = table.Column<Guid>   (name = "SmallGroupId", nullable = false) | ||||
|               }), | ||||
|       constraints = | ||||
| @ -246,16 +245,15 @@ type InitialDatabase () = | ||||
|       columns = | ||||
|         (fun table -> | ||||
|             { prayerRequestId   = table.Column<Guid>     (name = "PrayerRequestId", nullable = false) | ||||
|               doNotExpire       = table.Column<bool>     (name = "DoNotExpire", nullable = false) | ||||
|               enteredDate       = table.Column<DateTime> (name = "EnteredDate", nullable = false) | ||||
|               isManuallyExpired = table.Column<bool>     (name = "IsManuallyExpired", nullable = false) | ||||
|               notifyChaplain    = table.Column<bool>     (name = "NotifyChaplain", nullable = false) | ||||
|               requestType       = table.Column<string>   (name = "RequestType", nullable = false) | ||||
|               requestor         = table.Column<string>   (name = "Requestor", nullable = true) | ||||
|               smallGroupId      = table.Column<Guid>     (name = "SmallGroupId", nullable = false) | ||||
|               text              = table.Column<string>   (name = "Text", nullable = false) | ||||
|               updatedDate       = table.Column<DateTime> (name = "UpdatedDate", nullable = false) | ||||
|               userId            = table.Column<Guid>     (name = "UserId", nullable = false) | ||||
|               expiration        = table.Column<bool>     (name = "Expiration",      nullable = false) | ||||
|               enteredDate       = table.Column<DateTime> (name = "EnteredDate",     nullable = false) | ||||
|               notifyChaplain    = table.Column<bool>     (name = "NotifyChaplain",  nullable = false) | ||||
|               requestType       = table.Column<string>   (name = "RequestType",     nullable = false) | ||||
|               requestor         = table.Column<string>   (name = "Requestor",       nullable = true) | ||||
|               smallGroupId      = table.Column<Guid>     (name = "SmallGroupId",    nullable = false) | ||||
|               text              = table.Column<string>   (name = "Text",            nullable = false) | ||||
|               updatedDate       = table.Column<DateTime> (name = "UpdatedDate",     nullable = false) | ||||
|               userId            = table.Column<Guid>     (name = "UserId",          nullable = false) | ||||
|               }), | ||||
|       constraints = | ||||
|         fun table -> | ||||
| @ -283,7 +281,7 @@ type InitialDatabase () = | ||||
|       schema  = "pt", | ||||
|       columns = | ||||
|         (fun table -> | ||||
|             { userId       = table.Column<Guid> (name = "UserId", nullable = false) | ||||
|             { userId       = table.Column<Guid> (name = "UserId",       nullable = false) | ||||
|               smallGroupId = table.Column<Guid> (name = "SmallGroupId", nullable = false) | ||||
|               }), | ||||
|       constraints = | ||||
| @ -351,7 +349,7 @@ type InitialDatabase () = | ||||
|           b.Property<Guid>("smallGroupId") |> ignore | ||||
|           b.Property<int>("daysToExpire").ValueGeneratedOnAdd().HasDefaultValue(14) |> ignore | ||||
|           b.Property<int>("daysToKeepNew").ValueGeneratedOnAdd().HasDefaultValue(7) |> ignore | ||||
|           b.Property<string>("defaultEmailType").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("Html") |> ignore | ||||
|           b.Property<string>("defaultEmailType").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("H") |> ignore | ||||
|           b.Property<string>("emailFromAddress").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("prayer@djs-consulting.com") |> ignore | ||||
|           b.Property<string>("emailFromName").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("PrayerTracker") |> ignore | ||||
|           b.Property<string>("groupPassword").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("") |> ignore | ||||
| @ -388,11 +386,10 @@ type InitialDatabase () = | ||||
|       typeof<PrayerRequest>, | ||||
|       fun b -> | ||||
|           b.Property<Guid>("prayerRequestId").ValueGeneratedOnAdd() |> ignore | ||||
|           b.Property<bool>("doNotExpire") |> ignore | ||||
|           b.Property<DateTime>("enteredDate") |> ignore | ||||
|           b.Property<bool>("isManuallyExpired") |> ignore | ||||
|           b.Property<DateTime>("enteredDate").IsRequired() |> ignore | ||||
|           b.Property<string>("expiration").IsRequired().HasMaxLength 1 |> ignore | ||||
|           b.Property<bool>("notifyChaplain") |> ignore | ||||
|           b.Property<string>("requestType").IsRequired() |> ignore | ||||
|           b.Property<string>("requestType").IsRequired().HasMaxLength 1 |> ignore | ||||
|           b.Property<string>("requestor") |> ignore | ||||
|           b.Property<Guid>("smallGroupId") |> ignore | ||||
|           b.Property<string>("text").IsRequired() |> ignore | ||||
|  | ||||
| @ -36,7 +36,7 @@ type AppDbContextModelSnapshot () = | ||||
|           b.Property<Guid>("smallGroupId") |> ignore | ||||
|           b.Property<int>("daysToExpire").ValueGeneratedOnAdd().HasDefaultValue(14) |> ignore | ||||
|           b.Property<int>("daysToKeepNew").ValueGeneratedOnAdd().HasDefaultValue(7) |> ignore | ||||
|           b.Property<string>("defaultEmailType").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("Html") |> ignore | ||||
|           b.Property<string>("defaultEmailType").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("H").HasMaxLength(1) |> ignore | ||||
|           b.Property<string>("emailFromAddress").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("prayer@djs-consulting.com") |> ignore | ||||
|           b.Property<string>("emailFromName").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("PrayerTracker") |> ignore | ||||
|           b.Property<string>("groupPassword").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("") |> ignore | ||||
| @ -73,11 +73,10 @@ type AppDbContextModelSnapshot () = | ||||
|       typeof<PrayerRequest>, | ||||
|       fun b -> | ||||
|           b.Property<Guid>("prayerRequestId").ValueGeneratedOnAdd() |> ignore | ||||
|           b.Property<bool>("doNotExpire") |> ignore | ||||
|           b.Property<DateTime>("enteredDate") |> ignore | ||||
|           b.Property<bool>("isManuallyExpired") |> ignore | ||||
|           b.Property<string>("expiration").IsRequired().HasMaxLength(1) |> ignore | ||||
|           b.Property<bool>("notifyChaplain") |> ignore | ||||
|           b.Property<string>("requestType").IsRequired() |> ignore | ||||
|           b.Property<string>("requestType").IsRequired().HasMaxLength(1) |> ignore | ||||
|           b.Property<string>("requestor") |> ignore | ||||
|           b.Property<Guid>("smallGroupId") |> ignore | ||||
|           b.Property<string>("text").IsRequired() |> ignore | ||||
|  | ||||
| @ -48,6 +48,52 @@ let churchTests = | ||||
|       } | ||||
|     ] | ||||
| 
 | ||||
| [<Tests>] | ||||
| let emailFormatTests = | ||||
|   testList "EmailFormat" [ | ||||
|     test "HtmlFormat code is correct" { | ||||
|       Expect.equal HtmlFormat.code "H" "The code for HtmlFormat should have been \"H\"" | ||||
|       } | ||||
|     test "PlainTextFormat code is correct" { | ||||
|       Expect.equal PlainTextFormat.code "P" "The code for PlainTextFormat should have been \"P\"" | ||||
|       } | ||||
|     test "fromCode H should return HtmlFormat" { | ||||
|       Expect.equal (EmailFormat.fromCode "H") HtmlFormat "\"H\" should have been converted to HtmlFormat" | ||||
|       } | ||||
|     test "fromCode P should return ShortDate" { | ||||
|       Expect.equal (EmailFormat.fromCode "P") PlainTextFormat "\"P\" should have been converted to PlainTextFormat" | ||||
|       } | ||||
|     test "fromCode Z should raise" { | ||||
|       Expect.throws (fun () -> EmailFormat.fromCode "Z" |> ignore) "An unknown code should have raised an exception" | ||||
|       } | ||||
|     ] | ||||
| 
 | ||||
| [<Tests>] | ||||
| let expirationTests = | ||||
|   testList "Expiration" [ | ||||
|     test "Automatic code is correct" { | ||||
|       Expect.equal Automatic.code "A" "The code for Automatic should have been \"A\"" | ||||
|       } | ||||
|     test "Manual code is correct" { | ||||
|       Expect.equal Manual.code "M" "The code for Manual should have been \"M\"" | ||||
|       } | ||||
|     test "Forced code is correct" { | ||||
|       Expect.equal Forced.code "F" "The code for Forced should have been \"F\"" | ||||
|       } | ||||
|     test "fromCode A should return Automatic" { | ||||
|       Expect.equal (Expiration.fromCode "A") Automatic "\"A\" should have been converted to Automatic" | ||||
|       } | ||||
|     test "fromCode M should return Manual" { | ||||
|       Expect.equal (Expiration.fromCode "M") Manual "\"M\" should have been converted to Manual" | ||||
|       } | ||||
|     test "fromCode F should return Forced" { | ||||
|       Expect.equal (Expiration.fromCode "F") Forced "\"F\" should have been converted to Forced" | ||||
|       } | ||||
|     test "fromCode V should raise" { | ||||
|       Expect.throws (fun () -> Expiration.fromCode "V" |> ignore) "An unknown code should have raised an exception" | ||||
|       } | ||||
|     ] | ||||
| 
 | ||||
| [<Tests>] | ||||
| let listPreferencesTests = | ||||
|   testList "ListPreferences" [ | ||||
| @ -67,7 +113,7 @@ let listPreferencesTests = | ||||
|       Expect.equal mt.textFontSize 12 "The default text font size should have been 12" | ||||
|       Expect.equal mt.requestSort SortByDate "The default request sort should have been by date" | ||||
|       Expect.equal mt.groupPassword "" "The default group password should have been blank" | ||||
|       Expect.equal mt.defaultEmailType EmailType.Html "The default e-mail type should have been HTML" | ||||
|       Expect.equal mt.defaultEmailType HtmlFormat "The default e-mail type should have been HTML" | ||||
|       Expect.isFalse mt.isPublic "The isPublic flag should not have been set" | ||||
|       Expect.equal mt.timeZoneId "America/Denver" "The default time zone should have been America/Denver" | ||||
|       Expect.equal mt.timeZone.timeZoneId "" "The default preferences should have included an empty time zone" | ||||
| @ -96,34 +142,33 @@ let prayerRequestTests = | ||||
|     test "empty is as expected" { | ||||
|       let mt = PrayerRequest.empty | ||||
|       Expect.equal mt.prayerRequestId Guid.Empty "The request ID should have been an empty GUID" | ||||
|       Expect.equal mt.requestType RequestType.Current "The request type should have been Current" | ||||
|       Expect.equal mt.requestType CurrentRequest "The request type should have been Current" | ||||
|       Expect.equal mt.userId Guid.Empty "The user ID should have been an empty GUID" | ||||
|       Expect.equal mt.smallGroupId Guid.Empty "The small group ID should have been an empty GUID" | ||||
|       Expect.equal mt.enteredDate DateTime.MinValue "The entered date should have been the minimum" | ||||
|       Expect.equal mt.updatedDate DateTime.MinValue "The updated date should have been the minimum" | ||||
|       Expect.isNone mt.requestor "The requestor should not exist" | ||||
|       Expect.equal mt.text "" "The request text should have been blank" | ||||
|       Expect.isFalse mt.doNotExpire "The do not expire flag should not have been set" | ||||
|       Expect.isFalse mt.notifyChaplain "The notify chaplain flag should not have been set" | ||||
|       Expect.isFalse mt.isManuallyExpired "The is manually expired flag should not have been set" | ||||
|       Expect.equal mt.expiration Automatic "The expiration should have been Automatic" | ||||
|       Expect.equal mt.user.userId Guid.Empty "The user should have been an empty one" | ||||
|       Expect.equal mt.smallGroup.smallGroupId Guid.Empty "The small group should have been an empty one" | ||||
|       } | ||||
|     test "isExpired always returns false for expecting requests" { | ||||
|       let req = { PrayerRequest.empty with requestType = RequestType.Expecting } | ||||
|       let req = { PrayerRequest.empty with requestType = Expecting } | ||||
|       Expect.isFalse (req.isExpired DateTime.Now 0) "An expecting request should never be considered expired" | ||||
|       } | ||||
|     test "isExpired always returns false for never-expired requests" { | ||||
|       let req = { PrayerRequest.empty with updatedDate = DateTime.Now.AddMonths -1; doNotExpire = true } | ||||
|     test "isExpired always returns false for manually-expired requests" { | ||||
|       let req = { PrayerRequest.empty with updatedDate = DateTime.Now.AddMonths -1; expiration = Manual } | ||||
|       Expect.isFalse (req.isExpired DateTime.Now 4) "A never-expired request should never be considered expired" | ||||
|       } | ||||
|     test "isExpired always returns false for recurring requests" { | ||||
|       let req = { PrayerRequest.empty with requestType = RequestType.Recurring } | ||||
|     test "isExpired always returns false for long term/recurring requests" { | ||||
|       let req = { PrayerRequest.empty with requestType = LongTermRequest } | ||||
|       Expect.isFalse (req.isExpired DateTime.Now 0) "A recurring/long-term request should never be considered expired" | ||||
|       } | ||||
|     test "isExpired always returns true for manually expired requests" { | ||||
|       let req = { PrayerRequest.empty with updatedDate = DateTime.Now; isManuallyExpired = true } | ||||
|       Expect.isTrue (req.isExpired DateTime.Now 5) "A manually expired request should always be considered expired" | ||||
|     test "isExpired always returns true for force-expired requests" { | ||||
|       let req = { PrayerRequest.empty with updatedDate = DateTime.Now; expiration = Forced } | ||||
|       Expect.isTrue (req.isExpired DateTime.Now 5) "A force-expired request should always be considered expired" | ||||
|       } | ||||
|     test "isExpired returns false for non-expired requests" { | ||||
|       let req = { PrayerRequest.empty with updatedDate = DateTime.Now.AddDays -5. } | ||||
| @ -134,13 +179,13 @@ let prayerRequestTests = | ||||
|       Expect.isTrue (req.isExpired DateTime.Now 7) "A request updated 8 days ago should be considered expired" | ||||
|       } | ||||
|     test "updateRequired returns false for expired requests" { | ||||
|       let req = { PrayerRequest.empty with isManuallyExpired = true } | ||||
|       let req = { PrayerRequest.empty with expiration = Forced } | ||||
|       Expect.isFalse (req.updateRequired DateTime.Now 7 4) "An expired request should not require an update" | ||||
|       } | ||||
|     test "updateRequired returns false when an update is not required for an active request" { | ||||
|       let req = | ||||
|         { PrayerRequest.empty with | ||||
|             requestType = RequestType.Recurring | ||||
|             requestType = LongTermRequest | ||||
|             updatedDate = DateTime.Now.AddDays -14. | ||||
|           } | ||||
|       Expect.isFalse (req.updateRequired DateTime.Now 7 4) | ||||
| @ -149,7 +194,7 @@ let prayerRequestTests = | ||||
|     test "updateRequired returns true when an update is required for an active request" { | ||||
|       let req = | ||||
|         { PrayerRequest.empty with | ||||
|             requestType = RequestType.Recurring | ||||
|             requestType = LongTermRequest | ||||
|             updatedDate = DateTime.Now.AddDays -34. | ||||
|           } | ||||
|       Expect.isTrue (req.updateRequired DateTime.Now 7 4) | ||||
| @ -157,6 +202,47 @@ let prayerRequestTests = | ||||
|       } | ||||
|     ] | ||||
| 
 | ||||
| [<Tests>] | ||||
| let prayerRequestTypeTests = | ||||
|   testList "PrayerRequestType" [ | ||||
|     test "CurrentRequest code is correct" { | ||||
|       Expect.equal CurrentRequest.code "C" "The code for CurrentRequest should have been \"C\"" | ||||
|       } | ||||
|     test "LongTermRequest code is correct" { | ||||
|       Expect.equal LongTermRequest.code "L" "The code for LongTermRequest should have been \"L\"" | ||||
|       } | ||||
|     test "PraiseReport code is correct" { | ||||
|       Expect.equal PraiseReport.code "P" "The code for PraiseReport should have been \"P\"" | ||||
|       } | ||||
|     test "Expecting code is correct" { | ||||
|       Expect.equal Expecting.code "E" "The code for Expecting should have been \"E\"" | ||||
|       } | ||||
|     test "Announcement code is correct" { | ||||
|       Expect.equal Announcement.code "A" "The code for Announcement should have been \"A\"" | ||||
|       } | ||||
|     test "fromCode C should return CurrentRequest" { | ||||
|       Expect.equal (PrayerRequestType.fromCode "C") CurrentRequest | ||||
|         "\"C\" should have been converted to CurrentRequest" | ||||
|       } | ||||
|     test "fromCode L should return LongTermRequest" { | ||||
|       Expect.equal (PrayerRequestType.fromCode "L") LongTermRequest | ||||
|         "\"L\" should have been converted to LongTermRequest" | ||||
|       } | ||||
|     test "fromCode P should return PraiseReport" { | ||||
|       Expect.equal (PrayerRequestType.fromCode "P") PraiseReport "\"P\" should have been converted to PraiseReport" | ||||
|       } | ||||
|     test "fromCode E should return Expecting" { | ||||
|       Expect.equal (PrayerRequestType.fromCode "E") Expecting "\"E\" should have been converted to Expecting" | ||||
|       } | ||||
|     test "fromCode A should return Announcement" { | ||||
|       Expect.equal (PrayerRequestType.fromCode "A") Announcement "\"A\" should have been converted to Announcement" | ||||
|       } | ||||
|     test "fromCode R should raise" { | ||||
|       Expect.throws (fun () -> PrayerRequestType.fromCode "R" |> ignore) | ||||
|         "An unknown code should have raised an exception" | ||||
|       } | ||||
|     ] | ||||
| 
 | ||||
| [<Tests>] | ||||
| let requestSortTests = | ||||
|   testList "RequestSort" [ | ||||
|  | ||||
| @ -31,15 +31,15 @@ module ReferenceListTests = | ||||
|   let emailTypeListTests = | ||||
|     testList "ReferenceList.emailTypeList" [ | ||||
|       test "includes default type" { | ||||
|         let typs = ReferenceList.emailTypeList EmailType.Html _s | ||||
|         let typs = ReferenceList.emailTypeList HtmlFormat _s | ||||
|         Expect.hasCountOf typs 3u countAll "There should have been 3 e-mail type options returned" | ||||
|         let top = Seq.head typs | ||||
|         Expect.equal (fst top) "" "The default option should have been blank" | ||||
|         Expect.equal (snd top).Value "Group Default (HTML Format)" "The default option label was incorrect" | ||||
|         let nxt = typs |> Seq.skip 1 |> Seq.head | ||||
|         Expect.equal (fst nxt) EmailType.Html "The 2nd option should have been HTML" | ||||
|         Expect.equal (fst nxt) HtmlFormat.code "The 2nd option should have been HTML" | ||||
|         let lst = typs |> Seq.last | ||||
|         Expect.equal (fst lst) EmailType.PlainText "The 3rd option should have been plain text" | ||||
|         Expect.equal (fst lst) PlainTextFormat.code "The 3rd option should have been plain text" | ||||
|         } | ||||
|       ] | ||||
|    | ||||
| @ -49,15 +49,15 @@ module ReferenceListTests = | ||||
|       test "excludes immediate expiration if not required" { | ||||
|         let exps = ReferenceList.expirationList _s false | ||||
|         Expect.hasCountOf exps 2u countAll "There should have been 2 expiration types returned" | ||||
|         Expect.exists exps (fun exp -> fst exp = "N") "The option for normal expiration was not found" | ||||
|         Expect.exists exps (fun exp -> fst exp = "Y") "The option for \"never expire\" was not found" | ||||
|         Expect.exists exps (fun (exp, _) -> exp = Automatic.code) "The option for automatic expiration was not found" | ||||
|         Expect.exists exps (fun (exp, _) -> exp = Manual.code) "The option for manual expiration was not found" | ||||
|         } | ||||
|       test "includes immediate expiration if required" { | ||||
|         let exps = ReferenceList.expirationList _s true | ||||
|         Expect.hasCountOf exps 3u countAll "There should have been 3 expiration types returned" | ||||
|         Expect.exists exps (fun exp -> fst exp = "N") "The option for normal expiration was not found" | ||||
|         Expect.exists exps (fun exp -> fst exp = "Y") "The option for \"never expire\" was not found" | ||||
|         Expect.exists exps (fun exp -> fst exp = "X") "The option for \"expire immediately\" was not found" | ||||
|         Expect.exists exps (fun (exp, _) -> exp = Automatic.code) "The option for automatic expiration was not found" | ||||
|         Expect.exists exps (fun (exp, _) -> exp = Manual.code) "The option for manual expiration was not found" | ||||
|         Expect.exists exps (fun (exp, _) -> exp = Forced.code) "The option for immediate expiration was not found" | ||||
|         } | ||||
|       ] | ||||
|    | ||||
| @ -69,14 +69,12 @@ module ReferenceListTests = | ||||
|       yield! testFixture withList [ | ||||
|         yield "returns 5 types", | ||||
|           fun typs -> Expect.hasCountOf typs 5u countAll "There should have been 5 request types returned" | ||||
|         yield! [ RequestType.Current; RequestType.Recurring; RequestType.Praise; RequestType.Expecting; | ||||
|                  RequestType.Announcement | ||||
|           ] | ||||
|         |> List.map (fun typ -> | ||||
|             sprintf "contains \"%s\"" typ, | ||||
|               fun typs -> | ||||
|                   Expect.isSome (typs |> List.tryFind (fun x -> fst x = typ)) | ||||
|                     (sprintf "The \"%s\" option was not found" typ)) | ||||
|         yield! [ CurrentRequest; LongTermRequest; PraiseReport; Expecting; Announcement ] | ||||
|           |> List.map (fun typ -> | ||||
|               sprintf "contains \"%O\"" typ, | ||||
|                 fun typs -> | ||||
|                     Expect.isSome (typs |> List.tryFind (fun x -> fst x = typ)) | ||||
|                       (sprintf "The \"%O\" option was not found" typ)) | ||||
|         ] | ||||
|       ] | ||||
| 
 | ||||
| @ -232,8 +230,8 @@ let editMemberTests = | ||||
|       Expect.equal edit.emailType "" "The e-mail type should have been blank for group default" | ||||
|       } | ||||
|     test "fromMember populates with specific format" { | ||||
|       let edit = EditMember.fromMember { Member.empty with format = Some EmailType.Html } | ||||
|       Expect.equal edit.emailType EmailType.Html "The e-mail type was not filled correctly" | ||||
|       let edit = EditMember.fromMember { Member.empty with format = Some HtmlFormat.code } | ||||
|       Expect.equal edit.emailType HtmlFormat.code "The e-mail type was not filled correctly" | ||||
|       } | ||||
|     test "empty is as expected" { | ||||
|       let edit = EditMember.empty | ||||
| @ -263,7 +261,7 @@ let editPreferencesTests = | ||||
|       Expect.equal edit.requestSort prefs.requestSort.code "The request sort was not filled correctly" | ||||
|       Expect.equal edit.emailFromName prefs.emailFromName "The e-mail from name was not filled correctly" | ||||
|       Expect.equal edit.emailFromAddress prefs.emailFromAddress "The e-mail from address was not filled correctly" | ||||
|       Expect.equal edit.defaultEmailType prefs.defaultEmailType "The default e-mail type was not filled correctly" | ||||
|       Expect.equal edit.defaultEmailType prefs.defaultEmailType.code "The default e-mail type was not filled correctly" | ||||
|       Expect.equal edit.headingLineType "Name" "The heading line color type was not derived correctly" | ||||
|       Expect.equal edit.headingLineColor prefs.lineColor "The heading line color was not filled correctly" | ||||
|       Expect.equal edit.headingTextType "Name" "The heading text color type was not derived correctly" | ||||
| @ -303,39 +301,29 @@ let editRequestTests = | ||||
|     test "empty is as expected" { | ||||
|       let mt = EditRequest.empty | ||||
|       Expect.equal mt.requestId Guid.Empty "The request ID should be an empty GUID" | ||||
|       Expect.equal mt.requestType "" "The request type should have been blank" | ||||
|       Expect.equal mt.requestType CurrentRequest.code "The request type should have been \"Current\"" | ||||
|       Expect.isNone mt.enteredDate "The entered date should have been None" | ||||
|       Expect.isNone mt.skipDateUpdate "The \"skip date update\" flag should have been None" | ||||
|       Expect.isNone mt.requestor "The requestor should have been None" | ||||
|       Expect.equal mt.expiration "N" "The expiration should have been \"N\"" | ||||
|       Expect.equal mt.expiration Automatic.code "The expiration should have been \"A\" (Automatic)" | ||||
|       Expect.equal mt.text "" "The text should have been blank" | ||||
|       } | ||||
|     test "fromRequest succeeds when a request has the do-not-expire flag set" { | ||||
|     test "fromRequest succeeds" { | ||||
|       let req = | ||||
|         { PrayerRequest.empty with | ||||
|             prayerRequestId = Guid.NewGuid () | ||||
|             requestType     = RequestType.Current | ||||
|             requestType     = CurrentRequest | ||||
|             requestor       = Some "Me" | ||||
|             doNotExpire     = true | ||||
|             expiration      = Manual | ||||
|             text            = "the text" | ||||
|           } | ||||
|       let edit = EditRequest.fromRequest req | ||||
|       Expect.equal edit.requestId req.prayerRequestId "The request ID was not filled correctly" | ||||
|       Expect.equal edit.requestType req.requestType "The request type was not filled correctly" | ||||
|       Expect.equal edit.requestType req.requestType.code "The request type was not filled correctly" | ||||
|       Expect.equal edit.requestor req.requestor "The requestor was not filled correctly" | ||||
|       Expect.equal edit.expiration "Y" "The expiration should have been \"Y\" since the do-not-expire flag was set" | ||||
|       Expect.equal edit.expiration Manual.code "The expiration was not filled correctly" | ||||
|       Expect.equal edit.text req.text "The text was not filled correctly" | ||||
|       } | ||||
|     test "fromRequest succeeds when a request has the do-not-expire flag unset" { | ||||
|       let req = | ||||
|         { PrayerRequest.empty with | ||||
|             requestor       = None | ||||
|             doNotExpire     = false | ||||
|           } | ||||
|       let edit = EditRequest.fromRequest req | ||||
|       Expect.equal edit.requestor req.requestor "The requestor was not filled correctly" | ||||
|       Expect.equal edit.expiration "N" "The expiration should have been \"N\" since the do-not-expire flag was not set" | ||||
|       } | ||||
|     test "isNew works for a new request" { | ||||
|       Expect.isTrue (EditRequest.empty.isNew ()) "An empty GUID should be flagged as a new request" | ||||
|       } | ||||
| @ -469,19 +457,19 @@ let requestListTests = | ||||
|     let withRequestList f () = | ||||
|       { requests   = [ | ||||
|           { PrayerRequest.empty with | ||||
|               requestType = RequestType.Current | ||||
|               requestType = CurrentRequest | ||||
|               requestor   = Some "Zeb" | ||||
|               text        = "zyx" | ||||
|               updatedDate = DateTime.Today | ||||
|             } | ||||
|           { PrayerRequest.empty with | ||||
|               requestType = RequestType.Current | ||||
|               requestType = CurrentRequest | ||||
|               requestor   = Some "Aaron" | ||||
|               text        = "abc" | ||||
|               updatedDate = DateTime.Today - TimeSpan.FromDays 9. | ||||
|             } | ||||
|           { PrayerRequest.empty with | ||||
|               requestType = RequestType.Praise | ||||
|               requestType = PraiseReport | ||||
|               text        = "nmo" | ||||
|               updatedDate = DateTime.Today | ||||
|             } | ||||
| @ -566,19 +554,19 @@ let requestListTests = | ||||
|           Expect.stringContains text "  + nmo\n \n" "Last request not found" | ||||
|       "isNew succeeds for both old and new requests", | ||||
|       fun reqList -> | ||||
|           let reqs = reqList.requestsInCategory RequestType.Current | ||||
|           let reqs = reqList.requestsInCategory CurrentRequest | ||||
|           Expect.hasCountOf reqs 2u countAll "There should have been two requests" | ||||
|           Expect.isTrue (reqList.isNew (List.head reqs)) "The first request should have been new" | ||||
|           Expect.isFalse (reqList.isNew (List.last reqs)) "The second request should not have been new" | ||||
|       "requestsInCategory succeeds when requests exist", | ||||
|       fun reqList -> | ||||
|           let reqs = reqList.requestsInCategory RequestType.Current | ||||
|           let reqs = reqList.requestsInCategory CurrentRequest | ||||
|           Expect.hasCountOf reqs 2u countAll "There should have been two requests" | ||||
|           let first = List.head reqs | ||||
|           Expect.equal first.text "zyx" "The requests should be sorted by updated date descending" | ||||
|       "requestsInCategory succeeds when requests do not exist", | ||||
|       fun reqList -> | ||||
|           Expect.isEmpty (reqList.requestsInCategory "ABC") "There should have been no category \"ABC\" requests" | ||||
|           Expect.isEmpty (reqList.requestsInCategory Announcement) "There should have been no \"Announcement\" requests" | ||||
|       "requestsInCategory succeeds and sorts by requestor", | ||||
|       fun reqList -> | ||||
|           let newList = | ||||
| @ -588,7 +576,7 @@ let requestListTests = | ||||
|                       preferences = { reqList.listGroup.preferences with requestSort = SortByRequestor } | ||||
|                     } | ||||
|               } | ||||
|           let reqs = newList.requestsInCategory RequestType.Current | ||||
|           let reqs = newList.requestsInCategory CurrentRequest | ||||
|           Expect.hasCountOf reqs 2u countAll "There should have been two requests" | ||||
|           let first = List.head reqs | ||||
|           Expect.equal first.text "abc" "The requests should be sorted by requestor" | ||||
|  | ||||
| @ -23,7 +23,7 @@ let edit (m : EditRequest) today ctx vi = | ||||
|           label [ _for "requestType" ] [ locStr s.["Request Type"] ] | ||||
|           ReferenceList.requestTypeList s | ||||
|           |> Seq.ofList | ||||
|           |> Seq.map (fun item -> fst item, (snd item).Value) | ||||
|           |> Seq.map (fun (typ, desc) -> typ.code, desc.Value) | ||||
|           |> selectList "requestType" m.requestType [ _required; _autofocus ] | ||||
|           ] | ||||
|         yield div [ _class "pt-field" ] [ | ||||
|  | ||||
| @ -45,7 +45,7 @@ let announcement isAdmin ctx vi = | ||||
|           label [ _for "requestType" ] [ locStr s.["Request Type"] ] | ||||
|           reqTypes | ||||
|           |> Seq.ofList | ||||
|           |> Seq.map (fun item -> fst item, (snd item).Value) | ||||
|           |> Seq.map (fun (typ, desc) -> typ.code, desc.Value) | ||||
|           |> selectList "requestType" "Announcement" [] | ||||
|           ] | ||||
|         ] | ||||
| @ -407,7 +407,9 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = | ||||
|             label [ _for "defaultEmailType" ] [ locStr s.["E-mail Format"] ] | ||||
|             seq { | ||||
|               yield "", selectDefault s.["Select"].Value | ||||
|               yield! ReferenceList.emailTypeList "" s |> Seq.skip 1 |> Seq.map (fun typ -> fst typ, (snd typ).Value) | ||||
|               yield! ReferenceList.emailTypeList HtmlFormat s | ||||
|                 |> Seq.skip 1 | ||||
|                 |> Seq.map (fun typ -> fst typ, (snd typ).Value) | ||||
|               } | ||||
|             |> selectList "defaultEmailType" m.defaultEmailType [ _required ] | ||||
|             ] | ||||
|  | ||||
| @ -22,30 +22,28 @@ module ReferenceList = | ||||
|     // Localize the default type | ||||
|     let defaultType = | ||||
|       match def with | ||||
|       | EmailType.Html -> s.["HTML Format"].Value | ||||
|       | EmailType.PlainText -> s.["Plain-Text Format"].Value | ||||
|       | EmailType.AttachedPdf -> s.["Attached PDF"].Value | ||||
|       | _ -> "" | ||||
|       | HtmlFormat -> s.["HTML Format"].Value | ||||
|       | PlainTextFormat -> s.["Plain-Text Format"].Value | ||||
|     seq { | ||||
|       yield "", LocalizedString ("", sprintf "%s (%s)" s.["Group Default"].Value defaultType) | ||||
|       yield EmailType.Html, s.["HTML Format"] | ||||
|       yield EmailType.PlainText, s.["Plain-Text Format"] | ||||
|       yield HtmlFormat.code, s.["HTML Format"] | ||||
|       yield PlainTextFormat.code, s.["Plain-Text Format"] | ||||
|       } | ||||
| 
 | ||||
|   /// A list of expiration options | ||||
|   let expirationList (s : IStringLocalizer) includeExpireNow = | ||||
|     [ yield "N", s.["Expire Normally"] | ||||
|       yield "Y", s.["Request Never Expires"] | ||||
|       match includeExpireNow with true -> yield "X", s.["Expire Immediately"] | false -> () | ||||
|     [ yield Automatic.code, s.["Expire Normally"] | ||||
|       yield Manual.code, s.["Request Never Expires"] | ||||
|       match includeExpireNow with true -> yield Forced.code, s.["Expire Immediately"] | false -> () | ||||
|       ] | ||||
| 
 | ||||
|   /// A list of request types | ||||
|   let requestTypeList (s : IStringLocalizer) = | ||||
|     [ RequestType.Current,      s.["Current Requests"] | ||||
|       RequestType.Recurring,    s.["Long-Term Requests"] | ||||
|       RequestType.Praise,       s.["Praise Reports"] | ||||
|       RequestType.Expecting,    s.["Expecting"] | ||||
|       RequestType.Announcement, s.["Announcements"] | ||||
|     [ CurrentRequest,  s.["Current Requests"] | ||||
|       LongTermRequest, s.["Long-Term Requests"] | ||||
|       PraiseReport,    s.["Praise Reports"] | ||||
|       Expecting,       s.["Expecting"] | ||||
|       Announcement,    s.["Announcements"] | ||||
|       ] | ||||
|      | ||||
| 
 | ||||
| @ -292,7 +290,7 @@ with | ||||
|       requestSort         = prefs.requestSort.code | ||||
|       emailFromName       = prefs.emailFromName | ||||
|       emailFromAddress    = prefs.emailFromAddress | ||||
|       defaultEmailType    = prefs.defaultEmailType | ||||
|       defaultEmailType    = prefs.defaultEmailType.code | ||||
|       headingLineType     = setType prefs.lineColor | ||||
|       headingLineColor    = prefs.lineColor | ||||
|       headingTextType     = setType prefs.headingColor | ||||
| @ -325,7 +323,7 @@ with | ||||
|         requestSort         = RequestSort.fromCode this.requestSort | ||||
|         emailFromName       = this.emailFromName | ||||
|         emailFromAddress    = this.emailFromAddress | ||||
|         defaultEmailType    = this.defaultEmailType | ||||
|         defaultEmailType    = EmailFormat.fromCode this.defaultEmailType | ||||
|         lineColor           = this.headingLineColor | ||||
|         headingColor        = this.headingTextColor | ||||
|         listFonts           = this.listFonts | ||||
| @ -362,20 +360,20 @@ with | ||||
|   /// An empty instance to use for new requests | ||||
|   static member empty = | ||||
|     { requestId      = Guid.Empty | ||||
|       requestType    = "" | ||||
|       requestType    = CurrentRequest.code | ||||
|       enteredDate    = None | ||||
|       skipDateUpdate = None | ||||
|       requestor      = None | ||||
|       expiration     = "N" | ||||
|       expiration     = Automatic.code | ||||
|       text           = "" | ||||
|       } | ||||
|   /// Create an instance from an existing request | ||||
|   static member fromRequest req = | ||||
|     { EditRequest.empty with | ||||
|         requestId   = req.prayerRequestId | ||||
|         requestType = req.requestType | ||||
|         requestType = req.requestType.code | ||||
|         requestor   = req.requestor | ||||
|         expiration  = match req.doNotExpire with true -> "Y" | false -> "N" | ||||
|         expiration  = req.expiration.code | ||||
|         text        = req.text | ||||
|       } | ||||
|   /// Is this a new request? | ||||
| @ -516,7 +514,7 @@ type Overview = | ||||
|   { /// The total number of active requests | ||||
|     totalActiveReqs : int | ||||
|     /// The numbers of active requests by category | ||||
|     activeReqsByCat : Map<string, int> | ||||
|     activeReqsByCat : Map<PrayerRequestType, int> | ||||
|     /// A count of all requests | ||||
|     allReqs         : int | ||||
|     /// A count of all members | ||||
|  | ||||
| @ -67,18 +67,15 @@ let sendEmails (client : SmtpClient) (recipients : Member list) grp subj html te | ||||
|     let plainTextMsg = createTextMessage grp subj text s | ||||
| 
 | ||||
|     for mbr in recipients do | ||||
|       let emailType = match mbr.format with Some f -> f | None -> grp.preferences.defaultEmailType | ||||
|       let emailTo = MailboxAddress (mbr.memberName, mbr.email) | ||||
|       let emailType = match mbr.format with Some f -> EmailFormat.fromCode f | None -> grp.preferences.defaultEmailType | ||||
|       let emailTo   = MailboxAddress (mbr.memberName, mbr.email) | ||||
|       match emailType with | ||||
|       | EmailType.Html -> | ||||
|       | HtmlFormat -> | ||||
|           htmlMsg.To.Add emailTo | ||||
|           do! client.SendAsync htmlMsg | ||||
|           htmlMsg.To.Clear () | ||||
|       | EmailType.PlainText -> | ||||
|       | PlainTextFormat -> | ||||
|           plainTextMsg.To.Add emailTo | ||||
|           do! client.SendAsync plainTextMsg | ||||
|           plainTextMsg.To.Clear () | ||||
|       | EmailType.AttachedPdf -> | ||||
|           raise <| NotImplementedException "Attached PDF format has not been implemented" | ||||
|       | _ -> invalidOp <| sprintf "Unknown e-mail type %s passed" emailType | ||||
|     } | ||||
|  | ||||
| @ -136,7 +136,7 @@ let expire reqId : HttpHandler = | ||||
|       | Ok r -> | ||||
|           let db = ctx.dbContext () | ||||
|           let s  = Views.I18N.localizer.Force () | ||||
|           db.UpdateEntry { r with isManuallyExpired = true } | ||||
|           db.UpdateEntry { r with expiration = Forced } | ||||
|           let! _ = db.SaveChangesAsync () | ||||
|           addInfo ctx s.["Successfully {0} prayer request", s.["Expired"].Value.ToLower ()] | ||||
|           return! redirectTo false "/prayer-requests" next ctx | ||||
| @ -247,7 +247,7 @@ let restore reqId : HttpHandler = | ||||
|       | Ok r -> | ||||
|           let db = ctx.dbContext () | ||||
|           let s  = Views.I18N.localizer.Force () | ||||
|           db.UpdateEntry { r with isManuallyExpired = false; updatedDate = DateTime.Now } | ||||
|           db.UpdateEntry { r with expiration = Automatic; updatedDate = DateTime.Now } | ||||
|           let! _ = db.SaveChangesAsync () | ||||
|           addInfo ctx s.["Successfully {0} prayer request", s.["Restored"].Value.ToLower ()] | ||||
|           return! redirectTo false "/prayer-requests" next ctx | ||||
| @ -273,11 +273,10 @@ let save : HttpHandler = | ||||
|           | Some pr -> | ||||
|               let upd8 = | ||||
|                 { pr with | ||||
|                     requestType       = m.requestType | ||||
|                     requestor         = m.requestor | ||||
|                     text              = ckEditorToText m.text | ||||
|                     doNotExpire       = m.expiration = "Y" | ||||
|                     isManuallyExpired = m.expiration = "X" | ||||
|                     requestType = PrayerRequestType.fromCode m.requestType | ||||
|                     requestor   = m.requestor | ||||
|                     text        = ckEditorToText m.text | ||||
|                     expiration  = Expiration.fromCode m.expiration | ||||
|                   } | ||||
|               let grp = currentGroup ctx | ||||
|               let now = grp.localDateNow (ctx.GetService<IClock> ()) | ||||
|  | ||||
| @ -387,7 +387,7 @@ let sendAnnouncement : HttpHandler = | ||||
|                   prayerRequestId = Guid.NewGuid () | ||||
|                   smallGroupId    = grp.smallGroupId | ||||
|                   userId          = usr.userId | ||||
|                   requestType     = Option.get m.requestType | ||||
|                   requestType     = (Option.get >> PrayerRequestType.fromCode) m.requestType | ||||
|                   text            = requestText | ||||
|                   enteredDate     = now | ||||
|                   updatedDate     = now | ||||
|  | ||||
| @ -4,3 +4,24 @@ create index "IX_PrayerRequest_Requestor_TRGM" on "PrayerRequest" using GIN (COA | ||||
| create index "IX_PrayerRequest_Text_TRGM" on "PrayerRequest" using GIN ("Text" gin_trgm_ops); | ||||
| alter table "ListPreference" add column "PageSize" int not null default 100; | ||||
| alter table "ListPreference" add column "AsOfDateDisplay" varchar(1) not null default 'N'; | ||||
| /* RequestType to 1 character code */ | ||||
| update "PrayerRequest" set "RequestType" = 'C' where "RequestType" = 'Current'; | ||||
| update "PrayerRequest" set "RequestType" = 'L' where "RequestType" = 'Recurring'; | ||||
| update "PrayerRequest" set "RequestType" = 'P' where "RequestType" = 'Praise'; | ||||
| update "PrayerRequest" set "RequestType" = 'E' where "RequestType" = 'Expecting'; | ||||
| update "PrayerRequest" set "RequestType" = 'A' where "RequestType" = 'Announcement'; | ||||
| alter table "PrayerRequest" alter column "RequestType" set data type varchar(1); | ||||
| /* Change expiration to a 1-character code field */ | ||||
| alter table "PrayerRequest" add column "Expiration" varchar(1); | ||||
| update "PrayerRequest" set "Expiration" = case when "IsManuallyExpired" then 'F' when "DoNotExpire" then 'M' else 'A' end; | ||||
| alter table "PrayerRequest" alter column "Expiration" set not null; | ||||
| alter table "PrayerRequest" drop column "DoNotExpire"; | ||||
| alter table "PrayerRequest" drop column "IsManuallyExpired"; | ||||
| /* Change e-mail type to 1-character code field in list preferences and members */ | ||||
| update "ListPreference" set "DefaultEmailType" = 'H' where "DefaultEmailType" = 'Html'; | ||||
| update "ListPreference" set "DefaultEmailType" = 'P' where "DefaultEmailType" = 'Text'; | ||||
| alter table "ListPreference" alter column "DefaultEmailType" set default 'H'; | ||||
| alter table "ListPreference" alter column "DefaultEmailType" set data type varchar(1); | ||||
| update "Member" set "Format" = 'H' where "Format" = 'Html'; | ||||
| update "Member" set "Format" = 'P' where "Format" = 'Text'; | ||||
| alter table "Member" alter column "Format" set data type varchar(1); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user