From cd97f78a3e25dfe7de03c760515b0ea54e8bd072 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 5 Aug 2022 18:53:57 -0400 Subject: [PATCH] Add last seen field to users (#39) --- src/PrayerTracker.Data/DataAccess.fs | 2 +- src/PrayerTracker.Data/Entities.fs | 78 ++++++++++--------- .../20161217153124_InitialDatabase.fs | 16 ++-- .../Migrations/AppDbContextModelSnapshot.fs | 1 + src/PrayerTracker.UI/Resources/Common.es.resx | 3 + src/PrayerTracker.UI/User.fs | 5 +- src/PrayerTracker/Extensions.fs | 7 +- src/names-to-lower.sql | 1 + 8 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/PrayerTracker.Data/DataAccess.fs b/src/PrayerTracker.Data/DataAccess.fs index 9d5e7cd..2abcde8 100644 --- a/src/PrayerTracker.Data/DataAccess.fs +++ b/src/PrayerTracker.Data/DataAccess.fs @@ -231,7 +231,7 @@ type AppDbContext with let! usr = this.Users.Include(fun u -> u.SmallGroups).SingleOrDefaultAsync (fun u -> u.Id = userId) return Option.fromObject usr } - + /// Get a list of all users member this.AllUsers () = backgroundTask { let! users = this.Users.OrderBy(fun u -> u.LastName).ThenBy(fun u -> u.FirstName).ToListAsync () diff --git a/src/PrayerTracker.Data/Entities.fs b/src/PrayerTracker.Data/Entities.fs index 0b7b964..62418fc 100644 --- a/src/PrayerTracker.Data/Entities.fs +++ b/src/PrayerTracker.Data/Entities.fs @@ -375,7 +375,6 @@ type ChurchStats = (*-- ENTITIES --*) -open System.Collections.Generic open FSharp.EFCore.OptionConverter open Microsoft.EntityFrameworkCore open NodaTime @@ -401,7 +400,7 @@ type [] Church = InterfaceAddress : string option /// Small groups for this church - SmallGroups : ICollection + SmallGroups : ResizeArray } with /// An empty church @@ -413,7 +412,7 @@ with State = "" HasVpsInterface = false InterfaceAddress = None - SmallGroups = List () + SmallGroups = ResizeArray () } /// Configure EF for this entity @@ -430,9 +429,9 @@ with } |> List.ofSeq |> ignore) |> ignore mb.Model.FindEntityType(typeof).FindProperty(nameof Church.empty.Id) - .SetValueConverter(Converters.ChurchIdConverter ()) + .SetValueConverter (Converters.ChurchIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof Church.empty.InterfaceAddress) - .SetValueConverter(OptionConverter ()) + .SetValueConverter (OptionConverter ()) /// Preferences for the form and format of the prayer request list @@ -556,15 +555,15 @@ with } |> List.ofSeq |> ignore) |> ignore mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.SmallGroupId) - .SetValueConverter(Converters.SmallGroupIdConverter ()) + .SetValueConverter (Converters.SmallGroupIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.RequestSort) - .SetValueConverter(Converters.RequestSortConverter ()) + .SetValueConverter (Converters.RequestSortConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.DefaultEmailType) - .SetValueConverter(Converters.EmailFormatConverter ()) + .SetValueConverter (Converters.EmailFormatConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.TimeZoneId) - .SetValueConverter(Converters.TimeZoneIdConverter ()) + .SetValueConverter (Converters.TimeZoneIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.AsOfDateDisplay) - .SetValueConverter(Converters.AsOfDateDisplayConverter ()) + .SetValueConverter (Converters.AsOfDateDisplayConverter ()) /// A member of a small group @@ -612,11 +611,11 @@ with } |> List.ofSeq |> ignore) |> ignore mb.Model.FindEntityType(typeof).FindProperty(nameof Member.empty.Id) - .SetValueConverter(Converters.MemberIdConverter ()) + .SetValueConverter (Converters.MemberIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof Member.empty.SmallGroupId) - .SetValueConverter(Converters.SmallGroupIdConverter ()) + .SetValueConverter (Converters.SmallGroupIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof Member.empty.Format) - .SetValueConverter(Converters.EmailFormatOptionConverter ()) + .SetValueConverter (Converters.EmailFormatOptionConverter ()) /// This represents a single prayer request @@ -707,17 +706,17 @@ with } |> List.ofSeq |> ignore) |> ignore mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.Id) - .SetValueConverter(Converters.PrayerRequestIdConverter ()) + .SetValueConverter (Converters.PrayerRequestIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.RequestType) - .SetValueConverter(Converters.PrayerRequestTypeConverter ()) + .SetValueConverter (Converters.PrayerRequestTypeConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.UserId) - .SetValueConverter(Converters.UserIdConverter ()) + .SetValueConverter (Converters.UserIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.SmallGroupId) - .SetValueConverter(Converters.SmallGroupIdConverter ()) + .SetValueConverter (Converters.SmallGroupIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.Requestor) - .SetValueConverter(OptionConverter ()) + .SetValueConverter (OptionConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.Expiration) - .SetValueConverter(Converters.ExpirationConverter ()) + .SetValueConverter (Converters.ExpirationConverter ()) /// This represents a small group (Sunday School class, Bible study group, etc.) @@ -738,13 +737,13 @@ and [] SmallGroup = Preferences : ListPreferences /// The members of the group - Members : ICollection + Members : ResizeArray /// Prayer requests for this small group - PrayerRequests : ICollection + PrayerRequests : ResizeArray /// The users authorized to manage this group - Users : ICollection + Users : ResizeArray } with @@ -755,9 +754,9 @@ with Name = "" Church = Church.empty Preferences = ListPreferences.empty - Members = List () - PrayerRequests = List () - Users = List () + Members = ResizeArray () + PrayerRequests = ResizeArray () + Users = ResizeArray () } /// Get the local date for this group @@ -788,9 +787,9 @@ with } |> List.ofSeq |> ignore) |> ignore mb.Model.FindEntityType(typeof).FindProperty(nameof SmallGroup.empty.Id) - .SetValueConverter(Converters.SmallGroupIdConverter ()) + .SetValueConverter (Converters.SmallGroupIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof SmallGroup.empty.ChurchId) - .SetValueConverter(Converters.ChurchIdConverter ()) + .SetValueConverter (Converters.ChurchIdConverter ()) /// This represents a time zone in which a class may reside @@ -829,7 +828,7 @@ with } |> List.ofSeq |> ignore) |> ignore mb.Model.FindEntityType(typeof).FindProperty(nameof TimeZone.empty.Id) - .SetValueConverter(Converters.TimeZoneIdConverter ()) + .SetValueConverter (Converters.TimeZoneIdConverter ()) /// This represents a user of PrayerTracker @@ -855,8 +854,11 @@ and [] User = /// The salt for the user's hashed password Salt : Guid option + /// The last time the user was seen (set whenever the user is loaded into a session) + LastSeen : DateTime option + /// The small groups which this user is authorized - SmallGroups : ICollection + SmallGroups : ResizeArray } with @@ -869,7 +871,8 @@ with IsAdmin = false PasswordHash = "" Salt = None - SmallGroups = List () + LastSeen = None + SmallGroups = ResizeArray () } /// The full name of the user @@ -889,12 +892,15 @@ with it.Property(fun u -> u.IsAdmin).HasColumnName "is_admin" it.Property(fun u -> u.PasswordHash).HasColumnName("password_hash").IsRequired () it.Property(fun u -> u.Salt).HasColumnName "salt" + it.Property(fun u -> u.LastSeen).HasColumnName "last_seen" } |> List.ofSeq |> ignore) |> ignore mb.Model.FindEntityType(typeof).FindProperty(nameof User.empty.Id) - .SetValueConverter(Converters.UserIdConverter ()) + .SetValueConverter (Converters.UserIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof User.empty.Salt) - .SetValueConverter(OptionConverter ()) + .SetValueConverter (OptionConverter ()) + mb.Model.FindEntityType(typeof).FindProperty(nameof User.empty.LastSeen) + .SetValueConverter (OptionConverter ()) /// Cross-reference between user and small group @@ -930,15 +936,15 @@ with it.Property(fun usg -> usg.UserId).HasColumnName "user_id" it.Property(fun usg -> usg.SmallGroupId).HasColumnName "small_group_id" it.HasOne(fun usg -> usg.User) - .WithMany(fun u -> u.SmallGroups :> IEnumerable) + .WithMany(fun u -> u.SmallGroups :> seq) .HasForeignKey(fun usg -> usg.UserId :> obj) it.HasOne(fun usg -> usg.SmallGroup) - .WithMany(fun sg -> sg.Users :> IEnumerable) + .WithMany(fun sg -> sg.Users :> seq) .HasForeignKey(fun usg -> usg.SmallGroupId :> obj) } |> List.ofSeq |> ignore) |> ignore mb.Model.FindEntityType(typeof).FindProperty(nameof UserSmallGroup.empty.UserId) - .SetValueConverter(Converters.UserIdConverter ()) + .SetValueConverter (Converters.UserIdConverter ()) mb.Model.FindEntityType(typeof).FindProperty(nameof UserSmallGroup.empty.SmallGroupId) - .SetValueConverter(Converters.SmallGroupIdConverter ()) + .SetValueConverter (Converters.SmallGroupIdConverter ()) \ No newline at end of file diff --git a/src/PrayerTracker.Data/Migrations/20161217153124_InitialDatabase.fs b/src/PrayerTracker.Data/Migrations/20161217153124_InitialDatabase.fs index bc7a5f3..ca33df6 100644 --- a/src/PrayerTracker.Data/Migrations/20161217153124_InitialDatabase.fs +++ b/src/PrayerTracker.Data/Migrations/20161217153124_InitialDatabase.fs @@ -48,13 +48,14 @@ type InitialDatabase () = name = "pt_user", schema = "pt", columns = (fun table -> - {| Id = table.Column (name = "id", nullable = false) - Email = table.Column (name = "email", nullable = false) - FirstName = table.Column (name = "first_name", nullable = false) - IsAdmin = table.Column (name = "is_admin", nullable = false) - LastName = table.Column (name = "last_name", nullable = false) - PasswordHash = table.Column (name = "password_hash", nullable = false) - Salt = table.Column (name = "salt", nullable = true) + {| Id = table.Column (name = "id", nullable = false) + Email = table.Column (name = "email", nullable = false) + FirstName = table.Column (name = "first_name", nullable = false) + IsAdmin = table.Column (name = "is_admin", nullable = false) + LastName = table.Column (name = "last_name", nullable = false) + PasswordHash = table.Column (name = "password_hash", nullable = false) + Salt = table.Column (name = "salt", nullable = true) + LastSeen = table.Column (name = "last_seen", nullable = true) |}), constraints = fun table -> table.PrimaryKey("pk_pt_user", fun x -> upcast x.Id) |> ignore) @@ -321,6 +322,7 @@ type InitialDatabase () = b.Property("LastName").HasColumnName("last_name").IsRequired() |> ignore b.Property("PasswordHash").HasColumnName("password_hash").IsRequired() |> ignore b.Property("Salt").HasColumnName("salt") |> ignore + b.Property("LastSeen").HasColumnName("last_seen") |> ignore b.HasKey("Id") |> ignore b.ToTable("pt_user") |> ignore) |> ignore diff --git a/src/PrayerTracker.Data/Migrations/AppDbContextModelSnapshot.fs b/src/PrayerTracker.Data/Migrations/AppDbContextModelSnapshot.fs index 2b1aa10..688e153 100644 --- a/src/PrayerTracker.Data/Migrations/AppDbContextModelSnapshot.fs +++ b/src/PrayerTracker.Data/Migrations/AppDbContextModelSnapshot.fs @@ -106,6 +106,7 @@ type AppDbContextModelSnapshot () = b.Property("LastName").HasColumnName("last_name").IsRequired() |> ignore b.Property("PasswordHash").HasColumnName("password_hash").IsRequired() |> ignore b.Property("Salt").HasColumnName("salt") |> ignore + b.Property("LastSeen").HasColumnName("last_seen") |> ignore b.HasKey("Id") |> ignore b.ToTable("pt_user") |> ignore) |> ignore diff --git a/src/PrayerTracker.UI/Resources/Common.es.resx b/src/PrayerTracker.UI/Resources/Common.es.resx index 4ad4283..5eb1ca7 100644 --- a/src/PrayerTracker.UI/Resources/Common.es.resx +++ b/src/PrayerTracker.UI/Resources/Common.es.resx @@ -825,4 +825,7 @@ Estado o Provincia + + Ultima vez Visto + \ No newline at end of file diff --git a/src/PrayerTracker.UI/User.fs b/src/PrayerTracker.UI/User.fs index bd6657c..718ff04 100644 --- a/src/PrayerTracker.UI/User.fs +++ b/src/PrayerTracker.UI/User.fs @@ -192,7 +192,7 @@ let maintain (users : User list) ctx viewInfo = | [] -> space | _ -> table [ _class "pt-table pt-action-table" ] [ - tableHeadings s [ "Actions"; "Name"; "Admin?" ] + tableHeadings s [ "Actions"; "Name"; "Last Seen"; "Admin?" ] users |> List.map (fun user -> let userId = shortGuid user.Id.Value @@ -212,6 +212,9 @@ let maintain (users : User list) ctx viewInfo = ] ] td [] [ str user.Name ] + td [] [ + str (match user.LastSeen with Some dt -> dt.ToString s["MMMM d, yyyy"] | None -> "--") + ] td [ _class "pt-center-text" ] [ if user.IsAdmin then strong [] [ locStr s["Yes"] ] else locStr s["No"] ] diff --git a/src/PrayerTracker/Extensions.fs b/src/PrayerTracker/Extensions.fs index 8c1b107..72063c0 100644 --- a/src/PrayerTracker/Extensions.fs +++ b/src/PrayerTracker/Extensions.fs @@ -1,6 +1,7 @@ [] module PrayerTracker.Extensions +open System open Microsoft.AspNetCore.Http open Microsoft.FSharpLu open Newtonsoft.Json @@ -98,7 +99,11 @@ type HttpContext with | Some userId -> match! this.Db.TryUserById userId with | Some user -> - this.Session.CurrentUser <- Some user + // Set last seen for user + this.Db.UpdateEntry { user with LastSeen = Some DateTime.UtcNow } + let! _ = this.Db.SaveChangesAsync () + this.Session.CurrentUser <- + Some { user with PasswordHash = ""; SmallGroups = ResizeArray () } return Some user | None -> return None | None -> return None diff --git a/src/names-to-lower.sql b/src/names-to-lower.sql index 1a33ae3..1e9ec87 100644 --- a/src/names-to-lower.sql +++ b/src/names-to-lower.sql @@ -94,6 +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; -- User / Small Group ALTER TABLE pt."User_SmallGroup" RENAME COLUMN "UserId" TO user_id;