First cut of user admin page (#19)

This commit is contained in:
Daniel J. Summers 2022-07-19 22:51:51 -04:00
parent 1e987fdf72
commit 41ae1d8dad
17 changed files with 605 additions and 464 deletions

View File

@ -105,47 +105,47 @@ module Map =
/// Create a category from the current row in the given data reader /// Create a category from the current row in the given data reader
let toCategory rdr : Category = let toCategory rdr : Category =
{ Id = toCategoryId rdr { Id = toCategoryId rdr
WebLogId = getString "web_log_id" rdr |> WebLogId WebLogId = getString "web_log_id" rdr |> WebLogId
Name = getString "name" rdr Name = getString "name" rdr
Slug = getString "slug" rdr Slug = getString "slug" rdr
Description = tryString "description" rdr Description = tryString "description" rdr
ParentId = tryString "parent_id" rdr |> Option.map CategoryId ParentId = tryString "parent_id" rdr |> Option.map CategoryId
} }
/// Create a custom feed from the current row in the given data reader /// Create a custom feed from the current row in the given data reader
let toCustomFeed rdr : CustomFeed = let toCustomFeed rdr : CustomFeed =
{ Id = getString "id" rdr |> CustomFeedId { Id = getString "id" rdr |> CustomFeedId
Source = getString "source" rdr |> CustomFeedSource.parse Source = getString "source" rdr |> CustomFeedSource.parse
Path = getString "path" rdr |> Permalink Path = getString "path" rdr |> Permalink
Podcast = Podcast =
if rdr.IsDBNull (rdr.GetOrdinal "title") then if rdr.IsDBNull (rdr.GetOrdinal "title") then
None None
else else
Some { Some {
Title = getString "title" rdr Title = getString "title" rdr
Subtitle = tryString "subtitle" rdr Subtitle = tryString "subtitle" rdr
ItemsInFeed = getInt "items_in_feed" rdr ItemsInFeed = getInt "items_in_feed" rdr
Summary = getString "summary" rdr Summary = getString "summary" rdr
DisplayedAuthor = getString "displayed_author" rdr DisplayedAuthor = getString "displayed_author" rdr
Email = getString "email" rdr Email = getString "email" rdr
ImageUrl = getString "image_url" rdr |> Permalink ImageUrl = getString "image_url" rdr |> Permalink
AppleCategory = getString "apple_category" rdr AppleCategory = getString "apple_category" rdr
AppleSubcategory = tryString "apple_subcategory" rdr AppleSubcategory = tryString "apple_subcategory" rdr
Explicit = getString "explicit" rdr |> ExplicitRating.parse Explicit = getString "explicit" rdr |> ExplicitRating.parse
DefaultMediaType = tryString "default_media_type" rdr DefaultMediaType = tryString "default_media_type" rdr
MediaBaseUrl = tryString "media_base_url" rdr MediaBaseUrl = tryString "media_base_url" rdr
PodcastGuid = tryGuid "podcast_guid" rdr PodcastGuid = tryGuid "podcast_guid" rdr
FundingUrl = tryString "funding_url" rdr FundingUrl = tryString "funding_url" rdr
FundingText = tryString "funding_text" rdr FundingText = tryString "funding_text" rdr
Medium = tryString "medium" rdr |> Option.map PodcastMedium.parse Medium = tryString "medium" rdr |> Option.map PodcastMedium.parse
} }
} }
/// Create a meta item from the current row in the given data reader /// Create a meta item from the current row in the given data reader
let toMetaItem rdr : MetaItem = let toMetaItem rdr : MetaItem =
{ Name = getString "name" rdr { Name = getString "name" rdr
Value = getString "value" rdr Value = getString "value" rdr
} }
/// Create a permalink from the current row in the given data reader /// 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 /// Create a revision from the current row in the given data reader
let toRevision rdr : Revision = let toRevision rdr : Revision =
{ AsOf = getDateTime "as_of" rdr { AsOf = getDateTime "as_of" rdr
Text = getString "revision_text" rdr |> MarkupText.parse Text = getString "revision_text" rdr |> MarkupText.parse
} }
/// Create a tag mapping from the current row in the given data reader /// Create a tag mapping from the current row in the given data reader
let toTagMap rdr : TagMap = let toTagMap rdr : TagMap =
{ Id = getString "id" rdr |> TagMapId { Id = getString "id" rdr |> TagMapId
WebLogId = getString "web_log_id" rdr |> WebLogId WebLogId = getString "web_log_id" rdr |> WebLogId
Tag = getString "tag" rdr Tag = getString "tag" rdr
UrlValue = getString "url_value" rdr UrlValue = getString "url_value" rdr
} }
/// Create a theme from the current row in the given data reader (excludes templates) /// Create a theme from the current row in the given data reader (excludes templates)
@ -236,15 +236,15 @@ module Map =
dataStream.ToArray () dataStream.ToArray ()
else else
[||] [||]
{ Id = ThemeAssetId (ThemeId (getString "theme_id" rdr), getString "path" rdr) { Id = ThemeAssetId (ThemeId (getString "theme_id" rdr), getString "path" rdr)
UpdatedOn = getDateTime "updated_on" rdr UpdatedOn = getDateTime "updated_on" rdr
Data = assetData Data = assetData
} }
/// Create a theme template from the current row in the given data reader /// Create a theme template from the current row in the given data reader
let toThemeTemplate rdr : ThemeTemplate = let toThemeTemplate rdr : ThemeTemplate =
{ Name = getString "name" rdr { Name = getString "name" rdr
Text = getString "template" rdr Text = getString "template" rdr
} }
/// Create an uploaded file from the current row in the given data reader /// Create an uploaded file from the current row in the given data reader
@ -257,51 +257,51 @@ module Map =
dataStream.ToArray () dataStream.ToArray ()
else else
[||] [||]
{ Id = getString "id" rdr |> UploadId { Id = getString "id" rdr |> UploadId
WebLogId = getString "web_log_id" rdr |> WebLogId WebLogId = getString "web_log_id" rdr |> WebLogId
Path = getString "path" rdr |> Permalink Path = getString "path" rdr |> Permalink
UpdatedOn = getDateTime "updated_on" rdr UpdatedOn = getDateTime "updated_on" rdr
Data = data Data = data
} }
/// Create a web log from the current row in the given data reader /// Create a web log from the current row in the given data reader
let toWebLog rdr : WebLog = let toWebLog rdr : WebLog =
{ Id = getString "id" rdr |> WebLogId { Id = getString "id" rdr |> WebLogId
Name = getString "name" rdr Name = getString "name" rdr
Slug = getString "slug" rdr Slug = getString "slug" rdr
Subtitle = tryString "subtitle" rdr Subtitle = tryString "subtitle" rdr
DefaultPage = getString "default_page" rdr DefaultPage = getString "default_page" rdr
PostsPerPage = getInt "posts_per_page" rdr PostsPerPage = getInt "posts_per_page" rdr
ThemeId = getString "theme_id" rdr |> ThemeId ThemeId = getString "theme_id" rdr |> ThemeId
UrlBase = getString "url_base" rdr UrlBase = getString "url_base" rdr
TimeZone = getString "time_zone" rdr TimeZone = getString "time_zone" rdr
AutoHtmx = getBoolean "auto_htmx" rdr AutoHtmx = getBoolean "auto_htmx" rdr
Uploads = getString "uploads" rdr |> UploadDestination.parse Uploads = getString "uploads" rdr |> UploadDestination.parse
Rss = { Rss = {
IsFeedEnabled = getBoolean "is_feed_enabled" rdr IsFeedEnabled = getBoolean "is_feed_enabled" rdr
FeedName = getString "feed_name" rdr FeedName = getString "feed_name" rdr
ItemsInFeed = tryInt "items_in_feed" rdr ItemsInFeed = tryInt "items_in_feed" rdr
IsCategoryEnabled = getBoolean "is_category_enabled" rdr IsCategoryEnabled = getBoolean "is_category_enabled" rdr
IsTagEnabled = getBoolean "is_tag_enabled" rdr IsTagEnabled = getBoolean "is_tag_enabled" rdr
Copyright = tryString "copyright" rdr Copyright = tryString "copyright" rdr
CustomFeeds = [] CustomFeeds = []
} }
} }
/// Create a web log user from the current row in the given data reader /// Create a web log user from the current row in the given data reader
let toWebLogUser rdr : WebLogUser = let toWebLogUser rdr : WebLogUser =
{ Id = getString "id" rdr |> WebLogUserId { Id = getString "id" rdr |> WebLogUserId
WebLogId = getString "web_log_id" rdr |> WebLogId WebLogId = getString "web_log_id" rdr |> WebLogId
Email = getString "email" rdr Email = getString "email" rdr
FirstName = getString "first_name" rdr FirstName = getString "first_name" rdr
LastName = getString "last_name" rdr LastName = getString "last_name" rdr
PreferredName = getString "preferred_name" rdr PreferredName = getString "preferred_name" rdr
PasswordHash = getString "password_hash" rdr PasswordHash = getString "password_hash" rdr
Salt = getGuid "salt" rdr Salt = getGuid "salt" rdr
Url = tryString "url" rdr Url = tryString "url" rdr
AccessLevel = getString "access_level" rdr |> AccessLevel.parse AccessLevel = getString "access_level" rdr |> AccessLevel.parse
CreatedOn = getDateTime "created_on" rdr CreatedOn = getDateTime "created_on" rdr
LastSeenOn = tryDateTime "last_seen_on" rdr LastSeenOn = tryDateTime "last_seen_on" rdr
} }
/// Add a possibly-missing parameter, substituting null for None /// Add a possibly-missing parameter, substituting null for None

View File

@ -10,12 +10,12 @@ type SQLiteCategoryData (conn : SqliteConnection) =
/// Add parameters for category INSERT or UPDATE statements /// Add parameters for category INSERT or UPDATE statements
let addCategoryParameters (cmd : SqliteCommand) (cat : Category) = let addCategoryParameters (cmd : SqliteCommand) (cat : Category) =
[ cmd.Parameters.AddWithValue ("@id", CategoryId.toString cat.Id) [ cmd.Parameters.AddWithValue ("@id", CategoryId.toString cat.Id)
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString cat.WebLogId) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString cat.WebLogId)
cmd.Parameters.AddWithValue ("@name", cat.Name) cmd.Parameters.AddWithValue ("@name", cat.Name)
cmd.Parameters.AddWithValue ("@slug", cat.Slug) cmd.Parameters.AddWithValue ("@slug", cat.Slug)
cmd.Parameters.AddWithValue ("@description", maybe cat.Description) cmd.Parameters.AddWithValue ("@description", maybe cat.Description)
cmd.Parameters.AddWithValue ("@parentId", maybe (cat.ParentId |> Option.map CategoryId.toString)) cmd.Parameters.AddWithValue ("@parentId", maybe (cat.ParentId |> Option.map CategoryId.toString))
] |> ignore ] |> ignore
/// Add a category /// Add a category

