2022-06-23 00:35:12 +00:00
|
|
|
namespace MyWebLog.ViewModels
|
|
|
|
|
|
|
|
open System
|
|
|
|
open MyWebLog
|
|
|
|
|
|
|
|
/// Helper functions for view models
|
|
|
|
[<AutoOpen>]
|
|
|
|
module private Helpers =
|
|
|
|
|
|
|
|
/// Create a string option if a string is blank
|
|
|
|
let noneIfBlank (it : string) =
|
2022-06-27 21:47:00 +00:00
|
|
|
match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
|
2022-06-29 02:18:56 +00:00
|
|
|
/// The model used to display the admin dashboard
|
|
|
|
[<NoComparison; NoEquality>]
|
|
|
|
type DashboardModel =
|
|
|
|
{ /// The number of published posts
|
2022-07-18 03:10:30 +00:00
|
|
|
Posts : int
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The number of post drafts
|
2022-07-18 03:10:30 +00:00
|
|
|
Drafts : int
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The number of pages
|
2022-07-18 03:10:30 +00:00
|
|
|
Pages : int
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The number of pages in the page list
|
2022-07-18 03:10:30 +00:00
|
|
|
ListedPages : int
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The number of categories
|
2022-07-18 03:10:30 +00:00
|
|
|
Categories : int
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The top-level categories
|
2022-07-18 03:10:30 +00:00
|
|
|
TopLevelCategories : int
|
2022-06-29 02:18:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-06-23 00:35:12 +00:00
|
|
|
/// Details about a category, used to display category lists
|
|
|
|
[<NoComparison; NoEquality>]
|
|
|
|
type DisplayCategory =
|
|
|
|
{ /// The ID of the category
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The slug for the category
|
2022-07-18 03:10:30 +00:00
|
|
|
Slug : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The name of the category
|
2022-07-18 03:10:30 +00:00
|
|
|
Name : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A description of the category
|
2022-07-18 03:10:30 +00:00
|
|
|
Description : string option
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The parent category names for this (sub)category
|
2022-07-18 03:10:30 +00:00
|
|
|
ParentNames : string[]
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The number of posts in this category
|
2022-07-18 03:10:30 +00:00
|
|
|
PostCount : int
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A display version of a custom feed definition
|
|
|
|
type DisplayCustomFeed =
|
|
|
|
{ /// The ID of the custom feed
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The source of the custom feed
|
2022-07-18 03:10:30 +00:00
|
|
|
Source : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The relative path at which the custom feed is served
|
2022-07-18 03:10:30 +00:00
|
|
|
Path : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether this custom feed is for a podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
IsPodcast : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a display version from a custom feed
|
|
|
|
static member fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed =
|
|
|
|
let source =
|
|
|
|
match feed.source with
|
2022-07-18 03:10:30 +00:00
|
|
|
| Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}"
|
2022-06-23 00:35:12 +00:00
|
|
|
| Tag tag -> $"Tag: {tag}"
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = CustomFeedId.toString feed.id
|
|
|
|
Source = source
|
|
|
|
Path = Permalink.toString feed.path
|
|
|
|
IsPodcast = Option.isSome feed.podcast
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Details about a page used to display page lists
|
|
|
|
[<NoComparison; NoEquality>]
|
|
|
|
type DisplayPage =
|
|
|
|
{ /// The ID of this page
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
2022-07-17 02:17:57 +00:00
|
|
|
/// The ID of the author of this page
|
2022-07-18 03:10:30 +00:00
|
|
|
AuthorId : string
|
2022-07-17 02:17:57 +00:00
|
|
|
|
2022-06-23 00:35:12 +00:00
|
|
|
/// The title of the page
|
2022-07-18 03:10:30 +00:00
|
|
|
Title : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The link at which this page is displayed
|
2022-07-18 03:10:30 +00:00
|
|
|
Permalink : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// When this page was published
|
2022-07-18 03:10:30 +00:00
|
|
|
PublishedOn : DateTime
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// When this page was last updated
|
2022-07-18 03:10:30 +00:00
|
|
|
UpdatedOn : DateTime
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether this page shows as part of the web log's navigation
|
2022-07-18 03:10:30 +00:00
|
|
|
ShowInPageList : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Is this the default page?
|
2022-07-18 03:10:30 +00:00
|
|
|
IsDefault : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The text of the page
|
2022-07-18 03:10:30 +00:00
|
|
|
Text : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The metadata for the page
|
2022-07-18 03:10:30 +00:00
|
|
|
Metadata : MetaItem list
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = pageId
|
|
|
|
AuthorId = WebLogUserId.toString page.authorId
|
|
|
|
Title = page.title
|
|
|
|
Permalink = Permalink.toString page.permalink
|
|
|
|
PublishedOn = page.publishedOn
|
|
|
|
UpdatedOn = page.updatedOn
|
|
|
|
ShowInPageList = page.showInPageList
|
|
|
|
IsDefault = pageId = webLog.defaultPage
|
|
|
|
Text = ""
|
|
|
|
Metadata = []
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = pageId
|
|
|
|
AuthorId = WebLogUserId.toString page.authorId
|
|
|
|
Title = page.title
|
|
|
|
Permalink = Permalink.toString page.permalink
|
|
|
|
PublishedOn = page.publishedOn
|
|
|
|
UpdatedOn = page.updatedOn
|
|
|
|
ShowInPageList = page.showInPageList
|
|
|
|
IsDefault = pageId = webLog.defaultPage
|
|
|
|
Text = if extra = "" then page.text else page.text.Replace ("href=\"/", $"href=\"{extra}/")
|
|
|
|
Metadata = page.metadata
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-07-15 03:25:29 +00:00
|
|
|
/// Information about a revision used for display
|
2022-07-18 03:10:30 +00:00
|
|
|
[<NoComparison; NoEquality>]
|
2022-07-15 03:25:29 +00:00
|
|
|
type DisplayRevision =
|
|
|
|
{ /// The as-of date/time for the revision
|
2022-07-18 03:10:30 +00:00
|
|
|
AsOf : DateTime
|
2022-07-15 03:25:29 +00:00
|
|
|
|
|
|
|
/// The as-of date/time for the revision in the web log's local time zone
|
2022-07-18 03:10:30 +00:00
|
|
|
AsOfLocal : DateTime
|
2022-07-15 03:25:29 +00:00
|
|
|
|
|
|
|
/// The format of the text of the revision
|
2022-07-18 03:10:30 +00:00
|
|
|
Format : string
|
2022-07-15 03:25:29 +00:00
|
|
|
}
|
|
|
|
with
|
|
|
|
|
|
|
|
/// Create a display revision from an actual revision
|
|
|
|
static member fromRevision webLog (rev : Revision) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ AsOf = rev.asOf
|
|
|
|
AsOfLocal = WebLog.localTime webLog rev.asOf
|
|
|
|
Format = MarkupText.sourceType rev.text
|
2022-07-15 03:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-06-29 02:18:56 +00:00
|
|
|
open System.IO
|
2022-06-23 00:35:12 +00:00
|
|
|
|
2022-06-29 02:18:56 +00:00
|
|
|
/// Information about an uploaded file used for display
|
|
|
|
[<NoComparison; NoEquality>]
|
|
|
|
type DisplayUpload =
|
|
|
|
{ /// The ID of the uploaded file
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The name of the uploaded file
|
2022-07-18 03:10:30 +00:00
|
|
|
Name : string
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The path at which the file is served
|
2022-07-18 03:10:30 +00:00
|
|
|
Path : string
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The date/time the file was updated
|
2022-07-18 03:10:30 +00:00
|
|
|
UpdatedOn : DateTime option
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// The source for this file (created from UploadDestination DU)
|
2022-07-18 03:10:30 +00:00
|
|
|
Source : string
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
2022-06-29 02:18:56 +00:00
|
|
|
|
|
|
|
/// Create a display uploaded file
|
2022-07-04 17:19:16 +00:00
|
|
|
static member fromUpload webLog source (upload : Upload) =
|
2022-06-29 02:18:56 +00:00
|
|
|
let path = Permalink.toString upload.path
|
|
|
|
let name = Path.GetFileName path
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = UploadId.toString upload.id
|
|
|
|
Name = name
|
|
|
|
Path = path.Replace (name, "")
|
|
|
|
UpdatedOn = Some (WebLog.localTime webLog upload.updatedOn)
|
|
|
|
Source = UploadDestination.toString source
|
2022-06-29 02:18:56 +00:00
|
|
|
}
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
/// View model for editing categories
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type EditCategoryModel =
|
|
|
|
{ /// The ID of the category being edited
|
2022-07-18 03:10:30 +00:00
|
|
|
CategoryId : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The name of the category
|
2022-07-18 03:10:30 +00:00
|
|
|
Name : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The category's URL slug
|
2022-07-18 03:10:30 +00:00
|
|
|
Slug : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A description of the category (optional)
|
2022-07-18 03:10:30 +00:00
|
|
|
Description : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The ID of the category for which this is a subcategory (optional)
|
2022-07-18 03:10:30 +00:00
|
|
|
ParentId : string
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create an edit model from an existing category
|
|
|
|
static member fromCategory (cat : Category) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ CategoryId = CategoryId.toString cat.id
|
|
|
|
Name = cat.name
|
|
|
|
Slug = cat.slug
|
|
|
|
Description = defaultArg cat.description ""
|
|
|
|
ParentId = cat.parentId |> Option.map CategoryId.toString |> Option.defaultValue ""
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// View model to edit a custom RSS feed
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type EditCustomFeedModel =
|
|
|
|
{ /// The ID of the feed being editing
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The type of source for this feed ("category" or "tag")
|
2022-07-18 03:10:30 +00:00
|
|
|
SourceType : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The category ID or tag on which this feed is based
|
2022-07-18 03:10:30 +00:00
|
|
|
SourceValue : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The relative path at which this feed is served
|
2022-07-18 03:10:30 +00:00
|
|
|
Path : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether this feed defines a podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
IsPodcast : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The title of the podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
Title : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A subtitle for the podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
Subtitle : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The number of items in the podcast feed
|
2022-07-18 03:10:30 +00:00
|
|
|
ItemsInFeed : int
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A summary of the podcast (iTunes field)
|
2022-07-18 03:10:30 +00:00
|
|
|
Summary : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The display name of the podcast author (iTunes field)
|
2022-07-18 03:10:30 +00:00
|
|
|
DisplayedAuthor : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The e-mail address of the user who registered the podcast at iTunes
|
2022-07-18 03:10:30 +00:00
|
|
|
Email : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The link to the image for the podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
ImageUrl : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The category from iTunes under which this podcast is categorized
|
2022-07-18 03:10:30 +00:00
|
|
|
iTunesCategory : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A further refinement of the categorization of this podcast (iTunes field / values)
|
2022-07-18 03:10:30 +00:00
|
|
|
iTunesSubcategory : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The explictness rating (iTunes field)
|
2022-07-18 03:10:30 +00:00
|
|
|
Explicit : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The default media type for files in this podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
DefaultMediaType : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
|
2022-07-18 03:10:30 +00:00
|
|
|
MediaBaseUrl : string
|
2022-07-04 22:40:32 +00:00
|
|
|
|
|
|
|
/// The URL for funding information for the podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
FundingUrl : string
|
2022-07-04 22:40:32 +00:00
|
|
|
|
|
|
|
/// The text for the funding link
|
2022-07-18 03:10:30 +00:00
|
|
|
FundingText : string
|
2022-07-04 22:40:32 +00:00
|
|
|
|
|
|
|
/// A unique identifier to follow this podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
PodcastGuid : string
|
2022-07-04 22:40:32 +00:00
|
|
|
|
|
|
|
/// The medium for the content of this podcast
|
2022-07-18 03:10:30 +00:00
|
|
|
Medium : string
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// An empty custom feed model
|
|
|
|
static member empty =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = ""
|
|
|
|
SourceType = "category"
|
|
|
|
SourceValue = ""
|
|
|
|
Path = ""
|
|
|
|
IsPodcast = false
|
|
|
|
Title = ""
|
|
|
|
Subtitle = ""
|
|
|
|
ItemsInFeed = 25
|
|
|
|
Summary = ""
|
|
|
|
DisplayedAuthor = ""
|
|
|
|
Email = ""
|
|
|
|
ImageUrl = ""
|
|
|
|
iTunesCategory = ""
|
|
|
|
iTunesSubcategory = ""
|
|
|
|
Explicit = "no"
|
|
|
|
DefaultMediaType = "audio/mpeg"
|
|
|
|
MediaBaseUrl = ""
|
|
|
|
FundingUrl = ""
|
|
|
|
FundingText = ""
|
|
|
|
PodcastGuid = ""
|
|
|
|
Medium = ""
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a model from a custom feed
|
|
|
|
static member fromFeed (feed : CustomFeed) =
|
|
|
|
let rss =
|
|
|
|
{ EditCustomFeedModel.empty with
|
2022-07-18 03:10:30 +00:00
|
|
|
Id = CustomFeedId.toString feed.id
|
|
|
|
SourceType = match feed.source with Category _ -> "category" | Tag _ -> "tag"
|
|
|
|
SourceValue = match feed.source with Category (CategoryId catId) -> catId | Tag tag -> tag
|
|
|
|
Path = Permalink.toString feed.path
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
match feed.podcast with
|
|
|
|
| Some p ->
|
|
|
|
{ rss with
|
2022-07-18 03:10:30 +00:00
|
|
|
IsPodcast = true
|
|
|
|
Title = p.title
|
|
|
|
Subtitle = defaultArg p.subtitle ""
|
|
|
|
ItemsInFeed = p.itemsInFeed
|
|
|
|
Summary = p.summary
|
|
|
|
DisplayedAuthor = p.displayedAuthor
|
|
|
|
Email = p.email
|
|
|
|
ImageUrl = Permalink.toString p.imageUrl
|
|
|
|
iTunesCategory = p.iTunesCategory
|
|
|
|
iTunesSubcategory = defaultArg p.iTunesSubcategory ""
|
|
|
|
Explicit = ExplicitRating.toString p.explicit
|
|
|
|
DefaultMediaType = defaultArg p.defaultMediaType ""
|
|
|
|
MediaBaseUrl = defaultArg p.mediaBaseUrl ""
|
|
|
|
FundingUrl = defaultArg p.fundingUrl ""
|
|
|
|
FundingText = defaultArg p.fundingText ""
|
|
|
|
PodcastGuid = p.guid
|
2022-07-04 22:40:32 +00:00
|
|
|
|> Option.map (fun it -> it.ToString().ToLowerInvariant ())
|
|
|
|
|> Option.defaultValue ""
|
2022-07-18 03:10:30 +00:00
|
|
|
Medium = p.medium |> Option.map PodcastMedium.toString |> Option.defaultValue ""
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
| None -> rss
|
|
|
|
|
|
|
|
/// Update a feed with values from this model
|
|
|
|
member this.updateFeed (feed : CustomFeed) =
|
|
|
|
{ feed with
|
2022-07-18 03:10:30 +00:00
|
|
|
source = if this.SourceType = "tag" then Tag this.SourceValue else Category (CategoryId this.SourceValue)
|
|
|
|
path = Permalink this.Path
|
2022-06-23 00:35:12 +00:00
|
|
|
podcast =
|
2022-07-18 03:10:30 +00:00
|
|
|
if this.IsPodcast then
|
2022-06-23 00:35:12 +00:00
|
|
|
Some {
|
2022-07-18 03:10:30 +00:00
|
|
|
title = this.Title
|
|
|
|
subtitle = noneIfBlank this.Subtitle
|
|
|
|
itemsInFeed = this.ItemsInFeed
|
|
|
|
summary = this.Summary
|
|
|
|
displayedAuthor = this.DisplayedAuthor
|
|
|
|
email = this.Email
|
|
|
|
imageUrl = Permalink this.ImageUrl
|
|
|
|
iTunesCategory = this.iTunesCategory
|
|
|
|
iTunesSubcategory = noneIfBlank this.iTunesSubcategory
|
|
|
|
explicit = ExplicitRating.parse this.Explicit
|
|
|
|
defaultMediaType = noneIfBlank this.DefaultMediaType
|
|
|
|
mediaBaseUrl = noneIfBlank this.MediaBaseUrl
|
|
|
|
guid = noneIfBlank this.PodcastGuid |> Option.map Guid.Parse
|
|
|
|
fundingUrl = noneIfBlank this.FundingUrl
|
|
|
|
fundingText = noneIfBlank this.FundingText
|
|
|
|
medium = noneIfBlank this.Medium |> Option.map PodcastMedium.parse
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
/// View model to edit a page
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type EditPageModel =
|
|
|
|
{ /// The ID of the page being edited
|
2022-07-18 03:10:30 +00:00
|
|
|
PageId : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The title of the page
|
2022-07-18 03:10:30 +00:00
|
|
|
Title : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The permalink for the page
|
2022-07-18 03:10:30 +00:00
|
|
|
Permalink : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The template to use to display the page
|
2022-07-18 03:10:30 +00:00
|
|
|
Template : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether this page is shown in the page list
|
2022-07-18 03:10:30 +00:00
|
|
|
IsShownInPageList : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The source format for the text
|
2022-07-18 03:10:30 +00:00
|
|
|
Source : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The text of the page
|
2022-07-18 03:10:30 +00:00
|
|
|
Text : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Names of metadata items
|
2022-07-18 03:10:30 +00:00
|
|
|
MetaNames : string[]
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Values of metadata items
|
2022-07-18 03:10:30 +00:00
|
|
|
MetaValues : string[]
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create an edit model from an existing page
|
|
|
|
static member fromPage (page : Page) =
|
|
|
|
let latest =
|
|
|
|
match page.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
|
|
|
|
| Some rev -> rev
|
|
|
|
| None -> Revision.empty
|
|
|
|
let page = if page.metadata |> List.isEmpty then { page with metadata = [ MetaItem.empty ] } else page
|
2022-07-18 03:10:30 +00:00
|
|
|
{ PageId = PageId.toString page.id
|
|
|
|
Title = page.title
|
|
|
|
Permalink = Permalink.toString page.permalink
|
|
|
|
Template = defaultArg page.template ""
|
|
|
|
IsShownInPageList = page.showInPageList
|
|
|
|
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
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// View model to edit a post
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type EditPostModel =
|
|
|
|
{ /// The ID of the post being edited
|
2022-07-18 03:10:30 +00:00
|
|
|
PostId : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The title of the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Title : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The permalink for the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Permalink : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The source format for the text
|
2022-07-18 03:10:30 +00:00
|
|
|
Source : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The text of the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Text : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The tags for the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Tags : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The template used to display the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Template : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The category IDs for the post
|
2022-07-18 03:10:30 +00:00
|
|
|
CategoryIds : string[]
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The post status
|
2022-07-18 03:10:30 +00:00
|
|
|
Status : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether this post should be published
|
2022-07-18 03:10:30 +00:00
|
|
|
DoPublish : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Names of metadata items
|
2022-07-18 03:10:30 +00:00
|
|
|
MetaNames : string[]
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Values of metadata items
|
2022-07-18 03:10:30 +00:00
|
|
|
MetaValues : string[]
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether to override the published date/time
|
2022-07-18 03:10:30 +00:00
|
|
|
SetPublished : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The published date/time to override
|
2022-07-18 03:10:30 +00:00
|
|
|
PubOverride : Nullable<DateTime>
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether all revisions should be purged and the override date set as the updated date as well
|
2022-07-18 03:10:30 +00:00
|
|
|
SetUpdated : bool
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// Whether this post has a podcast episode
|
2022-07-18 03:10:30 +00:00
|
|
|
IsEpisode : bool
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The URL for the media for this episode (may be permalink)
|
2022-07-18 03:10:30 +00:00
|
|
|
Media : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The size (in bytes) of the media for this episode
|
2022-07-18 03:10:30 +00:00
|
|
|
Length : int64
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The duration of the media for this episode
|
2022-07-18 03:10:30 +00:00
|
|
|
Duration : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The media type (optional, defaults to podcast-defined media type)
|
2022-07-18 03:10:30 +00:00
|
|
|
MediaType : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The URL for the image for this episode (may be permalink; optional, defaults to podcast image)
|
2022-07-18 03:10:30 +00:00
|
|
|
ImageUrl : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// A subtitle for the episode (optional)
|
2022-07-18 03:10:30 +00:00
|
|
|
Subtitle : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The explicit rating for this episode (optional, defaults to podcast setting)
|
2022-07-18 03:10:30 +00:00
|
|
|
Explicit : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The URL for the chapter file for the episode (may be permalink; optional)
|
2022-07-18 03:10:30 +00:00
|
|
|
ChapterFile : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The type of the chapter file (optional; defaults to application/json+chapters if chapterFile is provided)
|
2022-07-18 03:10:30 +00:00
|
|
|
ChapterType : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The URL for the transcript (may be permalink; optional)
|
2022-07-18 03:10:30 +00:00
|
|
|
TranscriptUrl : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The MIME type for the transcript (optional, recommended if transcriptUrl is provided)
|
2022-07-18 03:10:30 +00:00
|
|
|
TranscriptType : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The language of the transcript (optional)
|
2022-07-18 03:10:30 +00:00
|
|
|
TranscriptLang : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// Whether the provided transcript should be presented as captions
|
2022-07-18 03:10:30 +00:00
|
|
|
TranscriptCaptions : bool
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The season number (optional)
|
2022-07-18 03:10:30 +00:00
|
|
|
SeasonNumber : int
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// A description of this season (optional, ignored if season number is not provided)
|
2022-07-18 03:10:30 +00:00
|
|
|
SeasonDescription : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// The episode number (decimal; optional)
|
2022-07-18 03:10:30 +00:00
|
|
|
EpisodeNumber : string
|
2022-06-27 11:36:29 +00:00
|
|
|
|
|
|
|
/// A description of this episode (optional, ignored if episode number is not provided)
|
2022-07-18 03:10:30 +00:00
|
|
|
EpisodeDescription : string
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
2022-06-27 11:36:29 +00:00
|
|
|
|
2022-06-23 00:35:12 +00:00
|
|
|
/// Create an edit model from an existing past
|
|
|
|
static member fromPost webLog (post : Post) =
|
|
|
|
let latest =
|
|
|
|
match post.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
|
|
|
|
| Some rev -> rev
|
|
|
|
| None -> Revision.empty
|
|
|
|
let post = if post.metadata |> List.isEmpty then { post with metadata = [ MetaItem.empty ] } else post
|
2022-06-27 11:36:29 +00:00
|
|
|
let episode = defaultArg post.episode Episode.empty
|
2022-07-18 03:10:30 +00:00
|
|
|
{ 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 ""
|
2022-06-27 11:36:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Update a post with values from the submitted form
|
|
|
|
member this.updatePost (post : Post) (revision : Revision) now =
|
|
|
|
{ post with
|
2022-07-18 03:10:30 +00:00
|
|
|
title = this.Title
|
|
|
|
permalink = Permalink this.Permalink
|
|
|
|
publishedOn = if this.DoPublish then Some now else post.publishedOn
|
2022-06-27 11:36:29 +00:00
|
|
|
updatedOn = now
|
|
|
|
text = MarkupText.toHtml revision.text
|
2022-07-18 03:10:30 +00:00
|
|
|
tags = this.Tags.Split ","
|
2022-06-27 11:36:29 +00:00
|
|
|
|> Seq.ofArray
|
|
|
|
|> Seq.map (fun it -> it.Trim().ToLower ())
|
|
|
|
|> Seq.filter (fun it -> it <> "")
|
|
|
|
|> Seq.sort
|
|
|
|
|> List.ofSeq
|
2022-07-18 03:10:30 +00:00
|
|
|
template = match this.Template.Trim () with "" -> None | tmpl -> Some tmpl
|
|
|
|
categoryIds = this.CategoryIds |> Array.map CategoryId |> List.ofArray
|
|
|
|
status = if this.DoPublish then Published else post.status
|
|
|
|
metadata = Seq.zip this.MetaNames this.MetaValues
|
2022-06-27 11:36:29 +00:00
|
|
|
|> Seq.filter (fun it -> fst it > "")
|
|
|
|
|> Seq.map (fun it -> { name = fst it; value = snd it })
|
|
|
|
|> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}")
|
|
|
|
|> List.ofSeq
|
|
|
|
revisions = match post.revisions |> List.tryHead with
|
|
|
|
| Some r when r.text = revision.text -> post.revisions
|
|
|
|
| _ -> revision :: post.revisions
|
|
|
|
episode =
|
2022-07-18 03:10:30 +00:00
|
|
|
if this.IsEpisode then
|
2022-06-27 11:36:29 +00:00
|
|
|
Some {
|
2022-07-18 03:10:30 +00:00
|
|
|
media = this.Media
|
|
|
|
length = this.Length
|
|
|
|
duration = noneIfBlank this.Duration |> Option.map TimeSpan.Parse
|
|
|
|
mediaType = noneIfBlank this.MediaType
|
|
|
|
imageUrl = noneIfBlank this.ImageUrl
|
|
|
|
subtitle = noneIfBlank this.Subtitle
|
|
|
|
explicit = noneIfBlank this.Explicit |> Option.map ExplicitRating.parse
|
|
|
|
chapterFile = noneIfBlank this.ChapterFile
|
|
|
|
chapterType = noneIfBlank this.ChapterType
|
|
|
|
transcriptUrl = noneIfBlank this.TranscriptUrl
|
|
|
|
transcriptType = noneIfBlank this.TranscriptType
|
|
|
|
transcriptLang = noneIfBlank this.TranscriptLang
|
|
|
|
transcriptCaptions = if this.TranscriptCaptions then Some true else None
|
|
|
|
seasonNumber = if this.SeasonNumber = 0 then None else Some this.SeasonNumber
|
|
|
|
seasonDescription = noneIfBlank this.SeasonDescription
|
|
|
|
episodeNumber = match noneIfBlank this.EpisodeNumber |> Option.map Double.Parse with
|
2022-06-27 21:47:00 +00:00
|
|
|
| Some it when it = 0.0 -> None
|
|
|
|
| Some it -> Some (double it)
|
|
|
|
| None -> None
|
2022-07-18 03:10:30 +00:00
|
|
|
episodeDescription = noneIfBlank this.EpisodeDescription
|
2022-06-27 11:36:29 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
None
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// View model to edit RSS settings
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type EditRssModel =
|
|
|
|
{ /// Whether the site feed of posts is enabled
|
2022-07-18 03:10:30 +00:00
|
|
|
IsFeedEnabled : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The name of the file generated for the site feed
|
2022-07-18 03:10:30 +00:00
|
|
|
FeedName : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Override the "posts per page" setting for the site feed
|
2022-07-18 03:10:30 +00:00
|
|
|
ItemsInFeed : int
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether feeds are enabled for all categories
|
2022-07-18 03:10:30 +00:00
|
|
|
IsCategoryEnabled : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether feeds are enabled for all tags
|
2022-07-18 03:10:30 +00:00
|
|
|
IsTagEnabled : bool
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A copyright string to be placed in all feeds
|
2022-07-18 03:10:30 +00:00
|
|
|
Copyright : string
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create an edit model from a set of RSS options
|
|
|
|
static member fromRssOptions (rss : RssOptions) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ IsFeedEnabled = rss.feedEnabled
|
|
|
|
FeedName = rss.feedName
|
|
|
|
ItemsInFeed = defaultArg rss.itemsInFeed 0
|
|
|
|
IsCategoryEnabled = rss.categoryEnabled
|
|
|
|
IsTagEnabled = rss.tagEnabled
|
|
|
|
Copyright = defaultArg rss.copyright ""
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Update RSS options from values in this mode
|
|
|
|
member this.updateOptions (rss : RssOptions) =
|
|
|
|
{ rss with
|
2022-07-18 03:10:30 +00:00
|
|
|
feedEnabled = this.IsFeedEnabled
|
|
|
|
feedName = this.FeedName
|
|
|
|
itemsInFeed = if this.ItemsInFeed = 0 then None else Some this.ItemsInFeed
|
|
|
|
categoryEnabled = this.IsCategoryEnabled
|
|
|
|
tagEnabled = this.IsTagEnabled
|
|
|
|
copyright = noneIfBlank this.Copyright
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// View model to edit a tag mapping
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type EditTagMapModel =
|
|
|
|
{ /// The ID of the tag mapping being edited
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The tag being mapped to a different link value
|
2022-07-18 03:10:30 +00:00
|
|
|
Tag : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The link value for the tag
|
2022-07-18 03:10:30 +00:00
|
|
|
UrlValue : string
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Whether this is a new tag mapping
|
2022-07-18 03:10:30 +00:00
|
|
|
member this.IsNew = this.Id = "new"
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Create an edit model from the tag mapping
|
|
|
|
static member fromMapping (tagMap : TagMap) : EditTagMapModel =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = TagMapId.toString tagMap.id
|
|
|
|
Tag = tagMap.tag
|
|
|
|
UrlValue = tagMap.urlValue
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// View model to edit a user
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type EditUserModel =
|
|
|
|
{ /// The user's first name
|
2022-07-18 03:10:30 +00:00
|
|
|
FirstName : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The user's last name
|
2022-07-18 03:10:30 +00:00
|
|
|
LastName : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The user's preferred name
|
2022-07-18 03:10:30 +00:00
|
|
|
PreferredName : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A new password for the user
|
2022-07-18 03:10:30 +00:00
|
|
|
NewPassword : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A new password for the user, confirmed
|
2022-07-18 03:10:30 +00:00
|
|
|
NewPasswordConfirm : string
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
/// Create an edit model from a user
|
|
|
|
static member fromUser (user : WebLogUser) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ FirstName = user.firstName
|
|
|
|
LastName = user.lastName
|
|
|
|
PreferredName = user.preferredName
|
|
|
|
NewPassword = ""
|
|
|
|
NewPasswordConfirm = ""
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The model to use to allow a user to log on
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type LogOnModel =
|
|
|
|
{ /// The user's e-mail address
|
2022-07-18 03:10:30 +00:00
|
|
|
EmailAddress : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The user's password
|
2022-07-18 03:10:30 +00:00
|
|
|
Password : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Where the user should be redirected once they have logged on
|
2022-07-18 03:10:30 +00:00
|
|
|
ReturnTo : string option
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// An empty log on model
|
|
|
|
static member empty =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ EmailAddress = ""; Password = ""; ReturnTo = None }
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
/// View model to manage permalinks
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type ManagePermalinksModel =
|
|
|
|
{ /// The ID for the entity being edited
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The type of entity being edited ("page" or "post")
|
2022-07-18 03:10:30 +00:00
|
|
|
Entity : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The current title of the page or post
|
2022-07-18 03:10:30 +00:00
|
|
|
CurrentTitle : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The current permalink of the page or post
|
2022-07-18 03:10:30 +00:00
|
|
|
CurrentPermalink : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The prior permalinks for the page or post
|
2022-07-18 03:10:30 +00:00
|
|
|
Prior : string[]
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a permalink model from a page
|
|
|
|
static member fromPage (pg : Page) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = PageId.toString pg.id
|
|
|
|
Entity = "page"
|
|
|
|
CurrentTitle = pg.title
|
|
|
|
CurrentPermalink = Permalink.toString pg.permalink
|
|
|
|
Prior = pg.priorPermalinks |> List.map Permalink.toString |> Array.ofList
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a permalink model from a post
|
|
|
|
static member fromPost (post : Post) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = PostId.toString post.id
|
|
|
|
Entity = "post"
|
|
|
|
CurrentTitle = post.title
|
|
|
|
CurrentPermalink = Permalink.toString post.permalink
|
|
|
|
Prior = post.priorPermalinks |> List.map Permalink.toString |> Array.ofList
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-07-15 03:25:29 +00:00
|
|
|
/// View model to manage revisions
|
2022-07-18 03:10:30 +00:00
|
|
|
[<NoComparison; NoEquality>]
|
2022-07-15 03:25:29 +00:00
|
|
|
type ManageRevisionsModel =
|
|
|
|
{ /// The ID for the entity being edited
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-07-15 03:25:29 +00:00
|
|
|
|
|
|
|
/// The type of entity being edited ("page" or "post")
|
2022-07-18 03:10:30 +00:00
|
|
|
Entity : string
|
2022-07-15 03:25:29 +00:00
|
|
|
|
|
|
|
/// The current title of the page or post
|
2022-07-18 03:10:30 +00:00
|
|
|
CurrentTitle : string
|
2022-07-15 03:25:29 +00:00
|
|
|
|
|
|
|
/// The revisions for the page or post
|
2022-07-18 03:10:30 +00:00
|
|
|
Revisions : DisplayRevision[]
|
2022-07-15 03:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a revision model from a page
|
|
|
|
static member fromPage webLog (pg : Page) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = PageId.toString pg.id
|
|
|
|
Entity = "page"
|
|
|
|
CurrentTitle = pg.title
|
|
|
|
Revisions = pg.revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList
|
2022-07-15 03:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a revision model from a post
|
|
|
|
static member fromPost webLog (post : Post) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Id = PostId.toString post.id
|
|
|
|
Entity = "post"
|
|
|
|
CurrentTitle = post.title
|
|
|
|
Revisions = post.revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList
|
2022-07-15 03:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-06-23 00:35:12 +00:00
|
|
|
/// View model for posts in a list
|
|
|
|
[<NoComparison; NoEquality>]
|
|
|
|
type PostListItem =
|
|
|
|
{ /// The ID of the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Id : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The ID of the user who authored the post
|
2022-07-18 03:10:30 +00:00
|
|
|
AuthorId : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The status of the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Status : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The title of the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Title : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The permalink for the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Permalink : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// When this post was published
|
2022-07-18 03:10:30 +00:00
|
|
|
PublishedOn : Nullable<DateTime>
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// When this post was last updated
|
2022-07-18 03:10:30 +00:00
|
|
|
UpdatedOn : DateTime
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The text of the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Text : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The IDs of the categories for this post
|
2022-07-18 03:10:30 +00:00
|
|
|
CategoryIds : string list
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Tags for the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Tags : string list
|
2022-06-23 00:35:12 +00:00
|
|
|
|
2022-06-28 02:16:53 +00:00
|
|
|
/// The podcast episode information for this post
|
2022-07-18 03:10:30 +00:00
|
|
|
Episode : Episode option
|
2022-06-28 02:16:53 +00:00
|
|
|
|
2022-06-23 00:35:12 +00:00
|
|
|
/// Metadata for the post
|
2022-07-18 03:10:30 +00:00
|
|
|
Metadata : MetaItem list
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a post list item from a post
|
|
|
|
static member fromPost (webLog : WebLog) (post : Post) =
|
|
|
|
let _, extra = WebLog.hostAndPath webLog
|
|
|
|
let inTZ = WebLog.localTime webLog
|
2022-07-18 03:10:30 +00:00
|
|
|
{ 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
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// View model for displaying posts
|
|
|
|
type PostDisplay =
|
|
|
|
{ /// The posts to be displayed
|
2022-07-18 03:10:30 +00:00
|
|
|
Posts : PostListItem[]
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Author ID -> name lookup
|
2022-07-18 03:10:30 +00:00
|
|
|
Authors : MetaItem list
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A subtitle for the page
|
2022-07-18 03:10:30 +00:00
|
|
|
Subtitle : string option
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The link to view newer (more recent) posts
|
2022-07-18 03:10:30 +00:00
|
|
|
NewerLink : string option
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The name of the next newer post (single-post only)
|
2022-07-18 03:10:30 +00:00
|
|
|
NewerName : string option
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The link to view older (less recent) posts
|
2022-07-18 03:10:30 +00:00
|
|
|
OlderLink : string option
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The name of the next older post (single-post only)
|
2022-07-18 03:10:30 +00:00
|
|
|
OlderName : string option
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// View model for editing web log settings
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type SettingsModel =
|
|
|
|
{ /// The name of the web log
|
2022-07-18 03:10:30 +00:00
|
|
|
Name : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
2022-07-07 16:42:37 +00:00
|
|
|
/// The slug of the web log
|
2022-07-18 03:10:30 +00:00
|
|
|
Slug : string
|
2022-07-07 16:42:37 +00:00
|
|
|
|
2022-06-23 00:35:12 +00:00
|
|
|
/// The subtitle of the web log
|
2022-07-18 03:10:30 +00:00
|
|
|
Subtitle : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The default page
|
2022-07-18 03:10:30 +00:00
|
|
|
DefaultPage : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// How many posts should appear on index pages
|
2022-07-18 03:10:30 +00:00
|
|
|
PostsPerPage : int
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The time zone in which dates/times should be displayed
|
2022-07-18 03:10:30 +00:00
|
|
|
TimeZone : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The theme to use to display the web log
|
2022-07-18 03:10:30 +00:00
|
|
|
ThemePath : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Whether to automatically load htmx
|
2022-07-18 03:10:30 +00:00
|
|
|
AutoHtmx : bool
|
2022-07-07 16:42:37 +00:00
|
|
|
|
|
|
|
/// The default location for uploads
|
2022-07-18 03:10:30 +00:00
|
|
|
Uploads : string
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a settings model from a web log
|
|
|
|
static member fromWebLog (webLog : WebLog) =
|
2022-07-18 03:10:30 +00:00
|
|
|
{ Name = webLog.name
|
|
|
|
Slug = webLog.slug
|
|
|
|
Subtitle = defaultArg webLog.subtitle ""
|
|
|
|
DefaultPage = webLog.defaultPage
|
|
|
|
PostsPerPage = webLog.postsPerPage
|
|
|
|
TimeZone = webLog.timeZone
|
|
|
|
ThemePath = webLog.themePath
|
|
|
|
AutoHtmx = webLog.autoHtmx
|
|
|
|
Uploads = UploadDestination.toString webLog.uploads
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Update a web log with settings from the form
|
|
|
|
member this.update (webLog : WebLog) =
|
|
|
|
{ webLog with
|
2022-07-18 03:10:30 +00:00
|
|
|
name = this.Name
|
|
|
|
slug = this.Slug
|
|
|
|
subtitle = if this.Subtitle = "" then None else Some this.Subtitle
|
|
|
|
defaultPage = this.DefaultPage
|
|
|
|
postsPerPage = this.PostsPerPage
|
|
|
|
timeZone = this.TimeZone
|
|
|
|
themePath = this.ThemePath
|
|
|
|
autoHtmx = this.AutoHtmx
|
|
|
|
uploads = UploadDestination.parse this.Uploads
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-07-02 00:59:21 +00:00
|
|
|
/// View model for uploading a file
|
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type UploadFileModel =
|
|
|
|
{ /// The upload destination
|
2022-07-18 03:10:30 +00:00
|
|
|
Destination : string
|
2022-07-02 00:59:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A message displayed to the user
|
2022-06-23 00:35:12 +00:00
|
|
|
[<CLIMutable; NoComparison; NoEquality>]
|
|
|
|
type UserMessage =
|
|
|
|
{ /// The level of the message
|
2022-07-18 03:10:30 +00:00
|
|
|
Level : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// The message
|
2022-07-18 03:10:30 +00:00
|
|
|
Message : string
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// Further details about the message
|
2022-07-18 03:10:30 +00:00
|
|
|
Detail : string option
|
2022-06-23 00:35:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Functions to support user messages
|
|
|
|
module UserMessage =
|
|
|
|
|
|
|
|
/// An empty user message (use one of the others for pre-filled level)
|
2022-07-18 03:10:30 +00:00
|
|
|
let empty = { Level = ""; Message = ""; Detail = None }
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A blank success message
|
2022-07-18 03:10:30 +00:00
|
|
|
let success = { empty with Level = "success" }
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A blank informational message
|
2022-07-18 03:10:30 +00:00
|
|
|
let info = { empty with Level = "primary" }
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A blank warning message
|
2022-07-18 03:10:30 +00:00
|
|
|
let warning = { empty with Level = "warning" }
|
2022-06-23 00:35:12 +00:00
|
|
|
|
|
|
|
/// A blank error message
|
2022-07-18 03:10:30 +00:00
|
|
|
let error = { empty with Level = "danger" }
|