From 41ae1d8dad9b2f519c621a627dbedc260a059d31 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 19 Jul 2022 22:51:51 -0400 Subject: [PATCH] First cut of user admin page (#19) --- src/MyWebLog.Data/SQLite/Helpers.fs | 162 ++++---- .../SQLite/SQLiteCategoryData.fs | 12 +- src/MyWebLog.Data/SQLite/SQLitePageData.fs | 20 +- src/MyWebLog.Data/SQLite/SQLitePostData.fs | 54 +-- src/MyWebLog.Data/SQLite/SQLiteUploadData.fs | 10 +- src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs | 76 ++-- .../SQLite/SQLiteWebLogUserData.fs | 24 +- src/MyWebLog.Domain/DataTypes.fs | 172 ++++----- src/MyWebLog.Domain/SupportTypes.fs | 72 ++-- src/MyWebLog.Domain/ViewModels.fs | 351 ++++++++++-------- src/MyWebLog/DotLiquidBespoke.fs | 11 +- src/MyWebLog/Handlers/Routes.fs | 1 + src/MyWebLog/Handlers/User.fs | 20 + src/admin-theme/_layout.liquid | 1 + src/admin-theme/user-list-body.liquid | 56 +++ src/admin-theme/user-list.liquid | 20 + src/admin-theme/wwwroot/admin.js | 7 +- 17 files changed, 605 insertions(+), 464 deletions(-) create mode 100644 src/admin-theme/user-list-body.liquid create mode 100644 src/admin-theme/user-list.liquid diff --git a/src/MyWebLog.Data/SQLite/Helpers.fs b/src/MyWebLog.Data/SQLite/Helpers.fs index 6cf0619..05c25b2 100644 --- a/src/MyWebLog.Data/SQLite/Helpers.fs +++ b/src/MyWebLog.Data/SQLite/Helpers.fs @@ -105,47 +105,47 @@ module Map = /// Create a category from the current row in the given data reader let toCategory rdr : Category = - { Id = toCategoryId rdr - WebLogId = getString "web_log_id" rdr |> WebLogId - Name = getString "name" rdr - Slug = getString "slug" rdr - Description = tryString "description" rdr - ParentId = tryString "parent_id" rdr |> Option.map CategoryId + { Id = toCategoryId rdr + WebLogId = getString "web_log_id" rdr |> WebLogId + Name = getString "name" rdr + Slug = getString "slug" rdr + Description = tryString "description" rdr + ParentId = tryString "parent_id" rdr |> Option.map CategoryId } /// Create a custom feed from the current row in the given data reader let toCustomFeed rdr : CustomFeed = - { Id = getString "id" rdr |> CustomFeedId - Source = getString "source" rdr |> CustomFeedSource.parse - Path = getString "path" rdr |> Permalink - Podcast = - if rdr.IsDBNull (rdr.GetOrdinal "title") then - None - else - Some { - Title = getString "title" rdr - Subtitle = tryString "subtitle" rdr - ItemsInFeed = getInt "items_in_feed" rdr - Summary = getString "summary" rdr - DisplayedAuthor = getString "displayed_author" rdr - Email = getString "email" rdr - ImageUrl = getString "image_url" rdr |> Permalink - AppleCategory = getString "apple_category" rdr - AppleSubcategory = tryString "apple_subcategory" rdr - Explicit = getString "explicit" rdr |> ExplicitRating.parse - DefaultMediaType = tryString "default_media_type" rdr - MediaBaseUrl = tryString "media_base_url" rdr - PodcastGuid = tryGuid "podcast_guid" rdr - FundingUrl = tryString "funding_url" rdr - FundingText = tryString "funding_text" rdr - Medium = tryString "medium" rdr |> Option.map PodcastMedium.parse - } + { Id = getString "id" rdr |> CustomFeedId + Source = getString "source" rdr |> CustomFeedSource.parse + Path = getString "path" rdr |> Permalink + Podcast = + if rdr.IsDBNull (rdr.GetOrdinal "title") then + None + else + Some { + Title = getString "title" rdr + Subtitle = tryString "subtitle" rdr + ItemsInFeed = getInt "items_in_feed" rdr + Summary = getString "summary" rdr + DisplayedAuthor = getString "displayed_author" rdr + Email = getString "email" rdr + ImageUrl = getString "image_url" rdr |> Permalink + AppleCategory = getString "apple_category" rdr + AppleSubcategory = tryString "apple_subcategory" rdr + Explicit = getString "explicit" rdr |> ExplicitRating.parse + DefaultMediaType = tryString "default_media_type" rdr + MediaBaseUrl = tryString "media_base_url" rdr + PodcastGuid = tryGuid "podcast_guid" rdr + FundingUrl = tryString "funding_url" rdr + FundingText = tryString "funding_text" rdr + Medium = tryString "medium" rdr |> Option.map PodcastMedium.parse + } } /// Create a meta item from the current row in the given data reader let toMetaItem rdr : MetaItem = - { Name = getString "name" rdr - Value = getString "value" rdr + { Name = getString "name" rdr + Value = getString "value" rdr } /// Create a permalink from the current row in the given data reader @@ -206,16 +206,16 @@ module Map = /// Create a revision from the current row in the given data reader let toRevision rdr : Revision = - { AsOf = getDateTime "as_of" rdr - Text = getString "revision_text" rdr |> MarkupText.parse + { AsOf = getDateTime "as_of" rdr + Text = getString "revision_text" rdr |> MarkupText.parse } /// Create a tag mapping from the current row in the given data reader let toTagMap rdr : TagMap = - { Id = getString "id" rdr |> TagMapId - WebLogId = getString "web_log_id" rdr |> WebLogId - Tag = getString "tag" rdr - UrlValue = getString "url_value" rdr + { Id = getString "id" rdr |> TagMapId + WebLogId = getString "web_log_id" rdr |> WebLogId + Tag = getString "tag" rdr + UrlValue = getString "url_value" rdr } /// Create a theme from the current row in the given data reader (excludes templates) @@ -236,15 +236,15 @@ module Map = dataStream.ToArray () else [||] - { Id = ThemeAssetId (ThemeId (getString "theme_id" rdr), getString "path" rdr) - UpdatedOn = getDateTime "updated_on" rdr - Data = assetData + { Id = ThemeAssetId (ThemeId (getString "theme_id" rdr), getString "path" rdr) + UpdatedOn = getDateTime "updated_on" rdr + Data = assetData } /// Create a theme template from the current row in the given data reader let toThemeTemplate rdr : ThemeTemplate = - { Name = getString "name" rdr - Text = getString "template" rdr + { Name = getString "name" rdr + Text = getString "template" rdr } /// Create an uploaded file from the current row in the given data reader @@ -257,51 +257,51 @@ module Map = dataStream.ToArray () else [||] - { Id = getString "id" rdr |> UploadId - WebLogId = getString "web_log_id" rdr |> WebLogId - Path = getString "path" rdr |> Permalink - UpdatedOn = getDateTime "updated_on" rdr - Data = data + { Id = getString "id" rdr |> UploadId + WebLogId = getString "web_log_id" rdr |> WebLogId + Path = getString "path" rdr |> Permalink + UpdatedOn = getDateTime "updated_on" rdr + Data = data } /// Create a web log from the current row in the given data reader let toWebLog rdr : WebLog = - { Id = getString "id" rdr |> WebLogId - Name = getString "name" rdr - Slug = getString "slug" rdr - Subtitle = tryString "subtitle" rdr - DefaultPage = getString "default_page" rdr - PostsPerPage = getInt "posts_per_page" rdr - ThemeId = getString "theme_id" rdr |> ThemeId - UrlBase = getString "url_base" rdr - TimeZone = getString "time_zone" rdr - AutoHtmx = getBoolean "auto_htmx" rdr - Uploads = getString "uploads" rdr |> UploadDestination.parse - Rss = { - IsFeedEnabled = getBoolean "is_feed_enabled" rdr - FeedName = getString "feed_name" rdr - ItemsInFeed = tryInt "items_in_feed" rdr - IsCategoryEnabled = getBoolean "is_category_enabled" rdr - IsTagEnabled = getBoolean "is_tag_enabled" rdr - Copyright = tryString "copyright" rdr - CustomFeeds = [] - } + { Id = getString "id" rdr |> WebLogId + Name = getString "name" rdr + Slug = getString "slug" rdr + Subtitle = tryString "subtitle" rdr + DefaultPage = getString "default_page" rdr + PostsPerPage = getInt "posts_per_page" rdr + ThemeId = getString "theme_id" rdr |> ThemeId + UrlBase = getString "url_base" rdr + TimeZone = getString "time_zone" rdr + AutoHtmx = getBoolean "auto_htmx" rdr + Uploads = getString "uploads" rdr |> UploadDestination.parse + Rss = { + IsFeedEnabled = getBoolean "is_feed_enabled" rdr + FeedName = getString "feed_name" rdr + ItemsInFeed = tryInt "items_in_feed" rdr + IsCategoryEnabled = getBoolean "is_category_enabled" rdr + IsTagEnabled = getBoolean "is_tag_enabled" rdr + Copyright = tryString "copyright" rdr + CustomFeeds = [] + } } /// Create a web log user from the current row in the given data reader let toWebLogUser rdr : WebLogUser = - { Id = getString "id" rdr |> WebLogUserId - WebLogId = getString "web_log_id" rdr |> WebLogId - Email = getString "email" rdr - FirstName = getString "first_name" rdr - LastName = getString "last_name" rdr - PreferredName = getString "preferred_name" rdr - PasswordHash = getString "password_hash" rdr - Salt = getGuid "salt" rdr - Url = tryString "url" rdr - AccessLevel = getString "access_level" rdr |> AccessLevel.parse - CreatedOn = getDateTime "created_on" rdr - LastSeenOn = tryDateTime "last_seen_on" rdr + { Id = getString "id" rdr |> WebLogUserId + WebLogId = getString "web_log_id" rdr |> WebLogId + Email = getString "email" rdr + FirstName = getString "first_name" rdr + LastName = getString "last_name" rdr + PreferredName = getString "preferred_name" rdr + PasswordHash = getString "password_hash" rdr + Salt = getGuid "salt" rdr + Url = tryString "url" rdr + AccessLevel = getString "access_level" rdr |> AccessLevel.parse + CreatedOn = getDateTime "created_on" rdr + LastSeenOn = tryDateTime "last_seen_on" rdr } /// Add a possibly-missing parameter, substituting null for None diff --git a/src/MyWebLog.Data/SQLite/SQLiteCategoryData.fs b/src/MyWebLog.Data/SQLite/SQLiteCategoryData.fs index 1da225c..1133576 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteCategoryData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteCategoryData.fs @@ -10,12 +10,12 @@ type SQLiteCategoryData (conn : SqliteConnection) = /// Add parameters for category INSERT or UPDATE statements let addCategoryParameters (cmd : SqliteCommand) (cat : Category) = - [ cmd.Parameters.AddWithValue ("@id", CategoryId.toString cat.Id) - cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString cat.WebLogId) - cmd.Parameters.AddWithValue ("@name", cat.Name) - cmd.Parameters.AddWithValue ("@slug", cat.Slug) - cmd.Parameters.AddWithValue ("@description", maybe cat.Description) - cmd.Parameters.AddWithValue ("@parentId", maybe (cat.ParentId |> Option.map CategoryId.toString)) + [ cmd.Parameters.AddWithValue ("@id", CategoryId.toString cat.Id) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString cat.WebLogId) + cmd.Parameters.AddWithValue ("@name", cat.Name) + cmd.Parameters.AddWithValue ("@slug", cat.Slug) + cmd.Parameters.AddWithValue ("@description", maybe cat.Description) + cmd.Parameters.AddWithValue ("@parentId", maybe (cat.ParentId |> Option.map CategoryId.toString)) ] |> ignore /// Add a category diff --git a/src/MyWebLog.Data/SQLite/SQLitePageData.fs b/src/MyWebLog.Data/SQLite/SQLitePageData.fs index 0dac7bb..2f194f4 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePageData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePageData.fs @@ -12,16 +12,16 @@ type SQLitePageData (conn : SqliteConnection) = /// Add parameters for page INSERT or UPDATE statements let addPageParameters (cmd : SqliteCommand) (page : Page) = - [ cmd.Parameters.AddWithValue ("@id", PageId.toString page.Id) - cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString page.WebLogId) - cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString page.AuthorId) - cmd.Parameters.AddWithValue ("@title", page.Title) - cmd.Parameters.AddWithValue ("@permalink", Permalink.toString page.Permalink) - cmd.Parameters.AddWithValue ("@publishedOn", page.PublishedOn) - cmd.Parameters.AddWithValue ("@updatedOn", page.UpdatedOn) - cmd.Parameters.AddWithValue ("@isInPageList", page.IsInPageList) - cmd.Parameters.AddWithValue ("@template", maybe page.Template) - cmd.Parameters.AddWithValue ("@text", page.Text) + [ cmd.Parameters.AddWithValue ("@id", PageId.toString page.Id) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString page.WebLogId) + cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString page.AuthorId) + cmd.Parameters.AddWithValue ("@title", page.Title) + cmd.Parameters.AddWithValue ("@permalink", Permalink.toString page.Permalink) + cmd.Parameters.AddWithValue ("@publishedOn", page.PublishedOn) + cmd.Parameters.AddWithValue ("@updatedOn", page.UpdatedOn) + cmd.Parameters.AddWithValue ("@isInPageList", page.IsInPageList) + cmd.Parameters.AddWithValue ("@template", maybe page.Template) + cmd.Parameters.AddWithValue ("@text", page.Text) ] |> ignore /// Append meta items to a page diff --git a/src/MyWebLog.Data/SQLite/SQLitePostData.fs b/src/MyWebLog.Data/SQLite/SQLitePostData.fs index fb4b7ff..fdfa1e9 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePostData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePostData.fs @@ -13,37 +13,37 @@ type SQLitePostData (conn : SqliteConnection) = /// Add parameters for post INSERT or UPDATE statements let addPostParameters (cmd : SqliteCommand) (post : Post) = - [ cmd.Parameters.AddWithValue ("@id", PostId.toString post.Id) - cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString post.WebLogId) - cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString post.AuthorId) - cmd.Parameters.AddWithValue ("@status", PostStatus.toString post.Status) - cmd.Parameters.AddWithValue ("@title", post.Title) - cmd.Parameters.AddWithValue ("@permalink", Permalink.toString post.Permalink) - cmd.Parameters.AddWithValue ("@publishedOn", maybe post.PublishedOn) - cmd.Parameters.AddWithValue ("@updatedOn", post.UpdatedOn) - cmd.Parameters.AddWithValue ("@template", maybe post.Template) - cmd.Parameters.AddWithValue ("@text", post.Text) + [ cmd.Parameters.AddWithValue ("@id", PostId.toString post.Id) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString post.WebLogId) + cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString post.AuthorId) + cmd.Parameters.AddWithValue ("@status", PostStatus.toString post.Status) + cmd.Parameters.AddWithValue ("@title", post.Title) + cmd.Parameters.AddWithValue ("@permalink", Permalink.toString post.Permalink) + cmd.Parameters.AddWithValue ("@publishedOn", maybe post.PublishedOn) + cmd.Parameters.AddWithValue ("@updatedOn", post.UpdatedOn) + cmd.Parameters.AddWithValue ("@template", maybe post.Template) + cmd.Parameters.AddWithValue ("@text", post.Text) ] |> ignore /// Add parameters for episode INSERT or UPDATE statements let addEpisodeParameters (cmd : SqliteCommand) (ep : Episode) = - [ cmd.Parameters.AddWithValue ("@media", ep.Media) - cmd.Parameters.AddWithValue ("@length", ep.Length) - cmd.Parameters.AddWithValue ("@duration", maybe ep.Duration) - cmd.Parameters.AddWithValue ("@mediaType", maybe ep.MediaType) - cmd.Parameters.AddWithValue ("@imageUrl", maybe ep.ImageUrl) - cmd.Parameters.AddWithValue ("@subtitle", maybe ep.Subtitle) - cmd.Parameters.AddWithValue ("@explicit", maybe (ep.Explicit |> Option.map ExplicitRating.toString)) - cmd.Parameters.AddWithValue ("@chapterFile", maybe ep.ChapterFile) - cmd.Parameters.AddWithValue ("@chapterType", maybe ep.ChapterType) - cmd.Parameters.AddWithValue ("@transcriptUrl", maybe ep.TranscriptUrl) - cmd.Parameters.AddWithValue ("@transcriptType", maybe ep.TranscriptType) - cmd.Parameters.AddWithValue ("@transcriptLang", maybe ep.TranscriptLang) - cmd.Parameters.AddWithValue ("@transcriptCaptions", maybe ep.TranscriptCaptions) - cmd.Parameters.AddWithValue ("@seasonNumber", maybe ep.SeasonNumber) - cmd.Parameters.AddWithValue ("@seasonDescription", maybe ep.SeasonDescription) - cmd.Parameters.AddWithValue ("@episodeNumber", maybe (ep.EpisodeNumber |> Option.map string)) - cmd.Parameters.AddWithValue ("@episodeDescription", maybe ep.EpisodeDescription) + [ cmd.Parameters.AddWithValue ("@media", ep.Media) + cmd.Parameters.AddWithValue ("@length", ep.Length) + cmd.Parameters.AddWithValue ("@duration", maybe ep.Duration) + cmd.Parameters.AddWithValue ("@mediaType", maybe ep.MediaType) + cmd.Parameters.AddWithValue ("@imageUrl", maybe ep.ImageUrl) + cmd.Parameters.AddWithValue ("@subtitle", maybe ep.Subtitle) + cmd.Parameters.AddWithValue ("@explicit", maybe (ep.Explicit |> Option.map ExplicitRating.toString)) + cmd.Parameters.AddWithValue ("@chapterFile", maybe ep.ChapterFile) + cmd.Parameters.AddWithValue ("@chapterType", maybe ep.ChapterType) + cmd.Parameters.AddWithValue ("@transcriptUrl", maybe ep.TranscriptUrl) + cmd.Parameters.AddWithValue ("@transcriptType", maybe ep.TranscriptType) + cmd.Parameters.AddWithValue ("@transcriptLang", maybe ep.TranscriptLang) + cmd.Parameters.AddWithValue ("@transcriptCaptions", maybe ep.TranscriptCaptions) + cmd.Parameters.AddWithValue ("@seasonNumber", maybe ep.SeasonNumber) + cmd.Parameters.AddWithValue ("@seasonDescription", maybe ep.SeasonDescription) + cmd.Parameters.AddWithValue ("@episodeNumber", maybe (ep.EpisodeNumber |> Option.map string)) + cmd.Parameters.AddWithValue ("@episodeDescription", maybe ep.EpisodeDescription) ] |> ignore /// Append category IDs, tags, and meta items to a post diff --git a/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs b/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs index 4f20366..3960194 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs @@ -10,11 +10,11 @@ type SQLiteUploadData (conn : SqliteConnection) = /// Add parameters for uploaded file INSERT and UPDATE statements let addUploadParameters (cmd : SqliteCommand) (upload : Upload) = - [ cmd.Parameters.AddWithValue ("@id", UploadId.toString upload.Id) - cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString upload.WebLogId) - cmd.Parameters.AddWithValue ("@path", Permalink.toString upload.Path) - cmd.Parameters.AddWithValue ("@updatedOn", upload.UpdatedOn) - cmd.Parameters.AddWithValue ("@dataLength", upload.Data.Length) + [ cmd.Parameters.AddWithValue ("@id", UploadId.toString upload.Id) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString upload.WebLogId) + cmd.Parameters.AddWithValue ("@path", Permalink.toString upload.Path) + cmd.Parameters.AddWithValue ("@updatedOn", upload.UpdatedOn) + cmd.Parameters.AddWithValue ("@dataLength", upload.Data.Length) ] |> ignore /// Save an uploaded file diff --git a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs index 5762c7c..4fb7822 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs @@ -15,57 +15,57 @@ type SQLiteWebLogData (conn : SqliteConnection) = /// Add parameters for web log INSERT or web log/RSS options UPDATE statements let addWebLogRssParameters (cmd : SqliteCommand) (webLog : WebLog) = - [ cmd.Parameters.AddWithValue ("@isFeedEnabled", webLog.Rss.IsFeedEnabled) - cmd.Parameters.AddWithValue ("@feedName", webLog.Rss.FeedName) - cmd.Parameters.AddWithValue ("@itemsInFeed", maybe webLog.Rss.ItemsInFeed) - cmd.Parameters.AddWithValue ("@isCategoryEnabled", webLog.Rss.IsCategoryEnabled) - cmd.Parameters.AddWithValue ("@isTagEnabled", webLog.Rss.IsTagEnabled) - cmd.Parameters.AddWithValue ("@copyright", maybe webLog.Rss.Copyright) + [ cmd.Parameters.AddWithValue ("@isFeedEnabled", webLog.Rss.IsFeedEnabled) + cmd.Parameters.AddWithValue ("@feedName", webLog.Rss.FeedName) + cmd.Parameters.AddWithValue ("@itemsInFeed", maybe webLog.Rss.ItemsInFeed) + cmd.Parameters.AddWithValue ("@isCategoryEnabled", webLog.Rss.IsCategoryEnabled) + cmd.Parameters.AddWithValue ("@isTagEnabled", webLog.Rss.IsTagEnabled) + cmd.Parameters.AddWithValue ("@copyright", maybe webLog.Rss.Copyright) ] |> ignore /// Add parameters for web log INSERT or UPDATE statements let addWebLogParameters (cmd : SqliteCommand) (webLog : WebLog) = - [ cmd.Parameters.AddWithValue ("@id", WebLogId.toString webLog.Id) - cmd.Parameters.AddWithValue ("@name", webLog.Name) - cmd.Parameters.AddWithValue ("@slug", webLog.Slug) - cmd.Parameters.AddWithValue ("@subtitle", maybe webLog.Subtitle) - cmd.Parameters.AddWithValue ("@defaultPage", webLog.DefaultPage) - cmd.Parameters.AddWithValue ("@postsPerPage", webLog.PostsPerPage) - cmd.Parameters.AddWithValue ("@themeId", ThemeId.toString webLog.ThemeId) - cmd.Parameters.AddWithValue ("@urlBase", webLog.UrlBase) - cmd.Parameters.AddWithValue ("@timeZone", webLog.TimeZone) - cmd.Parameters.AddWithValue ("@autoHtmx", webLog.AutoHtmx) - cmd.Parameters.AddWithValue ("@uploads", UploadDestination.toString webLog.Uploads) + [ cmd.Parameters.AddWithValue ("@id", WebLogId.toString webLog.Id) + cmd.Parameters.AddWithValue ("@name", webLog.Name) + cmd.Parameters.AddWithValue ("@slug", webLog.Slug) + cmd.Parameters.AddWithValue ("@subtitle", maybe webLog.Subtitle) + cmd.Parameters.AddWithValue ("@defaultPage", webLog.DefaultPage) + cmd.Parameters.AddWithValue ("@postsPerPage", webLog.PostsPerPage) + cmd.Parameters.AddWithValue ("@themeId", ThemeId.toString webLog.ThemeId) + cmd.Parameters.AddWithValue ("@urlBase", webLog.UrlBase) + cmd.Parameters.AddWithValue ("@timeZone", webLog.TimeZone) + cmd.Parameters.AddWithValue ("@autoHtmx", webLog.AutoHtmx) + cmd.Parameters.AddWithValue ("@uploads", UploadDestination.toString webLog.Uploads) ] |> ignore addWebLogRssParameters cmd webLog /// Add parameters for custom feed INSERT or UPDATE statements let addCustomFeedParameters (cmd : SqliteCommand) webLogId (feed : CustomFeed) = - [ cmd.Parameters.AddWithValue ("@id", CustomFeedId.toString feed.Id) - cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) - cmd.Parameters.AddWithValue ("@source", CustomFeedSource.toString feed.Source) - cmd.Parameters.AddWithValue ("@path", Permalink.toString feed.Path) + [ cmd.Parameters.AddWithValue ("@id", CustomFeedId.toString feed.Id) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) + cmd.Parameters.AddWithValue ("@source", CustomFeedSource.toString feed.Source) + cmd.Parameters.AddWithValue ("@path", Permalink.toString feed.Path) ] |> ignore /// Add parameters for podcast INSERT or UPDATE statements let addPodcastParameters (cmd : SqliteCommand) feedId (podcast : PodcastOptions) = - [ cmd.Parameters.AddWithValue ("@feedId", CustomFeedId.toString feedId) - cmd.Parameters.AddWithValue ("@title", podcast.Title) - cmd.Parameters.AddWithValue ("@subtitle", maybe podcast.Subtitle) - cmd.Parameters.AddWithValue ("@itemsInFeed", podcast.ItemsInFeed) - cmd.Parameters.AddWithValue ("@summary", podcast.Summary) - cmd.Parameters.AddWithValue ("@displayedAuthor", podcast.DisplayedAuthor) - cmd.Parameters.AddWithValue ("@email", podcast.Email) - cmd.Parameters.AddWithValue ("@imageUrl", Permalink.toString podcast.ImageUrl) - cmd.Parameters.AddWithValue ("@appleCategory", podcast.AppleCategory) - cmd.Parameters.AddWithValue ("@appleSubcategory", maybe podcast.AppleSubcategory) - cmd.Parameters.AddWithValue ("@explicit", ExplicitRating.toString podcast.Explicit) - cmd.Parameters.AddWithValue ("@defaultMediaType", maybe podcast.DefaultMediaType) - cmd.Parameters.AddWithValue ("@mediaBaseUrl", maybe podcast.MediaBaseUrl) - cmd.Parameters.AddWithValue ("@podcastGuid", maybe podcast.PodcastGuid) - cmd.Parameters.AddWithValue ("@fundingUrl", maybe podcast.FundingUrl) - cmd.Parameters.AddWithValue ("@fundingText", maybe podcast.FundingText) - cmd.Parameters.AddWithValue ("@medium", maybe (podcast.Medium |> Option.map PodcastMedium.toString)) + [ cmd.Parameters.AddWithValue ("@feedId", CustomFeedId.toString feedId) + cmd.Parameters.AddWithValue ("@title", podcast.Title) + cmd.Parameters.AddWithValue ("@subtitle", maybe podcast.Subtitle) + cmd.Parameters.AddWithValue ("@itemsInFeed", podcast.ItemsInFeed) + cmd.Parameters.AddWithValue ("@summary", podcast.Summary) + cmd.Parameters.AddWithValue ("@displayedAuthor", podcast.DisplayedAuthor) + cmd.Parameters.AddWithValue ("@email", podcast.Email) + cmd.Parameters.AddWithValue ("@imageUrl", Permalink.toString podcast.ImageUrl) + cmd.Parameters.AddWithValue ("@appleCategory", podcast.AppleCategory) + cmd.Parameters.AddWithValue ("@appleSubcategory", maybe podcast.AppleSubcategory) + cmd.Parameters.AddWithValue ("@explicit", ExplicitRating.toString podcast.Explicit) + cmd.Parameters.AddWithValue ("@defaultMediaType", maybe podcast.DefaultMediaType) + cmd.Parameters.AddWithValue ("@mediaBaseUrl", maybe podcast.MediaBaseUrl) + cmd.Parameters.AddWithValue ("@podcastGuid", maybe podcast.PodcastGuid) + cmd.Parameters.AddWithValue ("@fundingUrl", maybe podcast.FundingUrl) + cmd.Parameters.AddWithValue ("@fundingText", maybe podcast.FundingText) + cmd.Parameters.AddWithValue ("@medium", maybe (podcast.Medium |> Option.map PodcastMedium.toString)) ] |> ignore /// Get the current custom feeds for a web log diff --git a/src/MyWebLog.Data/SQLite/SQLiteWebLogUserData.fs b/src/MyWebLog.Data/SQLite/SQLiteWebLogUserData.fs index b2b7918..57c4c68 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteWebLogUserData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteWebLogUserData.fs @@ -12,18 +12,18 @@ type SQLiteWebLogUserData (conn : SqliteConnection) = /// Add parameters for web log user INSERT or UPDATE statements let addWebLogUserParameters (cmd : SqliteCommand) (user : WebLogUser) = - [ cmd.Parameters.AddWithValue ("@id", WebLogUserId.toString user.Id) - cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString user.WebLogId) - cmd.Parameters.AddWithValue ("@email", user.Email) - cmd.Parameters.AddWithValue ("@firstName", user.FirstName) - cmd.Parameters.AddWithValue ("@lastName", user.LastName) - cmd.Parameters.AddWithValue ("@preferredName", user.PreferredName) - cmd.Parameters.AddWithValue ("@passwordHash", user.PasswordHash) - cmd.Parameters.AddWithValue ("@salt", user.Salt) - cmd.Parameters.AddWithValue ("@url", maybe user.Url) - cmd.Parameters.AddWithValue ("@accessLevel", AccessLevel.toString user.AccessLevel) - cmd.Parameters.AddWithValue ("@createdOn", user.CreatedOn) - cmd.Parameters.AddWithValue ("@lastSeenOn", maybe user.LastSeenOn) + [ cmd.Parameters.AddWithValue ("@id", WebLogUserId.toString user.Id) + cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString user.WebLogId) + cmd.Parameters.AddWithValue ("@email", user.Email) + cmd.Parameters.AddWithValue ("@firstName", user.FirstName) + cmd.Parameters.AddWithValue ("@lastName", user.LastName) + cmd.Parameters.AddWithValue ("@preferredName", user.PreferredName) + cmd.Parameters.AddWithValue ("@passwordHash", user.PasswordHash) + cmd.Parameters.AddWithValue ("@salt", user.Salt) + cmd.Parameters.AddWithValue ("@url", maybe user.Url) + cmd.Parameters.AddWithValue ("@accessLevel", AccessLevel.toString user.AccessLevel) + cmd.Parameters.AddWithValue ("@createdOn", user.CreatedOn) + cmd.Parameters.AddWithValue ("@lastSeenOn", maybe user.LastSeenOn) ] |> ignore // IMPLEMENTATION FUNCTIONS diff --git a/src/MyWebLog.Domain/DataTypes.fs b/src/MyWebLog.Domain/DataTypes.fs index 5427993..be25d27 100644 --- a/src/MyWebLog.Domain/DataTypes.fs +++ b/src/MyWebLog.Domain/DataTypes.fs @@ -30,12 +30,12 @@ module Category = /// An empty category let empty = - { Id = CategoryId.empty - WebLogId = WebLogId.empty - Name = "" - Slug = "" - Description = None - ParentId = None + { Id = CategoryId.empty + WebLogId = WebLogId.empty + Name = "" + Slug = "" + Description = None + ParentId = None } @@ -75,15 +75,15 @@ module Comment = /// An empty comment let empty = - { Id = CommentId.empty - PostId = PostId.empty - InReplyToId = None - Name = "" - Email = "" - Url = None - Status = Pending - PostedOn = DateTime.UtcNow - Text = "" + { Id = CommentId.empty + PostId = PostId.empty + InReplyToId = None + Name = "" + Email = "" + Url = None + Status = Pending + PostedOn = DateTime.UtcNow + Text = "" } @@ -135,19 +135,19 @@ module Page = /// An empty page let empty = - { Id = PageId.empty - WebLogId = WebLogId.empty - AuthorId = WebLogUserId.empty - Title = "" - Permalink = Permalink.empty - PublishedOn = DateTime.MinValue - UpdatedOn = DateTime.MinValue - IsInPageList = false - Template = None - Text = "" - Metadata = [] - PriorPermalinks = [] - Revisions = [] + { Id = PageId.empty + WebLogId = WebLogId.empty + AuthorId = WebLogUserId.empty + Title = "" + Permalink = Permalink.empty + PublishedOn = DateTime.MinValue + UpdatedOn = DateTime.MinValue + IsInPageList = false + Template = None + Text = "" + Metadata = [] + PriorPermalinks = [] + Revisions = [] } @@ -208,22 +208,22 @@ module Post = /// An empty post let empty = - { Id = PostId.empty - WebLogId = WebLogId.empty - AuthorId = WebLogUserId.empty - Status = Draft - Title = "" - Permalink = Permalink.empty - PublishedOn = None - UpdatedOn = DateTime.MinValue - Text = "" - Template = None - CategoryIds = [] - Tags = [] - Episode = None - Metadata = [] - PriorPermalinks = [] - Revisions = [] + { Id = PostId.empty + WebLogId = WebLogId.empty + AuthorId = WebLogUserId.empty + Status = Draft + Title = "" + Permalink = Permalink.empty + PublishedOn = None + UpdatedOn = DateTime.MinValue + Text = "" + Template = None + CategoryIds = [] + Tags = [] + Episode = None + Metadata = [] + PriorPermalinks = [] + Revisions = [] } @@ -247,10 +247,10 @@ module TagMap = /// An empty tag mapping let empty = - { Id = TagMapId.empty - WebLogId = WebLogId.empty - Tag = "" - UrlValue = "" + { Id = TagMapId.empty + WebLogId = WebLogId.empty + Tag = "" + UrlValue = "" } @@ -274,10 +274,10 @@ module Theme = /// An empty theme let empty = - { Id = ThemeId "" - Name = "" - Version = "" - Templates = [] + { Id = ThemeId "" + Name = "" + Version = "" + Templates = [] } @@ -299,9 +299,9 @@ module ThemeAsset = /// An empty theme asset let empty = - { Id = ThemeAssetId (ThemeId "", "") - UpdatedOn = DateTime.MinValue - Data = [||] + { Id = ThemeAssetId (ThemeId "", "") + UpdatedOn = DateTime.MinValue + Data = [||] } @@ -327,13 +327,13 @@ type Upload = module Upload = /// An empty upload - let empty = { - Id = UploadId.empty - WebLogId = WebLogId.empty - Path = Permalink.empty - UpdatedOn = DateTime.MinValue - Data = [||] - } + let empty = + { Id = UploadId.empty + WebLogId = WebLogId.empty + Path = Permalink.empty + UpdatedOn = DateTime.MinValue + Data = [||] + } /// A web log @@ -381,18 +381,18 @@ module WebLog = /// An empty web log let empty = - { Id = WebLogId.empty - Name = "" - Slug = "" - Subtitle = None - DefaultPage = "" - PostsPerPage = 10 - ThemeId = ThemeId "default" - UrlBase = "" - TimeZone = "" - Rss = RssOptions.empty - AutoHtmx = false - Uploads = Database + { Id = WebLogId.empty + Name = "" + Slug = "" + Subtitle = None + DefaultPage = "" + PostsPerPage = 10 + ThemeId = ThemeId "default" + UrlBase = "" + TimeZone = "" + Rss = RssOptions.empty + AutoHtmx = false + Uploads = Database } /// Get the host (including scheme) and extra path from the URL base @@ -461,18 +461,18 @@ module WebLogUser = /// An empty web log user let empty = - { Id = WebLogUserId.empty - WebLogId = WebLogId.empty - Email = "" - FirstName = "" - LastName = "" - PreferredName = "" - PasswordHash = "" - Salt = Guid.Empty - Url = None - AccessLevel = Author - CreatedOn = DateTime.UnixEpoch - LastSeenOn = None + { Id = WebLogUserId.empty + WebLogId = WebLogId.empty + Email = "" + FirstName = "" + LastName = "" + PreferredName = "" + PasswordHash = "" + Salt = Guid.Empty + Url = None + AccessLevel = Author + CreatedOn = DateTime.UnixEpoch + LastSeenOn = None } /// Get the user's displayed name diff --git a/src/MyWebLog.Domain/SupportTypes.fs b/src/MyWebLog.Domain/SupportTypes.fs index ef11552..4412b77 100644 --- a/src/MyWebLog.Domain/SupportTypes.fs +++ b/src/MyWebLog.Domain/SupportTypes.fs @@ -28,10 +28,10 @@ module AccessLevel = /// Weightings for access levels let private weights = - [ Author, 10 - Editor, 20 - WebLogAdmin, 30 - Administrator, 40 + [ Author, 10 + Editor, 20 + WebLogAdmin, 30 + Administrator, 40 ] |> Map.ofList @@ -195,25 +195,25 @@ type Episode = module Episode = /// An empty episode - let empty = { - Media = "" - Length = 0L - Duration = None - MediaType = None - ImageUrl = None - Subtitle = None - Explicit = None - ChapterFile = None - ChapterType = None - TranscriptUrl = None - TranscriptType = None - TranscriptLang = None - TranscriptCaptions = None - SeasonNumber = None - SeasonDescription = None - EpisodeNumber = None - EpisodeDescription = None - } + let empty = + { Media = "" + Length = 0L + Duration = None + MediaType = None + ImageUrl = None + Subtitle = None + Explicit = None + ChapterFile = None + ChapterType = None + TranscriptUrl = None + TranscriptType = None + TranscriptLang = None + TranscriptCaptions = None + SeasonNumber = None + SeasonDescription = None + EpisodeNumber = None + EpisodeDescription = None + } open Markdig @@ -285,8 +285,8 @@ module Revision = /// An empty revision let empty = - { AsOf = DateTime.UtcNow - Text = Html "" + { AsOf = DateTime.UtcNow + Text = Html "" } @@ -505,10 +505,10 @@ module CustomFeed = /// An empty custom feed let empty = - { Id = CustomFeedId "" - Source = Category (CategoryId "") - Path = Permalink "" - Podcast = None + { Id = CustomFeedId "" + Source = Category (CategoryId "") + Path = Permalink "" + Podcast = None } @@ -542,13 +542,13 @@ module RssOptions = /// An empty set of RSS options let empty = - { IsFeedEnabled = true - FeedName = "feed.xml" - ItemsInFeed = None - IsCategoryEnabled = true - IsTagEnabled = true - Copyright = None - CustomFeeds = [] + { IsFeedEnabled = true + FeedName = "feed.xml" + ItemsInFeed = None + IsCategoryEnabled = true + IsTagEnabled = true + Copyright = None + CustomFeeds = [] } diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index 33355ff..b4c3b4b 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -79,10 +79,10 @@ type DisplayCustomFeed = match feed.Source with | Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}" | Tag tag -> $"Tag: {tag}" - { Id = CustomFeedId.toString feed.Id - Source = source - Path = Permalink.toString feed.Path - IsPodcast = Option.isSome feed.Podcast + { Id = CustomFeedId.toString feed.Id + Source = source + Path = Permalink.toString feed.Path + IsPodcast = Option.isSome feed.Podcast } @@ -123,32 +123,32 @@ type DisplayPage = /// Create a minimal display page (no text or metadata) from a database page static member fromPageMinimal webLog (page : Page) = let pageId = PageId.toString page.Id - { Id = pageId - AuthorId = WebLogUserId.toString page.AuthorId - Title = page.Title - Permalink = Permalink.toString page.Permalink - PublishedOn = page.PublishedOn - UpdatedOn = page.UpdatedOn - IsInPageList = page.IsInPageList - IsDefault = pageId = webLog.DefaultPage - Text = "" - Metadata = [] + { Id = pageId + AuthorId = WebLogUserId.toString page.AuthorId + Title = page.Title + Permalink = Permalink.toString page.Permalink + PublishedOn = page.PublishedOn + UpdatedOn = page.UpdatedOn + IsInPageList = page.IsInPageList + IsDefault = pageId = webLog.DefaultPage + Text = "" + Metadata = [] } /// Create a display page from a database page static member fromPage webLog (page : Page) = let _, extra = WebLog.hostAndPath webLog let pageId = PageId.toString page.Id - { Id = pageId - AuthorId = WebLogUserId.toString page.AuthorId - Title = page.Title - Permalink = Permalink.toString page.Permalink - PublishedOn = page.PublishedOn - UpdatedOn = page.UpdatedOn - IsInPageList = page.IsInPageList - IsDefault = pageId = webLog.DefaultPage - Text = if extra = "" then page.Text else page.Text.Replace ("href=\"/", $"href=\"{extra}/") - Metadata = page.Metadata + { Id = pageId + AuthorId = WebLogUserId.toString page.AuthorId + Title = page.Title + Permalink = Permalink.toString page.Permalink + PublishedOn = page.PublishedOn + UpdatedOn = page.UpdatedOn + IsInPageList = page.IsInPageList + IsDefault = pageId = webLog.DefaultPage + Text = if extra = "" then page.Text else page.Text.Replace ("href=\"/", $"href=\"{extra}/") + Metadata = page.Metadata } @@ -168,9 +168,9 @@ with /// Create a display revision from an actual revision static member fromRevision webLog (rev : Revision) = - { AsOf = rev.AsOf - AsOfLocal = WebLog.localTime webLog rev.AsOf - Format = MarkupText.sourceType rev.Text + { AsOf = rev.AsOf + AsOfLocal = WebLog.localTime webLog rev.AsOf + Format = MarkupText.sourceType rev.Text } @@ -199,11 +199,56 @@ type DisplayUpload = static member fromUpload webLog source (upload : Upload) = let path = Permalink.toString upload.Path let name = Path.GetFileName path - { Id = UploadId.toString upload.Id - Name = name - Path = path.Replace (name, "") - UpdatedOn = Some (WebLog.localTime webLog upload.UpdatedOn) - Source = UploadDestination.toString source + { Id = UploadId.toString upload.Id + Name = name + Path = path.Replace (name, "") + UpdatedOn = Some (WebLog.localTime webLog upload.UpdatedOn) + Source = UploadDestination.toString source + } + + +/// View model to display a user's information +[] +type DisplayUser = + { /// The ID of the user + Id : string + + /// The user name (e-mail address) + Email : string + + /// The user's first name + FirstName : string + + /// The user's last name + LastName : string + + /// The user's preferred name + PreferredName : string + + /// The URL of the user's personal site + Url : string + + /// The user's access level + AccessLevel : string + + /// When the user was created + CreatedOn : DateTime + + /// When the user last logged on + LastSeenOn : Nullable + } + + /// Construct a displayed user from a web log user + static member fromUser webLog (user : WebLogUser) = + { Id = WebLogUserId.toString user.Id + Email = user.Email + FirstName = user.FirstName + LastName = user.LastName + PreferredName = user.PreferredName + Url = defaultArg user.Url "" + AccessLevel = AccessLevel.toString user.AccessLevel + CreatedOn = WebLog.localTime webLog user.CreatedOn + LastSeenOn = user.LastSeenOn |> Option.map (WebLog.localTime webLog) |> Option.toNullable } @@ -228,11 +273,11 @@ type EditCategoryModel = /// Create an edit model from an existing category static member fromCategory (cat : Category) = - { CategoryId = CategoryId.toString cat.Id - Name = cat.Name - Slug = cat.Slug - Description = defaultArg cat.Description "" - ParentId = cat.ParentId |> Option.map CategoryId.toString |> Option.defaultValue "" + { CategoryId = CategoryId.toString cat.Id + Name = cat.Name + Slug = cat.Slug + Description = defaultArg cat.Description "" + ParentId = cat.ParentId |> Option.map CategoryId.toString |> Option.defaultValue "" } @@ -305,27 +350,27 @@ type EditCustomFeedModel = /// An empty custom feed model static member empty = - { Id = "" - SourceType = "category" - SourceValue = "" - Path = "" - IsPodcast = false - Title = "" - Subtitle = "" - ItemsInFeed = 25 - Summary = "" - DisplayedAuthor = "" - Email = "" - ImageUrl = "" - AppleCategory = "" - AppleSubcategory = "" - Explicit = "no" - DefaultMediaType = "audio/mpeg" - MediaBaseUrl = "" - FundingUrl = "" - FundingText = "" - PodcastGuid = "" - Medium = "" + { Id = "" + SourceType = "category" + SourceValue = "" + Path = "" + IsPodcast = false + Title = "" + Subtitle = "" + ItemsInFeed = 25 + Summary = "" + DisplayedAuthor = "" + Email = "" + ImageUrl = "" + AppleCategory = "" + AppleSubcategory = "" + Explicit = "no" + DefaultMediaType = "audio/mpeg" + MediaBaseUrl = "" + FundingUrl = "" + FundingText = "" + PodcastGuid = "" + Medium = "" } /// Create a model from a custom feed @@ -413,11 +458,11 @@ type EditMyInfoModel = /// Create an edit model from a user static member fromUser (user : WebLogUser) = - { FirstName = user.FirstName - LastName = user.LastName - PreferredName = user.PreferredName - NewPassword = "" - NewPasswordConfirm = "" + { FirstName = user.FirstName + LastName = user.LastName + PreferredName = user.PreferredName + NewPassword = "" + NewPasswordConfirm = "" } @@ -459,15 +504,15 @@ type EditPageModel = | Some rev -> rev | None -> Revision.empty let page = if page.Metadata |> List.isEmpty then { page with Metadata = [ MetaItem.empty ] } else page - { PageId = PageId.toString page.Id - Title = page.Title - Permalink = Permalink.toString page.Permalink - Template = defaultArg page.Template "" - IsShownInPageList = page.IsInPageList - Source = MarkupText.sourceType latest.Text - Text = MarkupText.text latest.Text - MetaNames = page.Metadata |> List.map (fun m -> m.Name) |> Array.ofList - MetaValues = page.Metadata |> List.map (fun m -> m.Value) |> Array.ofList + { PageId = PageId.toString page.Id + Title = page.Title + Permalink = Permalink.toString page.Permalink + Template = defaultArg page.Template "" + IsShownInPageList = page.IsInPageList + Source = MarkupText.sourceType latest.Text + Text = MarkupText.text latest.Text + MetaNames = page.Metadata |> List.map (fun m -> m.Name) |> Array.ofList + MetaValues = page.Metadata |> List.map (fun m -> m.Value) |> Array.ofList } /// Whether this is a new page @@ -612,39 +657,39 @@ type EditPostModel = | None -> Revision.empty let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.empty ] } else post let episode = defaultArg post.Episode Episode.empty - { PostId = PostId.toString post.Id - Title = post.Title - Permalink = Permalink.toString post.Permalink - Source = MarkupText.sourceType latest.Text - Text = MarkupText.text latest.Text - Tags = String.Join (", ", post.Tags) - Template = defaultArg post.Template "" - CategoryIds = post.CategoryIds |> List.map CategoryId.toString |> Array.ofList - Status = PostStatus.toString post.Status - DoPublish = false - MetaNames = post.Metadata |> List.map (fun m -> m.Name) |> Array.ofList - MetaValues = post.Metadata |> List.map (fun m -> m.Value) |> Array.ofList - SetPublished = false - PubOverride = post.PublishedOn |> Option.map (WebLog.localTime webLog) |> Option.toNullable - SetUpdated = false - IsEpisode = Option.isSome post.Episode - Media = episode.Media - Length = episode.Length - Duration = defaultArg (episode.Duration |> Option.map (fun it -> it.ToString """hh\:mm\:ss""")) "" - MediaType = defaultArg episode.MediaType "" - ImageUrl = defaultArg episode.ImageUrl "" - Subtitle = defaultArg episode.Subtitle "" - Explicit = defaultArg (episode.Explicit |> Option.map ExplicitRating.toString) "" - ChapterFile = defaultArg episode.ChapterFile "" - ChapterType = defaultArg episode.ChapterType "" - TranscriptUrl = defaultArg episode.TranscriptUrl "" - TranscriptType = defaultArg episode.TranscriptType "" - TranscriptLang = defaultArg episode.TranscriptLang "" - TranscriptCaptions = defaultArg episode.TranscriptCaptions false - SeasonNumber = defaultArg episode.SeasonNumber 0 - SeasonDescription = defaultArg episode.SeasonDescription "" - EpisodeNumber = defaultArg (episode.EpisodeNumber |> Option.map string) "" - EpisodeDescription = defaultArg episode.EpisodeDescription "" + { PostId = PostId.toString post.Id + Title = post.Title + Permalink = Permalink.toString post.Permalink + Source = MarkupText.sourceType latest.Text + Text = MarkupText.text latest.Text + Tags = String.Join (", ", post.Tags) + Template = defaultArg post.Template "" + CategoryIds = post.CategoryIds |> List.map CategoryId.toString |> Array.ofList + Status = PostStatus.toString post.Status + DoPublish = false + MetaNames = post.Metadata |> List.map (fun m -> m.Name) |> Array.ofList + MetaValues = post.Metadata |> List.map (fun m -> m.Value) |> Array.ofList + SetPublished = false + PubOverride = post.PublishedOn |> Option.map (WebLog.localTime webLog) |> Option.toNullable + SetUpdated = false + IsEpisode = Option.isSome post.Episode + Media = episode.Media + Length = episode.Length + Duration = defaultArg (episode.Duration |> Option.map (fun it -> it.ToString """hh\:mm\:ss""")) "" + MediaType = defaultArg episode.MediaType "" + ImageUrl = defaultArg episode.ImageUrl "" + Subtitle = defaultArg episode.Subtitle "" + Explicit = defaultArg (episode.Explicit |> Option.map ExplicitRating.toString) "" + ChapterFile = defaultArg episode.ChapterFile "" + ChapterType = defaultArg episode.ChapterType "" + TranscriptUrl = defaultArg episode.TranscriptUrl "" + TranscriptType = defaultArg episode.TranscriptType "" + TranscriptLang = defaultArg episode.TranscriptLang "" + TranscriptCaptions = defaultArg episode.TranscriptCaptions false + SeasonNumber = defaultArg episode.SeasonNumber 0 + SeasonDescription = defaultArg episode.SeasonDescription "" + EpisodeNumber = defaultArg (episode.EpisodeNumber |> Option.map string) "" + EpisodeDescription = defaultArg episode.EpisodeDescription "" } /// Whether this is a new post @@ -736,12 +781,12 @@ type EditRssModel = /// Create an edit model from a set of RSS options static member fromRssOptions (rss : RssOptions) = - { IsFeedEnabled = rss.IsFeedEnabled - FeedName = rss.FeedName - ItemsInFeed = defaultArg rss.ItemsInFeed 0 - IsCategoryEnabled = rss.IsCategoryEnabled - IsTagEnabled = rss.IsTagEnabled - Copyright = defaultArg rss.Copyright "" + { IsFeedEnabled = rss.IsFeedEnabled + FeedName = rss.FeedName + ItemsInFeed = defaultArg rss.ItemsInFeed 0 + IsCategoryEnabled = rss.IsCategoryEnabled + IsTagEnabled = rss.IsTagEnabled + Copyright = defaultArg rss.Copyright "" } /// Update RSS options from values in this mode @@ -774,9 +819,9 @@ type EditTagMapModel = /// Create an edit model from the tag mapping static member fromMapping (tagMap : TagMap) : EditTagMapModel = - { Id = TagMapId.toString tagMap.Id - Tag = tagMap.Tag - UrlValue = tagMap.UrlValue + { Id = TagMapId.toString tagMap.Id + Tag = tagMap.Tag + UrlValue = tagMap.UrlValue } @@ -819,20 +864,20 @@ type ManagePermalinksModel = /// Create a permalink model from a page static member fromPage (pg : Page) = - { Id = PageId.toString pg.Id - Entity = "page" - CurrentTitle = pg.Title - CurrentPermalink = Permalink.toString pg.Permalink - Prior = pg.PriorPermalinks |> List.map Permalink.toString |> Array.ofList + { Id = PageId.toString pg.Id + Entity = "page" + CurrentTitle = pg.Title + CurrentPermalink = Permalink.toString pg.Permalink + Prior = pg.PriorPermalinks |> List.map Permalink.toString |> Array.ofList } /// Create a permalink model from a post static member fromPost (post : Post) = - { Id = PostId.toString post.Id - Entity = "post" - CurrentTitle = post.Title - CurrentPermalink = Permalink.toString post.Permalink - Prior = post.PriorPermalinks |> List.map Permalink.toString |> Array.ofList + { Id = PostId.toString post.Id + Entity = "post" + CurrentTitle = post.Title + CurrentPermalink = Permalink.toString post.Permalink + Prior = post.PriorPermalinks |> List.map Permalink.toString |> Array.ofList } @@ -854,18 +899,18 @@ type ManageRevisionsModel = /// Create a revision model from a page static member fromPage webLog (pg : Page) = - { Id = PageId.toString pg.Id - Entity = "page" - CurrentTitle = pg.Title - Revisions = pg.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList + { Id = PageId.toString pg.Id + Entity = "page" + CurrentTitle = pg.Title + Revisions = pg.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList } /// Create a revision model from a post static member fromPost webLog (post : Post) = - { Id = PostId.toString post.Id - Entity = "post" - CurrentTitle = post.Title - Revisions = post.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList + { Id = PostId.toString post.Id + Entity = "post" + CurrentTitle = post.Title + Revisions = post.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList } @@ -913,18 +958,18 @@ type PostListItem = static member fromPost (webLog : WebLog) (post : Post) = let _, extra = WebLog.hostAndPath webLog let inTZ = WebLog.localTime webLog - { Id = PostId.toString post.Id - AuthorId = WebLogUserId.toString post.AuthorId - Status = PostStatus.toString post.Status - Title = post.Title - Permalink = Permalink.toString post.Permalink - PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable - UpdatedOn = inTZ post.UpdatedOn - Text = if extra = "" then post.Text else post.Text.Replace ("href=\"/", $"href=\"{extra}/") - CategoryIds = post.CategoryIds |> List.map CategoryId.toString - Tags = post.Tags - Episode = post.Episode - Metadata = post.Metadata + { Id = PostId.toString post.Id + AuthorId = WebLogUserId.toString post.AuthorId + Status = PostStatus.toString post.Status + Title = post.Title + Permalink = Permalink.toString post.Permalink + PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable + UpdatedOn = inTZ post.UpdatedOn + Text = if extra = "" then post.Text else post.Text.Replace ("href=\"/", $"href=\"{extra}/") + CategoryIds = post.CategoryIds |> List.map CategoryId.toString + Tags = post.Tags + Episode = post.Episode + Metadata = post.Metadata } @@ -986,15 +1031,15 @@ type SettingsModel = /// Create a settings model from a web log static member fromWebLog (webLog : WebLog) = - { Name = webLog.Name - Slug = webLog.Slug - Subtitle = defaultArg webLog.Subtitle "" - DefaultPage = webLog.DefaultPage - PostsPerPage = webLog.PostsPerPage - TimeZone = webLog.TimeZone - ThemeId = ThemeId.toString webLog.ThemeId - AutoHtmx = webLog.AutoHtmx - Uploads = UploadDestination.toString webLog.Uploads + { Name = webLog.Name + Slug = webLog.Slug + Subtitle = defaultArg webLog.Subtitle "" + DefaultPage = webLog.DefaultPage + PostsPerPage = webLog.PostsPerPage + TimeZone = webLog.TimeZone + ThemeId = ThemeId.toString webLog.ThemeId + AutoHtmx = webLog.AutoHtmx + Uploads = UploadDestination.toString webLog.Uploads } /// Update a web log with settings from the form diff --git a/src/MyWebLog/DotLiquidBespoke.fs b/src/MyWebLog/DotLiquidBespoke.fs index c72d8c5..cd3698d 100644 --- a/src/MyWebLog/DotLiquidBespoke.fs +++ b/src/MyWebLog/DotLiquidBespoke.fs @@ -227,11 +227,12 @@ let register () = typeof; typeof; typeof; typeof; typeof typeof; typeof; typeof; typeof // View models - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof + typeof // Framework types typeof; typeof; typeof; typeof typeof; typeof; typeof; typeof diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs index 78cf085..919e2bd 100644 --- a/src/MyWebLog/Handlers/Routes.fs +++ b/src/MyWebLog/Handlers/Routes.fs @@ -149,6 +149,7 @@ let router : HttpHandler = choose [ route "/new" >=> Upload.showNew ]) subRoute "/user" (choose [ + route "s" >=> User.all route "/my-info" >=> User.myInfo ]) ] diff --git a/src/MyWebLog/Handlers/User.fs b/src/MyWebLog/Handlers/User.fs index e4a5276..557dcb5 100644 --- a/src/MyWebLog/Handlers/User.fs +++ b/src/MyWebLog/Handlers/User.fs @@ -5,6 +5,8 @@ open System open System.Security.Cryptography open System.Text +// ~~ LOG ON / LOG OFF ~~ + /// Hash a password for a given user let hashedPassword (plainText : string) (email : string) (salt : Guid) = let allSalt = Array.concat [ salt.ToByteArray (); Encoding.UTF8.GetBytes email ] @@ -69,6 +71,24 @@ let logOff : HttpHandler = fun next ctx -> task { return! redirectToGet "" next ctx } +// ~~ ADMINISTRATION ~~ + +// GET /admin/users +let all : HttpHandler = fun next ctx -> task { + let data = ctx.Data + let! tmpl = TemplateCache.get "admin" "user-list-body" data + let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id + let hash = Hash.FromAnonymousObject {| + page_title = "User Administration" + csrf = ctx.CsrfTokenSet + web_log = ctx.WebLog + users = users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList + |} + return! + addToHash "user_list" (tmpl.Render hash) hash + |> adminView "user-list" next ctx +} + /// Display the user "my info" page, with information possibly filled in let private showMyInfo (user : WebLogUser) (hash : Hash) : HttpHandler = fun next ctx -> addToHash "page_title" "Edit Your Information" hash diff --git a/src/admin-theme/_layout.liquid b/src/admin-theme/_layout.liquid index d43ff56..cc6f527 100644 --- a/src/admin-theme/_layout.liquid +++ b/src/admin-theme/_layout.liquid @@ -17,6 +17,7 @@ {% endif %} {% if is_web_log_admin %} {{ "admin/categories" | nav_link: "Categories" }} + {{ "admin/users" | nav_link: "Users" }} {{ "admin/settings" | nav_link: "Settings" }} {% endif %} diff --git a/src/admin-theme/user-list-body.liquid b/src/admin-theme/user-list-body.liquid new file mode 100644 index 0000000..40da7a5 --- /dev/null +++ b/src/admin-theme/user-list-body.liquid @@ -0,0 +1,56 @@ +
+ +
+ {%- assign user_col = "col-12 col-md-4 col-xl-3" -%} + {%- assign email_col = "col-12 col-md-4 col-xl-4" -%} + {%- assign cre8_col = "d-none d-xl-block col-xl-2" -%} + {%- assign last_col = "col-12 col-md-4 col-xl-3" -%} + {%- assign badge = "ms-2 badge bg" -%} + {% for user in users -%} +
+
+ {{ user.preferred_name }} + {%- if user.access_level == "Administrator" %} + ADMINISTRATOR + {%- elsif user.access_level == "WebLogAdmin" %} + WEB LOG ADMIN + {%- elsif user.access_level == "Editor" %} + EDITOR + {%- elsif user.access_level == "Author" %} + AUTHOR + {%- endif %}
+ + {%- assign user_url_base = "admin/user/" | append: user.id -%} + + Edit + + + {%- assign user_del_link = user_url_base | append: "/delete" | relative_link -%} + + Delete + + +
+ +
+ {{ user.created_on | date: "MMMM d, yyyy" }} +
+
+ {% if user.last_seen_on %} + {{ user.last_seen_on | date: "MMMM d, yyyy" }} at + {{ user.last_seen_on | date: "h:mmtt" | downcase }} + {% else %} + -- + {% endif %} +
+
+ {%- endfor %} +
diff --git a/src/admin-theme/user-list.liquid b/src/admin-theme/user-list.liquid new file mode 100644 index 0000000..067953a --- /dev/null +++ b/src/admin-theme/user-list.liquid @@ -0,0 +1,20 @@ +

{{ page_title }}

+
+ + Add a New User + +
+ {%- assign user_col = "col-12 col-md-4 col-xl-3" -%} + {%- assign email_col = "col-12 col-md-4 col-xl-4" -%} + {%- assign cre8_col = "d-none d-xl-block col-xl-2" -%} + {%- assign last_col = "col-12 col-md-4 col-xl-3" -%} +
+
User; Details; Last Log On
+ +
Created
+
Last Log On
+
+
+ {{ user_list }} +
diff --git a/src/admin-theme/wwwroot/admin.js b/src/admin-theme/wwwroot/admin.js index 82014ab..20cea99 100644 --- a/src/admin-theme/wwwroot/admin.js +++ b/src/admin-theme/wwwroot/admin.js @@ -334,13 +334,10 @@ htmx.on("htmx:afterOnLoad", function (evt) { }) htmx.on("htmx:responseError", function (evt) { - /** @type {XMLHttpRequest} */ const xhr = evt.detail.xhr const hdrs = xhr.getAllResponseHeaders() - // Show messages if there were any in the response - if (hdrs.indexOf("x-message") >= 0) { - Admin.showMessage(evt.detail.xhr.getResponseHeader("x-message")) - } else { + // Show an error message if there were none in the response + if (hdrs.indexOf("x-message") < 0) { Admin.showMessage(`danger|||${xhr.status}: ${xhr.statusText}`) } })