View File

@ -12,16 +12,16 @@ type SQLitePageData (conn : SqliteConnection) =
/// Add parameters for page INSERT or UPDATE statements /// Add parameters for page INSERT or UPDATE statements
let addPageParameters (cmd : SqliteCommand) (page : Page) = let addPageParameters (cmd : SqliteCommand) (page : Page) =
[ cmd.Parameters.AddWithValue ("@id", PageId.toString page.Id) [ cmd.Parameters.AddWithValue ("@id", PageId.toString page.Id)
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString page.WebLogId) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString page.WebLogId)
cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString page.AuthorId) cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString page.AuthorId)
cmd.Parameters.AddWithValue ("@title", page.Title) cmd.Parameters.AddWithValue ("@title", page.Title)
cmd.Parameters.AddWithValue ("@permalink", Permalink.toString page.Permalink) cmd.Parameters.AddWithValue ("@permalink", Permalink.toString page.Permalink)
cmd.Parameters.AddWithValue ("@publishedOn", page.PublishedOn) cmd.Parameters.AddWithValue ("@publishedOn", page.PublishedOn)
cmd.Parameters.AddWithValue ("@updatedOn", page.UpdatedOn) cmd.Parameters.AddWithValue ("@updatedOn", page.UpdatedOn)
cmd.Parameters.AddWithValue ("@isInPageList", page.IsInPageList) cmd.Parameters.AddWithValue ("@isInPageList", page.IsInPageList)
cmd.Parameters.AddWithValue ("@template", maybe page.Template) cmd.Parameters.AddWithValue ("@template", maybe page.Template)
cmd.Parameters.AddWithValue ("@text", page.Text) cmd.Parameters.AddWithValue ("@text", page.Text)
] |> ignore ] |> ignore
/// Append meta items to a page /// Append meta items to a page

View File

@ -13,37 +13,37 @@ type SQLitePostData (conn : SqliteConnection) =
/// Add parameters for post INSERT or UPDATE statements /// Add parameters for post INSERT or UPDATE statements
let addPostParameters (cmd : SqliteCommand) (post : Post) = let addPostParameters (cmd : SqliteCommand) (post : Post) =
[ cmd.Parameters.AddWithValue ("@id", PostId.toString post.Id) [ cmd.Parameters.AddWithValue ("@id", PostId.toString post.Id)
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString post.WebLogId) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString post.WebLogId)
cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString post.AuthorId) cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString post.AuthorId)
cmd.Parameters.AddWithValue ("@status", PostStatus.toString post.Status) cmd.Parameters.AddWithValue ("@status", PostStatus.toString post.Status)
cmd.Parameters.AddWithValue ("@title", post.Title) cmd.Parameters.AddWithValue ("@title", post.Title)
cmd.Parameters.AddWithValue ("@permalink", Permalink.toString post.Permalink) cmd.Parameters.AddWithValue ("@permalink", Permalink.toString post.Permalink)
cmd.Parameters.AddWithValue ("@publishedOn", maybe post.PublishedOn) cmd.Parameters.AddWithValue ("@publishedOn", maybe post.PublishedOn)
cmd.Parameters.AddWithValue ("@updatedOn", post.UpdatedOn) cmd.Parameters.AddWithValue ("@updatedOn", post.UpdatedOn)
cmd.Parameters.AddWithValue ("@template", maybe post.Template) cmd.Parameters.AddWithValue ("@template", maybe post.Template)
cmd.Parameters.AddWithValue ("@text", post.Text) cmd.Parameters.AddWithValue ("@text", post.Text)
] |> ignore ] |> ignore
/// Add parameters for episode INSERT or UPDATE statements /// Add parameters for episode INSERT or UPDATE statements
let addEpisodeParameters (cmd : SqliteCommand) (ep : Episode) = let addEpisodeParameters (cmd : SqliteCommand) (ep : Episode) =
[ cmd.Parameters.AddWithValue ("@media", ep.Media) [ cmd.Parameters.AddWithValue ("@media", ep.Media)
cmd.Parameters.AddWithValue ("@length", ep.Length) cmd.Parameters.AddWithValue ("@length", ep.Length)
cmd.Parameters.AddWithValue ("@duration", maybe ep.Duration) cmd.Parameters.AddWithValue ("@duration", maybe ep.Duration)
cmd.Parameters.AddWithValue ("@mediaType", maybe ep.MediaType) cmd.Parameters.AddWithValue ("@mediaType", maybe ep.MediaType)
cmd.Parameters.AddWithValue ("@imageUrl", maybe ep.ImageUrl) cmd.Parameters.AddWithValue ("@imageUrl", maybe ep.ImageUrl)
cmd.Parameters.AddWithValue ("@subtitle", maybe ep.Subtitle) cmd.Parameters.AddWithValue ("@subtitle", maybe ep.Subtitle)
cmd.Parameters.AddWithValue ("@explicit", maybe (ep.Explicit |> Option.map ExplicitRating.toString)) cmd.Parameters.AddWithValue ("@explicit", maybe (ep.Explicit |> Option.map ExplicitRating.toString))
cmd.Parameters.AddWithValue ("@chapterFile", maybe ep.ChapterFile) cmd.Parameters.AddWithValue ("@chapterFile", maybe ep.ChapterFile)
cmd.Parameters.AddWithValue ("@chapterType", maybe ep.ChapterType) cmd.Parameters.AddWithValue ("@chapterType", maybe ep.ChapterType)
cmd.Parameters.AddWithValue ("@transcriptUrl", maybe ep.TranscriptUrl) cmd.Parameters.AddWithValue ("@transcriptUrl", maybe ep.TranscriptUrl)
cmd.Parameters.AddWithValue ("@transcriptType", maybe ep.TranscriptType) cmd.Parameters.AddWithValue ("@transcriptType", maybe ep.TranscriptType)
cmd.Parameters.AddWithValue ("@transcriptLang", maybe ep.TranscriptLang) cmd.Parameters.AddWithValue ("@transcriptLang", maybe ep.TranscriptLang)
cmd.Parameters.AddWithValue ("@transcriptCaptions", maybe ep.TranscriptCaptions) cmd.Parameters.AddWithValue ("@transcriptCaptions", maybe ep.TranscriptCaptions)
cmd.Parameters.AddWithValue ("@seasonNumber", maybe ep.SeasonNumber) cmd.Parameters.AddWithValue ("@seasonNumber", maybe ep.SeasonNumber)
cmd.Parameters.AddWithValue ("@seasonDescription", maybe ep.SeasonDescription) cmd.Parameters.AddWithValue ("@seasonDescription", maybe ep.SeasonDescription)
cmd.Parameters.AddWithValue ("@episodeNumber", maybe (ep.EpisodeNumber |> Option.map string)) cmd.Parameters.AddWithValue ("@episodeNumber", maybe (ep.EpisodeNumber |> Option.map string))
cmd.Parameters.AddWithValue ("@episodeDescription", maybe ep.EpisodeDescription) cmd.Parameters.AddWithValue ("@episodeDescription", maybe ep.EpisodeDescription)
] |> ignore ] |> ignore
/// Append category IDs, tags, and meta items to a post /// Append category IDs, tags, and meta items to a post

View File

@ -10,11 +10,11 @@ type SQLiteUploadData (conn : SqliteConnection) =
/// Add parameters for uploaded file INSERT and UPDATE statements /// Add parameters for uploaded file INSERT and UPDATE statements
let addUploadParameters (cmd : SqliteCommand) (upload : Upload) = let addUploadParameters (cmd : SqliteCommand) (upload : Upload) =
[ cmd.Parameters.AddWithValue ("@id", UploadId.toString upload.Id) [ cmd.Parameters.AddWithValue ("@id", UploadId.toString upload.Id)
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString upload.WebLogId) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString upload.WebLogId)
cmd.Parameters.AddWithValue ("@path", Permalink.toString upload.Path) cmd.Parameters.AddWithValue ("@path", Permalink.toString upload.Path)
cmd.Parameters.AddWithValue ("@updatedOn", upload.UpdatedOn) cmd.Parameters.AddWithValue ("@updatedOn", upload.UpdatedOn)
cmd.Parameters.AddWithValue ("@dataLength", upload.Data.Length) cmd.Parameters.AddWithValue ("@dataLength", upload.Data.Length)
] |> ignore ] |> ignore
/// Save an uploaded file /// Save an uploaded file

View File

@ -15,57 +15,57 @@ type SQLiteWebLogData (conn : SqliteConnection) =
/// Add parameters for web log INSERT or web log/RSS options UPDATE statements /// Add parameters for web log INSERT or web log/RSS options UPDATE statements
let addWebLogRssParameters (cmd : SqliteCommand) (webLog : WebLog) = let addWebLogRssParameters (cmd : SqliteCommand) (webLog : WebLog) =
[ cmd.Parameters.AddWithValue ("@isFeedEnabled", webLog.Rss.IsFeedEnabled) [ cmd.Parameters.AddWithValue ("@isFeedEnabled", webLog.Rss.IsFeedEnabled)
cmd.Parameters.AddWithValue ("@feedName", webLog.Rss.FeedName) cmd.Parameters.AddWithValue ("@feedName", webLog.Rss.FeedName)
cmd.Parameters.AddWithValue ("@itemsInFeed", maybe webLog.Rss.ItemsInFeed) cmd.Parameters.AddWithValue ("@itemsInFeed", maybe webLog.Rss.ItemsInFeed)
cmd.Parameters.AddWithValue ("@isCategoryEnabled", webLog.Rss.IsCategoryEnabled) cmd.Parameters.AddWithValue ("@isCategoryEnabled", webLog.Rss.IsCategoryEnabled)
cmd.Parameters.AddWithValue ("@isTagEnabled", webLog.Rss.IsTagEnabled) cmd.Parameters.AddWithValue ("@isTagEnabled", webLog.Rss.IsTagEnabled)
cmd.Parameters.AddWithValue ("@copyright", maybe webLog.Rss.Copyright) cmd.Parameters.AddWithValue ("@copyright", maybe webLog.Rss.Copyright)
] |> ignore ] |> ignore
/// Add parameters for web log INSERT or UPDATE statements /// Add parameters for web log INSERT or UPDATE statements
let addWebLogParameters (cmd : SqliteCommand) (webLog : WebLog) = let addWebLogParameters (cmd : SqliteCommand) (webLog : WebLog) =
[ cmd.Parameters.AddWithValue ("@id", WebLogId.toString webLog.Id) [ cmd.Parameters.AddWithValue ("@id", WebLogId.toString webLog.Id)
cmd.Parameters.AddWithValue ("@name", webLog.Name) cmd.Parameters.AddWithValue ("@name", webLog.Name)
cmd.Parameters.AddWithValue ("@slug", webLog.Slug) cmd.Parameters.AddWithValue ("@slug", webLog.Slug)
cmd.Parameters.AddWithValue ("@subtitle", maybe webLog.Subtitle) cmd.Parameters.AddWithValue ("@subtitle", maybe webLog.Subtitle)
cmd.Parameters.AddWithValue ("@defaultPage", webLog.DefaultPage) cmd.Parameters.AddWithValue ("@defaultPage", webLog.DefaultPage)
cmd.Parameters.AddWithValue ("@postsPerPage", webLog.PostsPerPage) cmd.Parameters.AddWithValue ("@postsPerPage", webLog.PostsPerPage)
cmd.Parameters.AddWithValue ("@themeId", ThemeId.toString webLog.ThemeId) cmd.Parameters.AddWithValue ("@themeId", ThemeId.toString webLog.ThemeId)
cmd.Parameters.AddWithValue ("@urlBase", webLog.UrlBase) cmd.Parameters.AddWithValue ("@urlBase", webLog.UrlBase)
cmd.Parameters.AddWithValue ("@timeZone", webLog.TimeZone) cmd.Parameters.AddWithValue ("@timeZone", webLog.TimeZone)
cmd.Parameters.AddWithValue ("@autoHtmx", webLog.AutoHtmx) cmd.Parameters.AddWithValue ("@autoHtmx", webLog.AutoHtmx)
cmd.Parameters.AddWithValue ("@uploads", UploadDestination.toString webLog.Uploads) cmd.Parameters.AddWithValue ("@uploads", UploadDestination.toString webLog.Uploads)
] |> ignore ] |> ignore
addWebLogRssParameters cmd webLog addWebLogRssParameters cmd webLog
/// Add parameters for custom feed INSERT or UPDATE statements /// Add parameters for custom feed INSERT or UPDATE statements
let addCustomFeedParameters (cmd : SqliteCommand) webLogId (feed : CustomFeed) = let addCustomFeedParameters (cmd : SqliteCommand) webLogId (feed : CustomFeed) =
[ cmd.Parameters.AddWithValue ("@id", CustomFeedId.toString feed.Id) [ cmd.Parameters.AddWithValue ("@id", CustomFeedId.toString feed.Id)
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId)
cmd.Parameters.AddWithValue ("@source", CustomFeedSource.toString feed.Source) cmd.Parameters.AddWithValue ("@source", CustomFeedSource.toString feed.Source)
cmd.Parameters.AddWithValue ("@path", Permalink.toString feed.Path) cmd.Parameters.AddWithValue ("@path", Permalink.toString feed.Path)
] |> ignore ] |> ignore
/// Add parameters for podcast INSERT or UPDATE statements /// Add parameters for podcast INSERT or UPDATE statements
let addPodcastParameters (cmd : SqliteCommand) feedId (podcast : PodcastOptions) = let addPodcastParameters (cmd : SqliteCommand) feedId (podcast : PodcastOptions) =
[ cmd.Parameters.AddWithValue ("@feedId", CustomFeedId.toString feedId) [ cmd.Parameters.AddWithValue ("@feedId", CustomFeedId.toString feedId)
cmd.Parameters.AddWithValue ("@title", podcast.Title) cmd.Parameters.AddWithValue ("@title", podcast.Title)
cmd.Parameters.AddWithValue ("@subtitle", maybe podcast.Subtitle) cmd.Parameters.AddWithValue ("@subtitle", maybe podcast.Subtitle)
cmd.Parameters.AddWithValue ("@itemsInFeed", podcast.ItemsInFeed) cmd.Parameters.AddWithValue ("@itemsInFeed", podcast.ItemsInFeed)
cmd.Parameters.AddWithValue ("@summary", podcast.Summary) cmd.Parameters.AddWithValue ("@summary", podcast.Summary)
cmd.Parameters.AddWithValue ("@displayedAuthor", podcast.DisplayedAuthor) cmd.Parameters.AddWithValue ("@displayedAuthor", podcast.DisplayedAuthor)
cmd.Parameters.AddWithValue ("@email", podcast.Email) cmd.Parameters.AddWithValue ("@email", podcast.Email)
cmd.Parameters.AddWithValue ("@imageUrl", Permalink.toString podcast.ImageUrl) cmd.Parameters.AddWithValue ("@imageUrl", Permalink.toString podcast.ImageUrl)
cmd.Parameters.AddWithValue ("@appleCategory", podcast.AppleCategory) cmd.Parameters.AddWithValue ("@appleCategory", podcast.AppleCategory)
cmd.Parameters.AddWithValue ("@appleSubcategory", maybe podcast.AppleSubcategory) cmd.Parameters.AddWithValue ("@appleSubcategory", maybe podcast.AppleSubcategory)
cmd.Parameters.AddWithValue ("@explicit", ExplicitRating.toString podcast.Explicit) cmd.Parameters.AddWithValue ("@explicit", ExplicitRating.toString podcast.Explicit)
cmd.Parameters.AddWithValue ("@defaultMediaType", maybe podcast.DefaultMediaType) cmd.Parameters.AddWithValue ("@defaultMediaType", maybe podcast.DefaultMediaType)
cmd.Parameters.AddWithValue ("@mediaBaseUrl", maybe podcast.MediaBaseUrl) cmd.Parameters.AddWithValue ("@mediaBaseUrl", maybe podcast.MediaBaseUrl)
cmd.Parameters.AddWithValue ("@podcastGuid", maybe podcast.PodcastGuid) cmd.Parameters.AddWithValue ("@podcastGuid", maybe podcast.PodcastGuid)
cmd.Parameters.AddWithValue ("@fundingUrl", maybe podcast.FundingUrl) cmd.Parameters.AddWithValue ("@fundingUrl", maybe podcast.FundingUrl)
cmd.Parameters.AddWithValue ("@fundingText", maybe podcast.FundingText) cmd.Parameters.AddWithValue ("@fundingText", maybe podcast.FundingText)
cmd.Parameters.AddWithValue ("@medium", maybe (podcast.Medium |> Option.map PodcastMedium.toString)) cmd.Parameters.AddWithValue ("@medium", maybe (podcast.Medium |> Option.map PodcastMedium.toString))
] |> ignore ] |> ignore
/// Get the current custom feeds for a web log /// Get the current custom feeds for a web log

View File

@ -12,18 +12,18 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
/// Add parameters for web log user INSERT or UPDATE statements /// Add parameters for web log user INSERT or UPDATE statements
let addWebLogUserParameters (cmd : SqliteCommand) (user : WebLogUser) = let addWebLogUserParameters (cmd : SqliteCommand) (user : WebLogUser) =
[ cmd.Parameters.AddWithValue ("@id", WebLogUserId.toString user.Id) [ cmd.Parameters.AddWithValue ("@id", WebLogUserId.toString user.Id)
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString user.WebLogId) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString user.WebLogId)
cmd.Parameters.AddWithValue ("@email", user.Email) cmd.Parameters.AddWithValue ("@email", user.Email)
cmd.Parameters.AddWithValue ("@firstName", user.FirstName) cmd.Parameters.AddWithValue ("@firstName", user.FirstName)
cmd.Parameters.AddWithValue ("@lastName", user.LastName) cmd.Parameters.AddWithValue ("@lastName", user.LastName)
cmd.Parameters.AddWithValue ("@preferredName", user.PreferredName) cmd.Parameters.AddWithValue ("@preferredName", user.PreferredName)
cmd.Parameters.AddWithValue ("@passwordHash", user.PasswordHash) cmd.Parameters.AddWithValue ("@passwordHash", user.PasswordHash)
cmd.Parameters.AddWithValue ("@salt", user.Salt) cmd.Parameters.AddWithValue ("@salt", user.Salt)
cmd.Parameters.AddWithValue ("@url", maybe user.Url) cmd.Parameters.AddWithValue ("@url", maybe user.Url)
cmd.Parameters.AddWithValue ("@accessLevel", AccessLevel.toString user.AccessLevel) cmd.Parameters.AddWithValue ("@accessLevel", AccessLevel.toString user.AccessLevel)
cmd.Parameters.AddWithValue ("@createdOn", user.CreatedOn) cmd.Parameters.AddWithValue ("@createdOn", user.CreatedOn)
cmd.Parameters.AddWithValue ("@lastSeenOn", maybe user.LastSeenOn) cmd.Parameters.AddWithValue ("@lastSeenOn", maybe user.LastSeenOn)
] |> ignore ] |> ignore
// IMPLEMENTATION FUNCTIONS // IMPLEMENTATION FUNCTIONS

View File

@ -30,12 +30,12 @@ module Category =
/// An empty category /// An empty category
let empty = let empty =
{ Id = CategoryId.empty { Id = CategoryId.empty
WebLogId = WebLogId.empty WebLogId = WebLogId.empty
Name = "" Name = ""
Slug = "" Slug = ""
Description = None Description = None
ParentId = None ParentId = None
} }
@ -75,15 +75,15 @@ module Comment =
/// An empty comment /// An empty comment
let empty = let empty =
{ Id = CommentId.empty { Id = CommentId.empty
PostId = PostId.empty PostId = PostId.empty
InReplyToId = None InReplyToId = None
Name = "" Name = ""
Email = "" Email = ""
Url = None Url = None
Status = Pending Status = Pending
PostedOn = DateTime.UtcNow PostedOn = DateTime.UtcNow
Text = "" Text = ""
} }
@ -135,19 +135,19 @@ module Page =
/// An empty page /// An empty page
let empty = let empty =
{ Id = PageId.empty { Id = PageId.empty
WebLogId = WebLogId.empty WebLogId = WebLogId.empty
AuthorId = WebLogUserId.empty AuthorId = WebLogUserId.empty
Title = "" Title = ""
Permalink = Permalink.empty Permalink = Permalink.empty
PublishedOn = DateTime.MinValue PublishedOn = DateTime.MinValue
UpdatedOn = DateTime.MinValue UpdatedOn = DateTime.MinValue
IsInPageList = false IsInPageList = false
Template = None Template = None
Text = "" Text = ""
Metadata = [] Metadata = []
PriorPermalinks = [] PriorPermalinks = []
Revisions = [] Revisions = []
} }
@ -208,22 +208,22 @@ module Post =
/// An empty post /// An empty post
let empty = let empty =
{ Id = PostId.empty { Id = PostId.empty
WebLogId = WebLogId.empty WebLogId = WebLogId.empty
AuthorId = WebLogUserId.empty AuthorId = WebLogUserId.empty
Status = Draft Status = Draft
Title = "" Title = ""
Permalink = Permalink.empty Permalink = Permalink.empty
PublishedOn = None PublishedOn = None
UpdatedOn = DateTime.MinValue UpdatedOn = DateTime.MinValue
Text = "" Text = ""
Template = None Template = None
CategoryIds = [] CategoryIds = []
Tags = [] Tags = []
Episode = None Episode = None
Metadata = [] Metadata = []
PriorPermalinks = [] PriorPermalinks = []
Revisions = [] Revisions = []
} }
@ -247,10 +247,10 @@ module TagMap =
/// An empty tag mapping /// An empty tag mapping
let empty = let empty =
{ Id = TagMapId.empty { Id = TagMapId.empty
WebLogId = WebLogId.empty WebLogId = WebLogId.empty
Tag = "" Tag = ""
UrlValue = "" UrlValue = ""
} }
@ -274,10 +274,10 @@ module Theme =
/// An empty theme /// An empty theme
let empty = let empty =
{ Id = ThemeId "" { Id = ThemeId ""
Name = "" Name = ""
Version = "" Version = ""
Templates = [] Templates = []
} }
@ -299,9 +299,9 @@ module ThemeAsset =
/// An empty theme asset /// An empty theme asset
let empty = let empty =
{ Id = ThemeAssetId (ThemeId "", "") { Id = ThemeAssetId (ThemeId "", "")
UpdatedOn = DateTime.MinValue UpdatedOn = DateTime.MinValue
Data = [||] Data = [||]
} }
@ -327,13 +327,13 @@ type Upload =
module Upload = module Upload =
/// An empty upload /// An empty upload
let empty = { let empty =
Id = UploadId.empty { Id = UploadId.empty
WebLogId = WebLogId.empty WebLogId = WebLogId.empty
Path = Permalink.empty Path = Permalink.empty
UpdatedOn = DateTime.MinValue UpdatedOn = DateTime.MinValue
Data = [||] Data = [||]
} }
/// A web log /// A web log
@ -381,18 +381,18 @@ module WebLog =
/// An empty web log /// An empty web log
let empty = let empty =
{ Id = WebLogId.empty { Id = WebLogId.empty
Name = "" Name = ""
Slug = "" Slug = ""
Subtitle = None Subtitle = None
DefaultPage = "" DefaultPage = ""
PostsPerPage = 10 PostsPerPage = 10
ThemeId = ThemeId "default" ThemeId = ThemeId "default"
UrlBase = "" UrlBase = ""
TimeZone = "" TimeZone = ""
Rss = RssOptions.empty Rss = RssOptions.empty
AutoHtmx = false AutoHtmx = false
Uploads = Database Uploads = Database
} }
/// Get the host (including scheme) and extra path from the URL base /// Get the host (including scheme) and extra path from the URL base
@ -461,18 +461,18 @@ module WebLogUser =
/// An empty web log user /// An empty web log user
let empty = let empty =
{ Id = WebLogUserId.empty { Id = WebLogUserId.empty
WebLogId = WebLogId.empty WebLogId = WebLogId.empty
Email = "" Email = ""
FirstName = "" FirstName = ""
LastName = "" LastName = ""
PreferredName = "" PreferredName = ""
PasswordHash = "" PasswordHash = ""
Salt = Guid.Empty Salt = Guid.Empty
Url = None Url = None
AccessLevel = Author AccessLevel = Author
CreatedOn = DateTime.UnixEpoch CreatedOn = DateTime.UnixEpoch
LastSeenOn = None LastSeenOn = None
} }
/// Get the user's displayed name /// Get the user's displayed name

View File

@ -28,10 +28,10 @@ module AccessLevel =
/// Weightings for access levels /// Weightings for access levels
let private weights = let private weights =
[ Author, 10 [ Author, 10
Editor, 20 Editor, 20
WebLogAdmin, 30 WebLogAdmin, 30
Administrator, 40 Administrator, 40
] ]
|> Map.ofList |> Map.ofList
@ -195,25 +195,25 @@ type Episode =
module Episode = module Episode =
/// An empty episode /// An empty episode
let empty = { let empty =
Media = "" { Media = ""
Length = 0L Length = 0L
Duration = None Duration = None
MediaType = None MediaType = None
ImageUrl = None ImageUrl = None
Subtitle = None Subtitle = None
Explicit = None Explicit = None
ChapterFile = None ChapterFile = None
ChapterType = None ChapterType = None
TranscriptUrl = None TranscriptUrl = None
TranscriptType = None TranscriptType = None
TranscriptLang = None TranscriptLang = None
TranscriptCaptions = None TranscriptCaptions = None
SeasonNumber = None SeasonNumber = None
SeasonDescription = None SeasonDescription = None
EpisodeNumber = None EpisodeNumber = None
EpisodeDescription = None EpisodeDescription = None
} }
open Markdig open Markdig
@ -285,8 +285,8 @@ module Revision =
/// An empty revision /// An empty revision
let empty = let empty =
{ AsOf = DateTime.UtcNow { AsOf = DateTime.UtcNow
Text = Html "" Text = Html ""
} }
@ -505,10 +505,10 @@ module CustomFeed =
/// An empty custom feed /// An empty custom feed
let empty = let empty =
{ Id = CustomFeedId "" { Id = CustomFeedId ""
Source = Category (CategoryId "") Source = Category (CategoryId "")
Path = Permalink "" Path = Permalink ""
Podcast = None Podcast = None
} }
@ -542,13 +542,13 @@ module RssOptions =
/// An empty set of RSS options /// An empty set of RSS options
let empty = let empty =
{ IsFeedEnabled = true { IsFeedEnabled = true
FeedName = "feed.xml" FeedName = "feed.xml"
ItemsInFeed = None ItemsInFeed = None
IsCategoryEnabled = true IsCategoryEnabled = true
IsTagEnabled = true IsTagEnabled = true
Copyright = None Copyright = None
CustomFeeds = [] CustomFeeds = []
} }

View File

@ -79,10 +79,10 @@ type DisplayCustomFeed =
match feed.Source with match feed.Source with
| Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}" | Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}"
| Tag tag -> $"Tag: {tag}" | Tag tag -> $"Tag: {tag}"
{ Id = CustomFeedId.toString feed.Id { Id = CustomFeedId.toString feed.Id
Source = source Source = source
Path = Permalink.toString feed.Path Path = Permalink.toString feed.Path
IsPodcast = Option.isSome feed.Podcast IsPodcast = Option.isSome feed.Podcast
} }
@ -123,32 +123,32 @@ type DisplayPage =
/// Create a minimal display page (no text or metadata) from a database page /// Create a minimal display page (no text or metadata) from a database page
static member fromPageMinimal webLog (page : Page) = static member fromPageMinimal webLog (page : Page) =
let pageId = PageId.toString page.Id let pageId = PageId.toString page.Id
{ Id = pageId { Id = pageId
AuthorId = WebLogUserId.toString page.AuthorId AuthorId = WebLogUserId.toString page.AuthorId
Title = page.Title Title = page.Title
Permalink = Permalink.toString page.Permalink Permalink = Permalink.toString page.Permalink
PublishedOn = page.PublishedOn PublishedOn = page.PublishedOn
UpdatedOn = page.UpdatedOn UpdatedOn = page.UpdatedOn
IsInPageList = page.IsInPageList IsInPageList = page.IsInPageList
IsDefault = pageId = webLog.DefaultPage IsDefault = pageId = webLog.DefaultPage
Text = "" Text = ""
Metadata = [] Metadata = []
} }
/// Create a display page from a database page /// Create a display page from a database page
static member fromPage webLog (page : Page) = static member fromPage webLog (page : Page) =
let _, extra = WebLog.hostAndPath webLog let _, extra = WebLog.hostAndPath webLog
let pageId = PageId.toString page.Id let pageId = PageId.toString page.Id
{ Id = pageId { Id = pageId
AuthorId = WebLogUserId.toString page.AuthorId AuthorId = WebLogUserId.toString page.AuthorId
Title = page.Title Title = page.Title
Permalink = Permalink.toString page.Permalink Permalink = Permalink.toString page.Permalink
PublishedOn = page.PublishedOn PublishedOn = page.PublishedOn
UpdatedOn = page.UpdatedOn UpdatedOn = page.UpdatedOn
IsInPageList = page.IsInPageList IsInPageList = page.IsInPageList
IsDefault = pageId = webLog.DefaultPage IsDefault = pageId = webLog.DefaultPage
Text = if extra = "" then page.Text else page.Text.Replace ("href=\"/", $"href=\"{extra}/") Text = if extra = "" then page.Text else page.Text.Replace ("href=\"/", $"href=\"{extra}/")
Metadata = page.Metadata Metadata = page.Metadata
} }
@ -168,9 +168,9 @@ with
/// Create a display revision from an actual revision /// Create a display revision from an actual revision
static member fromRevision webLog (rev : Revision) = static member fromRevision webLog (rev : Revision) =
{ AsOf = rev.AsOf { AsOf = rev.AsOf
AsOfLocal = WebLog.localTime webLog rev.AsOf AsOfLocal = WebLog.localTime webLog rev.AsOf
Format = MarkupText.sourceType rev.Text Format = MarkupText.sourceType rev.Text
} }
@ -199,11 +199,56 @@ type DisplayUpload =
static member fromUpload webLog source (upload : Upload) = static member fromUpload webLog source (upload : Upload) =
let path = Permalink.toString upload.Path let path = Permalink.toString upload.Path
let name = Path.GetFileName path let name = Path.GetFileName path
{ Id = UploadId.toString upload.Id { Id = UploadId.toString upload.Id
Name = name Name = name
Path = path.Replace (name, "") Path = path.Replace (name, "")
UpdatedOn = Some (WebLog.localTime webLog upload.UpdatedOn) UpdatedOn = Some (WebLog.localTime webLog upload.UpdatedOn)
Source = UploadDestination.toString source Source = UploadDestination.toString source
}
/// View model to display a user's information
[<NoComparison; NoEquality>]
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<DateTime>
}
/// 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 /// Create an edit model from an existing category
static member fromCategory (cat : Category) = static member fromCategory (cat : Category) =
{ CategoryId = CategoryId.toString cat.Id { CategoryId = CategoryId.toString cat.Id
Name = cat.Name Name = cat.Name
Slug = cat.Slug Slug = cat.Slug
Description = defaultArg cat.Description "" Description = defaultArg cat.Description ""
ParentId = cat.ParentId |> Option.map CategoryId.toString |> Option.defaultValue "" ParentId = cat.ParentId |> Option.map CategoryId.toString |> Option.defaultValue ""
} }
@ -305,27 +350,27 @@ type EditCustomFeedModel =
/// An empty custom feed model /// An empty custom feed model
static member empty = static member empty =
{ Id = "" { Id = ""
SourceType = "category" SourceType = "category"
SourceValue = "" SourceValue = ""
Path = "" Path = ""
IsPodcast = false IsPodcast = false
Title = "" Title = ""
Subtitle = "" Subtitle = ""
ItemsInFeed = 25 ItemsInFeed = 25
Summary = "" Summary = ""
DisplayedAuthor = "" DisplayedAuthor = ""
Email = "" Email = ""
ImageUrl = "" ImageUrl = ""
AppleCategory = "" AppleCategory = ""
AppleSubcategory = "" AppleSubcategory = ""
Explicit = "no" Explicit = "no"
DefaultMediaType = "audio/mpeg" DefaultMediaType = "audio/mpeg"
MediaBaseUrl = "" MediaBaseUrl = ""
FundingUrl = "" FundingUrl = ""
FundingText = "" FundingText = ""
PodcastGuid = "" PodcastGuid = ""
Medium = "" Medium = ""
} }
/// Create a model from a custom feed /// Create a model from a custom feed
@ -413,11 +458,11 @@ type EditMyInfoModel =
/// Create an edit model from a user /// Create an edit model from a user
static member fromUser (user : WebLogUser) = static member fromUser (user : WebLogUser) =
{ FirstName = user.FirstName { FirstName = user.FirstName
LastName = user.LastName LastName = user.LastName
PreferredName = user.PreferredName PreferredName = user.PreferredName
NewPassword = "" NewPassword = ""
NewPasswordConfirm = "" NewPasswordConfirm = ""
} }
@ -459,15 +504,15 @@ type EditPageModel =
| Some rev -> rev | Some rev -> rev
| None -> Revision.empty | None -> Revision.empty
let page = if page.Metadata |> List.isEmpty then { page with Metadata = [ MetaItem.empty ] } else page let page = if page.Metadata |> List.isEmpty then { page with Metadata = [ MetaItem.empty ] } else page
{ PageId = PageId.toString page.Id { PageId = PageId.toString page.Id
Title = page.Title Title = page.Title
Permalink = Permalink.toString page.Permalink Permalink = Permalink.toString page.Permalink
Template = defaultArg page.Template "" Template = defaultArg page.Template ""
IsShownInPageList = page.IsInPageList IsShownInPageList = page.IsInPageList
Source = MarkupText.sourceType latest.Text Source = MarkupText.sourceType latest.Text
Text = MarkupText.text latest.Text Text = MarkupText.text latest.Text
MetaNames = page.Metadata |> List.map (fun m -> m.Name) |> Array.ofList MetaNames = page.Metadata |> List.map (fun m -> m.Name) |> Array.ofList
MetaValues = page.Metadata |> List.map (fun m -> m.Value) |> Array.ofList MetaValues = page.Metadata |> List.map (fun m -> m.Value) |> Array.ofList
} }
/// Whether this is a new page /// Whether this is a new page
@ -612,39 +657,39 @@ type EditPostModel =
| None -> Revision.empty | None -> Revision.empty
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.empty ] } else post let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.empty ] } else post
let episode = defaultArg post.Episode Episode.empty let episode = defaultArg post.Episode Episode.empty
{ PostId = PostId.toString post.Id { PostId = PostId.toString post.Id
Title = post.Title Title = post.Title
Permalink = Permalink.toString post.Permalink Permalink = Permalink.toString post.Permalink
Source = MarkupText.sourceType latest.Text Source = MarkupText.sourceType latest.Text
Text = MarkupText.text latest.Text Text = MarkupText.text latest.Text
Tags = String.Join (", ", post.Tags) Tags = String.Join (", ", post.Tags)
Template = defaultArg post.Template "" Template = defaultArg post.Template ""
CategoryIds = post.CategoryIds |> List.map CategoryId.toString |> Array.ofList CategoryIds = post.CategoryIds |> List.map CategoryId.toString |> Array.ofList
Status = PostStatus.toString post.Status Status = PostStatus.toString post.Status
DoPublish = false DoPublish = false
MetaNames = post.Metadata |> List.map (fun m -> m.Name) |> Array.ofList MetaNames = post.Metadata |> List.map (fun m -> m.Name) |> Array.ofList
MetaValues = post.Metadata |> List.map (fun m -> m.Value) |> Array.ofList MetaValues = post.Metadata |> List.map (fun m -> m.Value) |> Array.ofList
SetPublished = false SetPublished = false
PubOverride = post.PublishedOn |> Option.map (WebLog.localTime webLog) |> Option.toNullable PubOverride = post.PublishedOn |> Option.map (WebLog.localTime webLog) |> Option.toNullable
SetUpdated = false SetUpdated = false
IsEpisode = Option.isSome post.Episode IsEpisode = Option.isSome post.Episode
Media = episode.Media Media = episode.Media
Length = episode.Length Length = episode.Length
Duration = defaultArg (episode.Duration |> Option.map (fun it -> it.ToString """hh\:mm\:ss""")) "" Duration = defaultArg (episode.Duration |> Option.map (fun it -> it.ToString """hh\:mm\:ss""")) ""
MediaType = defaultArg episode.MediaType "" MediaType = defaultArg episode.MediaType ""
ImageUrl = defaultArg episode.ImageUrl "" ImageUrl = defaultArg episode.ImageUrl ""
Subtitle = defaultArg episode.Subtitle "" Subtitle = defaultArg episode.Subtitle ""
Explicit = defaultArg (episode.Explicit |> Option.map ExplicitRating.toString) "" Explicit = defaultArg (episode.Explicit |> Option.map ExplicitRating.toString) ""
ChapterFile = defaultArg episode.ChapterFile "" ChapterFile = defaultArg episode.ChapterFile ""
ChapterType = defaultArg episode.ChapterType "" ChapterType = defaultArg episode.ChapterType ""
TranscriptUrl = defaultArg episode.TranscriptUrl "" TranscriptUrl = defaultArg episode.TranscriptUrl ""
TranscriptType = defaultArg episode.TranscriptType "" TranscriptType = defaultArg episode.TranscriptType ""
TranscriptLang = defaultArg episode.TranscriptLang "" TranscriptLang = defaultArg episode.TranscriptLang ""
TranscriptCaptions = defaultArg episode.TranscriptCaptions false TranscriptCaptions = defaultArg episode.TranscriptCaptions false
SeasonNumber = defaultArg episode.SeasonNumber 0 SeasonNumber = defaultArg episode.SeasonNumber 0
SeasonDescription = defaultArg episode.SeasonDescription "" SeasonDescription = defaultArg episode.SeasonDescription ""
EpisodeNumber = defaultArg (episode.EpisodeNumber |> Option.map string) "" EpisodeNumber = defaultArg (episode.EpisodeNumber |> Option.map string) ""
EpisodeDescription = defaultArg episode.EpisodeDescription "" EpisodeDescription = defaultArg episode.EpisodeDescription ""
} }
/// Whether this is a new post /// Whether this is a new post
@ -736,12 +781,12 @@ type EditRssModel =
/// Create an edit model from a set of RSS options /// Create an edit model from a set of RSS options
static member fromRssOptions (rss : RssOptions) = static member fromRssOptions (rss : RssOptions) =
{ IsFeedEnabled = rss.IsFeedEnabled { IsFeedEnabled = rss.IsFeedEnabled
FeedName = rss.FeedName FeedName = rss.FeedName
ItemsInFeed = defaultArg rss.ItemsInFeed 0 ItemsInFeed = defaultArg rss.ItemsInFeed 0
IsCategoryEnabled = rss.IsCategoryEnabled IsCategoryEnabled = rss.IsCategoryEnabled
IsTagEnabled = rss.IsTagEnabled IsTagEnabled = rss.IsTagEnabled
Copyright = defaultArg rss.Copyright "" Copyright = defaultArg rss.Copyright ""
} }
/// Update RSS options from values in this mode /// Update RSS options from values in this mode
@ -774,9 +819,9 @@ type EditTagMapModel =
/// Create an edit model from the tag mapping /// Create an edit model from the tag mapping
static member fromMapping (tagMap : TagMap) : EditTagMapModel = static member fromMapping (tagMap : TagMap) : EditTagMapModel =
{ Id = TagMapId.toString tagMap.Id { Id = TagMapId.toString tagMap.Id
Tag = tagMap.Tag Tag = tagMap.Tag
UrlValue = tagMap.UrlValue UrlValue = tagMap.UrlValue
} }
@ -819,20 +864,20 @@ type ManagePermalinksModel =
/// Create a permalink model from a page /// Create a permalink model from a page
static member fromPage (pg : Page) = static member fromPage (pg : Page) =
{ Id = PageId.toString pg.Id { Id = PageId.toString pg.Id
Entity = "page" Entity = "page"
CurrentTitle = pg.Title CurrentTitle = pg.Title
CurrentPermalink = Permalink.toString pg.Permalink CurrentPermalink = Permalink.toString pg.Permalink
Prior = pg.PriorPermalinks |> List.map Permalink.toString |> Array.ofList Prior = pg.PriorPermalinks |> List.map Permalink.toString |> Array.ofList
} }
/// Create a permalink model from a post /// Create a permalink model from a post
static member fromPost (post : Post) = static member fromPost (post : Post) =
{ Id = PostId.toString post.Id { Id = PostId.toString post.Id
Entity = "post" Entity = "post"
CurrentTitle = post.Title CurrentTitle = post.Title
CurrentPermalink = Permalink.toString post.Permalink CurrentPermalink = Permalink.toString post.Permalink
Prior = post.PriorPermalinks |> List.map Permalink.toString |> Array.ofList Prior = post.PriorPermalinks |> List.map Permalink.toString |> Array.ofList
} }
@ -854,18 +899,18 @@ type ManageRevisionsModel =
/// Create a revision model from a page /// Create a revision model from a page
static member fromPage webLog (pg : Page) = static member fromPage webLog (pg : Page) =
{ Id = PageId.toString pg.Id { Id = PageId.toString pg.Id
Entity = "page" Entity = "page"
CurrentTitle = pg.Title CurrentTitle = pg.Title
Revisions = pg.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList Revisions = pg.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList
} }
/// Create a revision model from a post /// Create a revision model from a post
static member fromPost webLog (post : Post) = static member fromPost webLog (post : Post) =
{ Id = PostId.toString post.Id { Id = PostId.toString post.Id
Entity = "post" Entity = "post"
CurrentTitle = post.Title CurrentTitle = post.Title
Revisions = post.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList Revisions = post.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList
} }
@ -913,18 +958,18 @@ type PostListItem =
static member fromPost (webLog : WebLog) (post : Post) = static member fromPost (webLog : WebLog) (post : Post) =
let _, extra = WebLog.hostAndPath webLog let _, extra = WebLog.hostAndPath webLog
let inTZ = WebLog.localTime webLog let inTZ = WebLog.localTime webLog
{ Id = PostId.toString post.Id { Id = PostId.toString post.Id
AuthorId = WebLogUserId.toString post.AuthorId AuthorId = WebLogUserId.toString post.AuthorId
Status = PostStatus.toString post.Status Status = PostStatus.toString post.Status
Title = post.Title Title = post.Title
Permalink = Permalink.toString post.Permalink Permalink = Permalink.toString post.Permalink
PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable
UpdatedOn = inTZ post.UpdatedOn UpdatedOn = inTZ post.UpdatedOn
Text = if extra = "" then post.Text else post.Text.Replace ("href=\"/", $"href=\"{extra}/") Text = if extra = "" then post.Text else post.Text.Replace ("href=\"/", $"href=\"{extra}/")
CategoryIds = post.CategoryIds |> List.map CategoryId.toString CategoryIds = post.CategoryIds |> List.map CategoryId.toString
Tags = post.Tags Tags = post.Tags
Episode = post.Episode Episode = post.Episode
Metadata = post.Metadata Metadata = post.Metadata
} }
@ -986,15 +1031,15 @@ type SettingsModel =
/// Create a settings model from a web log /// Create a settings model from a web log
static member fromWebLog (webLog : WebLog) = static member fromWebLog (webLog : WebLog) =
{ Name = webLog.Name { Name = webLog.Name
Slug = webLog.Slug Slug = webLog.Slug
Subtitle = defaultArg webLog.Subtitle "" Subtitle = defaultArg webLog.Subtitle ""
DefaultPage = webLog.DefaultPage DefaultPage = webLog.DefaultPage
PostsPerPage = webLog.PostsPerPage PostsPerPage = webLog.PostsPerPage
TimeZone = webLog.TimeZone TimeZone = webLog.TimeZone
ThemeId = ThemeId.toString webLog.ThemeId ThemeId = ThemeId.toString webLog.ThemeId
AutoHtmx = webLog.AutoHtmx AutoHtmx = webLog.AutoHtmx
Uploads = UploadDestination.toString webLog.Uploads Uploads = UploadDestination.toString webLog.Uploads
} }
/// Update a web log with settings from the form /// Update a web log with settings from the form

View File

@ -227,11 +227,12 @@ let register () =
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page> typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog> typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
// View models // View models
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage> typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
typeof<DisplayRevision>; typeof<DisplayUpload>; typeof<EditCategoryModel>; typeof<EditCustomFeedModel> typeof<DisplayRevision>; typeof<DisplayUpload>; typeof<DisplayUser>; typeof<EditCategoryModel>
typeof<EditMyInfoModel>; typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditRssModel> typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>; typeof<EditPageModel>; typeof<EditPostModel>
typeof<EditTagMapModel>; typeof<LogOnModel>; typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel> typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<LogOnModel>; typeof<ManagePermalinksModel>
typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage> typeof<ManageRevisionsModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
typeof<UserMessage>
// Framework types // Framework types
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair> typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list> typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>

View File

@ -149,6 +149,7 @@ let router : HttpHandler = choose [
route "/new" >=> Upload.showNew route "/new" >=> Upload.showNew
]) ])
subRoute "/user" (choose [ subRoute "/user" (choose [
route "s" >=> User.all
route "/my-info" >=> User.myInfo route "/my-info" >=> User.myInfo
]) ])
] ]

View File

@ -5,6 +5,8 @@ open System
open System.Security.Cryptography open System.Security.Cryptography
open System.Text open System.Text
// ~~ LOG ON / LOG OFF ~~
/// Hash a password for a given user /// Hash a password for a given user
let hashedPassword (plainText : string) (email : string) (salt : Guid) = let hashedPassword (plainText : string) (email : string) (salt : Guid) =
let allSalt = Array.concat [ salt.ToByteArray (); Encoding.UTF8.GetBytes email ] 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 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 /// Display the user "my info" page, with information possibly filled in
let private showMyInfo (user : WebLogUser) (hash : Hash) : HttpHandler = fun next ctx -> let private showMyInfo (user : WebLogUser) (hash : Hash) : HttpHandler = fun next ctx ->
addToHash "page_title" "Edit Your Information" hash addToHash "page_title" "Edit Your Information" hash

View File

@ -17,6 +17,7 @@
{% endif %} {% endif %}
{% if is_web_log_admin %} {% if is_web_log_admin %}
{{ "admin/categories" | nav_link: "Categories" }} {{ "admin/categories" | nav_link: "Categories" }}
{{ "admin/users" | nav_link: "Users" }}
{{ "admin/settings" | nav_link: "Settings" }} {{ "admin/settings" | nav_link: "Settings" }}
{% endif %} {% endif %}
</ul> </ul>

View File

@ -0,0 +1,56 @@
<form method="post" id="userList" class="container" hx-target="this" hx-swap="outerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class="row mwl-table-detail" id="user_new"></div>
{%- 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 -%}
<div class="row mwl-table-detail" id="user_{{ user.id }}">
<div class="{{ user_col }} no-wrap">
{{ user.preferred_name }}
{%- if user.access_level == "Administrator" %}
<span class="{{ badge }}-success">ADMINISTRATOR</span>
{%- elsif user.access_level == "WebLogAdmin" %}
<span class="{{ badge }}-primary">WEB LOG ADMIN</span>
{%- elsif user.access_level == "Editor" %}
<span class="{{ badge }}-secondary">EDITOR</span>
{%- elsif user.access_level == "Author" %}
<span class="{{ badge }}-dark">AUTHOR</span>
{%- endif %}<br>
<small>
{%- assign user_url_base = "admin/user/" | append: user.id -%}
<a href="{{ user_url_base | append: "/edit" | relative_link }}" hx-target="#user_{{ user.id }}"
hx-swap="innerHTML show:#user_{{ user.id }}:top">
Edit
</a>
<span class="text-muted"> &bull; </span>
{%- assign user_del_link = user_url_base | append: "/delete" | relative_link -%}
<a href="{{ user_del_link }}" hx-post="{{ user_del_link }}" class="text-danger"
hx-confirm="Are you sure you want to delete the category &ldquo;{{ cat.name }}&rdquo;? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)">
Delete
</a>
</small>
</div>
<div class="{{ email_col }}">
{{ user.first_name }} {{ user.last_name }}<br>
<small class="text-muted">
{{ user.email }}
{%- unless user.url == "" %}<br>{{ user.url }}{% endunless %}
</small>
</div>
<div class="{{ cre8_col }}">
{{ user.created_on | date: "MMMM d, yyyy" }}
</div>
<div class="{{ last_col }}">
{% if user.last_seen_on %}
{{ user.last_seen_on | date: "MMMM d, yyyy" }} at
{{ user.last_seen_on | date: "h:mmtt" | downcase }}
{% else %}
--
{% endif %}
</div>
</div>
{%- endfor %}
</form>

View File

@ -0,0 +1,20 @@
<h2 class="my-3">{{ page_title }}</h2>
<article>
<a href="{{ "admin/user/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
hx-target="#user_new">
Add a New User
</a>
<div class="container">
{%- 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" -%}
<div class="row mwl-table-heading">
<div class="{{ user_col }}">User<span class="d-md-none">; Details; Last Log On</span></div>
<div class="{{ email_col }} d-none d-md-inline-block">Details</div>
<div class="{{ cre8_col }}">Created</div>
<div class="{{ last_col }} d-none d-md-block">Last Log On</div>
</div>
</div>
{{ user_list }}
</article>

View File

@ -334,13 +334,10 @@ htmx.on("htmx:afterOnLoad", function (evt) {
}) })
htmx.on("htmx:responseError", function (evt) { htmx.on("htmx:responseError", function (evt) {
/** @type {XMLHttpRequest} */
const xhr = evt.detail.xhr const xhr = evt.detail.xhr
const hdrs = xhr.getAllResponseHeaders() const hdrs = xhr.getAllResponseHeaders()
// Show messages if there were any in the response // Show an error message if there were none in the response
if (hdrs.indexOf("x-message") >= 0) { if (hdrs.indexOf("x-message") < 0) {
Admin.showMessage(evt.detail.xhr.getResponseHeader("x-message"))
} else {
Admin.showMessage(`danger|||${xhr.status}: ${xhr.statusText}`) Admin.showMessage(`danger|||${xhr.status}: ${xhr.statusText}`)
} }
}) })