@@ -7,22 +7,22 @@ open MyWebLog
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Category =
|
||||
{ /// The ID of the category
|
||||
id : CategoryId
|
||||
Id : CategoryId
|
||||
|
||||
/// The ID of the web log to which the category belongs
|
||||
webLogId : WebLogId
|
||||
WebLogId : WebLogId
|
||||
|
||||
/// The displayed name
|
||||
name : string
|
||||
Name : string
|
||||
|
||||
/// The slug (used in category URLs)
|
||||
slug : string
|
||||
Slug : string
|
||||
|
||||
/// A longer description of the category
|
||||
description : string option
|
||||
Description : string option
|
||||
|
||||
/// The parent ID of this category (if a subcategory)
|
||||
parentId : CategoryId option
|
||||
ParentId : CategoryId option
|
||||
}
|
||||
|
||||
/// Functions to support categories
|
||||
@@ -30,12 +30,12 @@ module Category =
|
||||
|
||||
/// An empty category
|
||||
let empty =
|
||||
{ id = CategoryId.empty
|
||||
webLogId = WebLogId.empty
|
||||
name = ""
|
||||
slug = ""
|
||||
description = None
|
||||
parentId = None
|
||||
{ Id = CategoryId.empty
|
||||
WebLogId = WebLogId.empty
|
||||
Name = ""
|
||||
Slug = ""
|
||||
Description = None
|
||||
ParentId = None
|
||||
}
|
||||
|
||||
|
||||
@@ -43,31 +43,31 @@ module Category =
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Comment =
|
||||
{ /// The ID of the comment
|
||||
id : CommentId
|
||||
Id : CommentId
|
||||
|
||||
/// The ID of the post to which this comment applies
|
||||
postId : PostId
|
||||
PostId : PostId
|
||||
|
||||
/// The ID of the comment to which this comment is a reply
|
||||
inReplyToId : CommentId option
|
||||
InReplyToId : CommentId option
|
||||
|
||||
/// The name of the commentor
|
||||
name : string
|
||||
Name : string
|
||||
|
||||
/// The e-mail address of the commentor
|
||||
email : string
|
||||
Email : string
|
||||
|
||||
/// The URL of the commentor's personal website
|
||||
url : string option
|
||||
Url : string option
|
||||
|
||||
/// The status of the comment
|
||||
status : CommentStatus
|
||||
Status : CommentStatus
|
||||
|
||||
/// When the comment was posted
|
||||
postedOn : DateTime
|
||||
PostedOn : DateTime
|
||||
|
||||
/// The text of the comment
|
||||
text : string
|
||||
Text : string
|
||||
}
|
||||
|
||||
/// Functions to support comments
|
||||
@@ -75,15 +75,15 @@ module Comment =
|
||||
|
||||
/// An empty comment
|
||||
let empty =
|
||||
{ id = CommentId.empty
|
||||
postId = PostId.empty
|
||||
inReplyToId = None
|
||||
name = ""
|
||||
email = ""
|
||||
url = None
|
||||
status = Pending
|
||||
postedOn = DateTime.UtcNow
|
||||
text = ""
|
||||
{ Id = CommentId.empty
|
||||
PostId = PostId.empty
|
||||
InReplyToId = None
|
||||
Name = ""
|
||||
Email = ""
|
||||
Url = None
|
||||
Status = Pending
|
||||
PostedOn = DateTime.UtcNow
|
||||
Text = ""
|
||||
}
|
||||
|
||||
|
||||
@@ -91,43 +91,43 @@ module Comment =
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Page =
|
||||
{ /// The ID of this page
|
||||
id : PageId
|
||||
Id : PageId
|
||||
|
||||
/// The ID of the web log to which this page belongs
|
||||
webLogId : WebLogId
|
||||
WebLogId : WebLogId
|
||||
|
||||
/// The ID of the author of this page
|
||||
authorId : WebLogUserId
|
||||
AuthorId : WebLogUserId
|
||||
|
||||
/// The title of the page
|
||||
title : string
|
||||
Title : string
|
||||
|
||||
/// The link at which this page is displayed
|
||||
permalink : Permalink
|
||||
Permalink : Permalink
|
||||
|
||||
/// When this page was published
|
||||
publishedOn : DateTime
|
||||
PublishedOn : DateTime
|
||||
|
||||
/// When this page was last updated
|
||||
updatedOn : DateTime
|
||||
UpdatedOn : DateTime
|
||||
|
||||
/// Whether this page shows as part of the web log's navigation
|
||||
showInPageList : bool
|
||||
IsInPageList : bool
|
||||
|
||||
/// The template to use when rendering this page
|
||||
template : string option
|
||||
Template : string option
|
||||
|
||||
/// The current text of the page
|
||||
text : string
|
||||
Text : string
|
||||
|
||||
/// Metadata for this page
|
||||
metadata : MetaItem list
|
||||
Metadata : MetaItem list
|
||||
|
||||
/// Permalinks at which this page may have been previously served (useful for migrated content)
|
||||
priorPermalinks : Permalink list
|
||||
PriorPermalinks : Permalink list
|
||||
|
||||
/// Revisions of this page
|
||||
revisions : Revision list
|
||||
Revisions : Revision list
|
||||
}
|
||||
|
||||
/// Functions to support pages
|
||||
@@ -135,19 +135,19 @@ module Page =
|
||||
|
||||
/// An empty page
|
||||
let empty =
|
||||
{ id = PageId.empty
|
||||
webLogId = WebLogId.empty
|
||||
authorId = WebLogUserId.empty
|
||||
title = ""
|
||||
permalink = Permalink.empty
|
||||
publishedOn = DateTime.MinValue
|
||||
updatedOn = DateTime.MinValue
|
||||
showInPageList = false
|
||||
template = None
|
||||
text = ""
|
||||
metadata = []
|
||||
priorPermalinks = []
|
||||
revisions = []
|
||||
{ Id = PageId.empty
|
||||
WebLogId = WebLogId.empty
|
||||
AuthorId = WebLogUserId.empty
|
||||
Title = ""
|
||||
Permalink = Permalink.empty
|
||||
PublishedOn = DateTime.MinValue
|
||||
UpdatedOn = DateTime.MinValue
|
||||
IsInPageList = false
|
||||
Template = None
|
||||
Text = ""
|
||||
Metadata = []
|
||||
PriorPermalinks = []
|
||||
Revisions = []
|
||||
}
|
||||
|
||||
|
||||
@@ -155,52 +155,52 @@ module Page =
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Post =
|
||||
{ /// The ID of this post
|
||||
id : PostId
|
||||
Id : PostId
|
||||
|
||||
/// The ID of the web log to which this post belongs
|
||||
webLogId : WebLogId
|
||||
WebLogId : WebLogId
|
||||
|
||||
/// The ID of the author of this post
|
||||
authorId : WebLogUserId
|
||||
AuthorId : WebLogUserId
|
||||
|
||||
/// The status
|
||||
status : PostStatus
|
||||
Status : PostStatus
|
||||
|
||||
/// The title
|
||||
title : string
|
||||
Title : string
|
||||
|
||||
/// The link at which the post resides
|
||||
permalink : Permalink
|
||||
Permalink : Permalink
|
||||
|
||||
/// The instant on which the post was originally published
|
||||
publishedOn : DateTime option
|
||||
PublishedOn : DateTime option
|
||||
|
||||
/// The instant on which the post was last updated
|
||||
updatedOn : DateTime
|
||||
UpdatedOn : DateTime
|
||||
|
||||
/// The template to use in displaying the post
|
||||
template : string option
|
||||
Template : string option
|
||||
|
||||
/// The text of the post in HTML (ready to display) format
|
||||
text : string
|
||||
Text : string
|
||||
|
||||
/// The Ids of the categories to which this is assigned
|
||||
categoryIds : CategoryId list
|
||||
CategoryIds : CategoryId list
|
||||
|
||||
/// The tags for the post
|
||||
tags : string list
|
||||
Tags : string list
|
||||
|
||||
/// Podcast episode information for this post
|
||||
episode : Episode option
|
||||
Episode : Episode option
|
||||
|
||||
/// Metadata for the post
|
||||
metadata : MetaItem list
|
||||
Metadata : MetaItem list
|
||||
|
||||
/// Permalinks at which this post may have been previously served (useful for migrated content)
|
||||
priorPermalinks : Permalink list
|
||||
PriorPermalinks : Permalink list
|
||||
|
||||
/// The revisions for this post
|
||||
revisions : Revision list
|
||||
Revisions : Revision list
|
||||
}
|
||||
|
||||
/// Functions to support posts
|
||||
@@ -208,38 +208,38 @@ module Post =
|
||||
|
||||
/// An empty post
|
||||
let empty =
|
||||
{ id = PostId.empty
|
||||
webLogId = WebLogId.empty
|
||||
authorId = WebLogUserId.empty
|
||||
status = Draft
|
||||
title = ""
|
||||
permalink = Permalink.empty
|
||||
publishedOn = None
|
||||
updatedOn = DateTime.MinValue
|
||||
text = ""
|
||||
template = None
|
||||
categoryIds = []
|
||||
tags = []
|
||||
episode = None
|
||||
metadata = []
|
||||
priorPermalinks = []
|
||||
revisions = []
|
||||
{ Id = PostId.empty
|
||||
WebLogId = WebLogId.empty
|
||||
AuthorId = WebLogUserId.empty
|
||||
Status = Draft
|
||||
Title = ""
|
||||
Permalink = Permalink.empty
|
||||
PublishedOn = None
|
||||
UpdatedOn = DateTime.MinValue
|
||||
Text = ""
|
||||
Template = None
|
||||
CategoryIds = []
|
||||
Tags = []
|
||||
Episode = None
|
||||
Metadata = []
|
||||
PriorPermalinks = []
|
||||
Revisions = []
|
||||
}
|
||||
|
||||
|
||||
/// A mapping between a tag and its URL value, used to translate restricted characters (ex. "#1" -> "number-1")
|
||||
type TagMap =
|
||||
{ /// The ID of this tag mapping
|
||||
id : TagMapId
|
||||
Id : TagMapId
|
||||
|
||||
/// The ID of the web log to which this tag mapping belongs
|
||||
webLogId : WebLogId
|
||||
WebLogId : WebLogId
|
||||
|
||||
/// The tag which should be mapped to a different value in links
|
||||
tag : string
|
||||
Tag : string
|
||||
|
||||
/// The value by which the tag should be linked
|
||||
urlValue : string
|
||||
UrlValue : string
|
||||
}
|
||||
|
||||
/// Functions to support tag mappings
|
||||
@@ -247,26 +247,26 @@ module TagMap =
|
||||
|
||||
/// An empty tag mapping
|
||||
let empty =
|
||||
{ id = TagMapId.empty
|
||||
webLogId = WebLogId.empty
|
||||
tag = ""
|
||||
urlValue = ""
|
||||
{ Id = TagMapId.empty
|
||||
WebLogId = WebLogId.empty
|
||||
Tag = ""
|
||||
UrlValue = ""
|
||||
}
|
||||
|
||||
|
||||
/// A theme
|
||||
type Theme =
|
||||
{ /// The ID / path of the theme
|
||||
id : ThemeId
|
||||
Id : ThemeId
|
||||
|
||||
/// A long name of the theme
|
||||
name : string
|
||||
Name : string
|
||||
|
||||
/// The version of the theme
|
||||
version : string
|
||||
Version : string
|
||||
|
||||
/// The templates for this theme
|
||||
templates: ThemeTemplate list
|
||||
Templates: ThemeTemplate list
|
||||
}
|
||||
|
||||
/// Functions to support themes
|
||||
@@ -274,10 +274,10 @@ module Theme =
|
||||
|
||||
/// An empty theme
|
||||
let empty =
|
||||
{ id = ThemeId ""
|
||||
name = ""
|
||||
version = ""
|
||||
templates = []
|
||||
{ Id = ThemeId ""
|
||||
Name = ""
|
||||
Version = ""
|
||||
Templates = []
|
||||
}
|
||||
|
||||
|
||||
@@ -285,32 +285,42 @@ module Theme =
|
||||
type ThemeAsset =
|
||||
{
|
||||
/// The ID of the asset (consists of theme and path)
|
||||
id : ThemeAssetId
|
||||
Id : ThemeAssetId
|
||||
|
||||
/// The updated date (set from the file date from the ZIP archive)
|
||||
updatedOn : DateTime
|
||||
UpdatedOn : DateTime
|
||||
|
||||
/// The data for the asset
|
||||
data : byte[]
|
||||
Data : byte[]
|
||||
}
|
||||
|
||||
/// Functions to support theme assets
|
||||
module ThemeAsset =
|
||||
|
||||
/// An empty theme asset
|
||||
let empty =
|
||||
{ Id = ThemeAssetId (ThemeId "", "")
|
||||
UpdatedOn = DateTime.MinValue
|
||||
Data = [||]
|
||||
}
|
||||
|
||||
|
||||
/// An uploaded file
|
||||
type Upload =
|
||||
{ /// The ID of the upload
|
||||
id : UploadId
|
||||
Id : UploadId
|
||||
|
||||
/// The ID of the web log to which this upload belongs
|
||||
webLogId : WebLogId
|
||||
WebLogId : WebLogId
|
||||
|
||||
/// The link at which this upload is served
|
||||
path : Permalink
|
||||
Path : Permalink
|
||||
|
||||
/// The updated date/time for this upload
|
||||
updatedOn : DateTime
|
||||
UpdatedOn : DateTime
|
||||
|
||||
/// The data for the upload
|
||||
data : byte[]
|
||||
Data : byte[]
|
||||
}
|
||||
|
||||
/// Functions to support uploaded files
|
||||
@@ -318,11 +328,11 @@ module Upload =
|
||||
|
||||
/// An empty upload
|
||||
let empty = {
|
||||
id = UploadId.empty
|
||||
webLogId = WebLogId.empty
|
||||
path = Permalink.empty
|
||||
updatedOn = DateTime.MinValue
|
||||
data = [||]
|
||||
Id = UploadId.empty
|
||||
WebLogId = WebLogId.empty
|
||||
Path = Permalink.empty
|
||||
UpdatedOn = DateTime.MinValue
|
||||
Data = [||]
|
||||
}
|
||||
|
||||
|
||||
@@ -330,40 +340,40 @@ module Upload =
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type WebLog =
|
||||
{ /// The ID of the web log
|
||||
id : WebLogId
|
||||
Id : WebLogId
|
||||
|
||||
/// The name of the web log
|
||||
name : string
|
||||
Name : string
|
||||
|
||||
/// The slug of the web log
|
||||
slug : string
|
||||
Slug : string
|
||||
|
||||
/// A subtitle for the web log
|
||||
subtitle : string option
|
||||
Subtitle : string option
|
||||
|
||||
/// The default page ("posts" or a page Id)
|
||||
defaultPage : string
|
||||
DefaultPage : string
|
||||
|
||||
/// The number of posts to display on pages of posts
|
||||
postsPerPage : int
|
||||
PostsPerPage : int
|
||||
|
||||
/// The path of the theme (within /themes)
|
||||
themePath : string
|
||||
/// The ID of the theme (also the path within /themes)
|
||||
ThemeId : ThemeId
|
||||
|
||||
/// The URL base
|
||||
urlBase : string
|
||||
UrlBase : string
|
||||
|
||||
/// The time zone in which dates/times should be displayed
|
||||
timeZone : string
|
||||
TimeZone : string
|
||||
|
||||
/// The RSS options for this web log
|
||||
rss : RssOptions
|
||||
Rss : RssOptions
|
||||
|
||||
/// Whether to automatically load htmx
|
||||
autoHtmx : bool
|
||||
AutoHtmx : bool
|
||||
|
||||
/// Where uploads are placed
|
||||
uploads : UploadDestination
|
||||
Uploads : UploadDestination
|
||||
}
|
||||
|
||||
/// Functions to support web logs
|
||||
@@ -371,29 +381,29 @@ module WebLog =
|
||||
|
||||
/// An empty web log
|
||||
let empty =
|
||||
{ id = WebLogId.empty
|
||||
name = ""
|
||||
slug = ""
|
||||
subtitle = None
|
||||
defaultPage = ""
|
||||
postsPerPage = 10
|
||||
themePath = "default"
|
||||
urlBase = ""
|
||||
timeZone = ""
|
||||
rss = RssOptions.empty
|
||||
autoHtmx = false
|
||||
uploads = Database
|
||||
{ Id = WebLogId.empty
|
||||
Name = ""
|
||||
Slug = ""
|
||||
Subtitle = None
|
||||
DefaultPage = ""
|
||||
PostsPerPage = 10
|
||||
ThemeId = ThemeId "default"
|
||||
UrlBase = ""
|
||||
TimeZone = ""
|
||||
Rss = RssOptions.empty
|
||||
AutoHtmx = false
|
||||
Uploads = Database
|
||||
}
|
||||
|
||||
/// Get the host (including scheme) and extra path from the URL base
|
||||
let hostAndPath webLog =
|
||||
let scheme = webLog.urlBase.Split "://"
|
||||
let scheme = webLog.UrlBase.Split "://"
|
||||
let host = scheme[1].Split "/"
|
||||
$"{scheme[0]}://{host[0]}", if host.Length > 1 then $"""/{String.Join ("/", host |> Array.skip 1)}""" else ""
|
||||
|
||||
/// Generate an absolute URL for the given link
|
||||
let absoluteUrl webLog permalink =
|
||||
$"{webLog.urlBase}/{Permalink.toString permalink}"
|
||||
$"{webLog.UrlBase}/{Permalink.toString permalink}"
|
||||
|
||||
/// Generate a relative URL for the given link
|
||||
let relativeUrl webLog permalink =
|
||||
@@ -403,47 +413,47 @@ module WebLog =
|
||||
/// Convert a UTC date/time to the web log's local date/time
|
||||
let localTime webLog (date : DateTime) =
|
||||
TimeZoneInfo.ConvertTimeFromUtc
|
||||
(DateTime (date.Ticks, DateTimeKind.Utc), TimeZoneInfo.FindSystemTimeZoneById webLog.timeZone)
|
||||
(DateTime (date.Ticks, DateTimeKind.Utc), TimeZoneInfo.FindSystemTimeZoneById webLog.TimeZone)
|
||||
|
||||
|
||||
/// A user of the web log
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type WebLogUser =
|
||||
{ /// The ID of the user
|
||||
id : WebLogUserId
|
||||
Id : WebLogUserId
|
||||
|
||||
/// The ID of the web log to which this user belongs
|
||||
webLogId : WebLogId
|
||||
WebLogId : WebLogId
|
||||
|
||||
/// The user name (e-mail address)
|
||||
userName : string
|
||||
Email : string
|
||||
|
||||
/// The user's first name
|
||||
firstName : string
|
||||
FirstName : string
|
||||
|
||||
/// The user's last name
|
||||
lastName : string
|
||||
LastName : string
|
||||
|
||||
/// The user's preferred name
|
||||
preferredName : string
|
||||
PreferredName : string
|
||||
|
||||
/// The hash of the user's password
|
||||
passwordHash : string
|
||||
PasswordHash : string
|
||||
|
||||
/// Salt used to calculate the user's password hash
|
||||
salt : Guid
|
||||
Salt : Guid
|
||||
|
||||
/// The URL of the user's personal site
|
||||
url : string option
|
||||
Url : string option
|
||||
|
||||
/// The user's access level
|
||||
accessLevel : AccessLevel
|
||||
AccessLevel : AccessLevel
|
||||
|
||||
/// When the user was created
|
||||
createdOn : DateTime
|
||||
CreatedOn : DateTime
|
||||
|
||||
/// When the user last logged on
|
||||
lastSeenOn : DateTime option
|
||||
LastSeenOn : DateTime option
|
||||
}
|
||||
|
||||
/// Functions to support web log users
|
||||
@@ -451,27 +461,27 @@ module WebLogUser =
|
||||
|
||||
/// An empty web log user
|
||||
let empty =
|
||||
{ id = WebLogUserId.empty
|
||||
webLogId = WebLogId.empty
|
||||
userName = ""
|
||||
firstName = ""
|
||||
lastName = ""
|
||||
preferredName = ""
|
||||
passwordHash = ""
|
||||
salt = Guid.Empty
|
||||
url = None
|
||||
accessLevel = Author
|
||||
createdOn = DateTime.UnixEpoch
|
||||
lastSeenOn = None
|
||||
{ Id = WebLogUserId.empty
|
||||
WebLogId = WebLogId.empty
|
||||
Email = ""
|
||||
FirstName = ""
|
||||
LastName = ""
|
||||
PreferredName = ""
|
||||
PasswordHash = ""
|
||||
Salt = Guid.Empty
|
||||
Url = None
|
||||
AccessLevel = Author
|
||||
CreatedOn = DateTime.UnixEpoch
|
||||
LastSeenOn = None
|
||||
}
|
||||
|
||||
/// Get the user's displayed name
|
||||
let displayName user =
|
||||
let name =
|
||||
seq { match user.preferredName with "" -> user.firstName | n -> n; " "; user.lastName }
|
||||
seq { match user.PreferredName with "" -> user.FirstName | n -> n; " "; user.LastName }
|
||||
|> Seq.reduce (+)
|
||||
name.Trim ()
|
||||
|
||||
/// Does a user have the required access level?
|
||||
let hasAccess level user =
|
||||
AccessLevel.hasAccess level user.accessLevel
|
||||
AccessLevel.hasAccess level user.AccessLevel
|
||||
|
||||
@@ -8,8 +8,8 @@ module private Helpers =
|
||||
|
||||
/// Create a new ID (short GUID)
|
||||
// https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID
|
||||
let newId() =
|
||||
Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-').Substring (0, 22)
|
||||
let newId () =
|
||||
Convert.ToBase64String(Guid.NewGuid().ToByteArray ()).Replace('/', '_').Replace('+', '-').Substring (0, 22)
|
||||
|
||||
|
||||
/// A user's access level
|
||||
@@ -140,55 +140,55 @@ module ExplicitRating =
|
||||
/// A podcast episode
|
||||
type Episode =
|
||||
{ /// The URL to the media file for the episode (may be permalink)
|
||||
media : string
|
||||
Media : string
|
||||
|
||||
/// The length of the media file, in bytes
|
||||
length : int64
|
||||
Length : int64
|
||||
|
||||
/// The duration of the episode
|
||||
duration : TimeSpan option
|
||||
Duration : TimeSpan option
|
||||
|
||||
/// The media type of the file (overrides podcast default if present)
|
||||
mediaType : string option
|
||||
MediaType : string option
|
||||
|
||||
/// The URL to the image file for this episode (overrides podcast image if present, may be permalink)
|
||||
imageUrl : string option
|
||||
ImageUrl : string option
|
||||
|
||||
/// A subtitle for this episode
|
||||
subtitle : string option
|
||||
Subtitle : string option
|
||||
|
||||
/// This episode's explicit rating (overrides podcast rating if present)
|
||||
explicit : ExplicitRating option
|
||||
Explicit : ExplicitRating option
|
||||
|
||||
/// A link to a chapter file
|
||||
chapterFile : string option
|
||||
ChapterFile : string option
|
||||
|
||||
/// The MIME type for the chapter file
|
||||
chapterType : string option
|
||||
ChapterType : string option
|
||||
|
||||
/// The URL for the transcript of the episode (may be permalink)
|
||||
transcriptUrl : string option
|
||||
TranscriptUrl : string option
|
||||
|
||||
/// The MIME type of the transcript
|
||||
transcriptType : string option
|
||||
TranscriptType : string option
|
||||
|
||||
/// The language in which the transcript is written
|
||||
transcriptLang : string option
|
||||
TranscriptLang : string option
|
||||
|
||||
/// If true, the transcript will be declared (in the feed) to be a captions file
|
||||
transcriptCaptions : bool option
|
||||
TranscriptCaptions : bool option
|
||||
|
||||
/// The season number (for serialized podcasts)
|
||||
seasonNumber : int option
|
||||
SeasonNumber : int option
|
||||
|
||||
/// A description of the season
|
||||
seasonDescription : string option
|
||||
SeasonDescription : string option
|
||||
|
||||
/// The episode number
|
||||
episodeNumber : double option
|
||||
EpisodeNumber : double option
|
||||
|
||||
/// A description of the episode
|
||||
episodeDescription : string option
|
||||
EpisodeDescription : string option
|
||||
}
|
||||
|
||||
/// Functions to support episodes
|
||||
@@ -196,23 +196,23 @@ module Episode =
|
||||
|
||||
/// An empty episode
|
||||
let empty = {
|
||||
media = ""
|
||||
length = 0L
|
||||
duration = None
|
||||
mediaType = None
|
||||
imageUrl = None
|
||||
subtitle = None
|
||||
explicit = None
|
||||
chapterFile = None
|
||||
chapterType = None
|
||||
transcriptUrl = None
|
||||
transcriptType = None
|
||||
transcriptLang = None
|
||||
transcriptCaptions = None
|
||||
seasonNumber = None
|
||||
seasonDescription = None
|
||||
episodeNumber = None
|
||||
episodeDescription = None
|
||||
Media = ""
|
||||
Length = 0L
|
||||
Duration = None
|
||||
MediaType = None
|
||||
ImageUrl = None
|
||||
Subtitle = None
|
||||
Explicit = None
|
||||
ChapterFile = None
|
||||
ChapterType = None
|
||||
TranscriptUrl = None
|
||||
TranscriptType = None
|
||||
TranscriptLang = None
|
||||
TranscriptCaptions = None
|
||||
SeasonNumber = None
|
||||
SeasonDescription = None
|
||||
EpisodeNumber = None
|
||||
EpisodeDescription = None
|
||||
}
|
||||
|
||||
|
||||
@@ -256,10 +256,10 @@ module MarkupText =
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type MetaItem =
|
||||
{ /// The name of the metadata value
|
||||
name : string
|
||||
Name : string
|
||||
|
||||
/// The metadata value
|
||||
value : string
|
||||
Value : string
|
||||
}
|
||||
|
||||
/// Functions to support metadata items
|
||||
@@ -267,17 +267,17 @@ module MetaItem =
|
||||
|
||||
/// An empty metadata item
|
||||
let empty =
|
||||
{ name = ""; value = "" }
|
||||
{ Name = ""; Value = "" }
|
||||
|
||||
|
||||
/// A revision of a page or post
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Revision =
|
||||
{ /// When this revision was saved
|
||||
asOf : DateTime
|
||||
AsOf : DateTime
|
||||
|
||||
/// The text of the revision
|
||||
text : MarkupText
|
||||
Text : MarkupText
|
||||
}
|
||||
|
||||
/// Functions to support revisions
|
||||
@@ -285,8 +285,8 @@ module Revision =
|
||||
|
||||
/// An empty revision
|
||||
let empty =
|
||||
{ asOf = DateTime.UtcNow
|
||||
text = Html ""
|
||||
{ AsOf = DateTime.UtcNow
|
||||
Text = Html ""
|
||||
}
|
||||
|
||||
|
||||
@@ -436,68 +436,68 @@ module CustomFeedSource =
|
||||
/// Options for a feed that describes a podcast
|
||||
type PodcastOptions =
|
||||
{ /// The title of the podcast
|
||||
title : string
|
||||
Title : string
|
||||
|
||||
/// A subtitle for the podcast
|
||||
subtitle : string option
|
||||
Subtitle : string option
|
||||
|
||||
/// The number of items in the podcast feed
|
||||
itemsInFeed : int
|
||||
ItemsInFeed : int
|
||||
|
||||
/// A summary of the podcast (iTunes field)
|
||||
summary : string
|
||||
Summary : string
|
||||
|
||||
/// The display name of the podcast author (iTunes field)
|
||||
displayedAuthor : string
|
||||
DisplayedAuthor : string
|
||||
|
||||
/// The e-mail address of the user who registered the podcast at iTunes
|
||||
email : string
|
||||
Email : string
|
||||
|
||||
/// The link to the image for the podcast
|
||||
imageUrl : Permalink
|
||||
ImageUrl : Permalink
|
||||
|
||||
/// The category from iTunes under which this podcast is categorized
|
||||
iTunesCategory : string
|
||||
/// The category from Apple Podcasts (iTunes) under which this podcast is categorized
|
||||
AppleCategory : string
|
||||
|
||||
/// A further refinement of the categorization of this podcast (iTunes field / values)
|
||||
iTunesSubcategory : string option
|
||||
/// A further refinement of the categorization of this podcast (Apple Podcasts/iTunes field / values)
|
||||
AppleSubcategory : string option
|
||||
|
||||
/// The explictness rating (iTunes field)
|
||||
explicit : ExplicitRating
|
||||
Explicit : ExplicitRating
|
||||
|
||||
/// The default media type for files in this podcast
|
||||
defaultMediaType : string option
|
||||
DefaultMediaType : string option
|
||||
|
||||
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
|
||||
mediaBaseUrl : string option
|
||||
MediaBaseUrl : string option
|
||||
|
||||
/// A GUID for this podcast
|
||||
guid : Guid option
|
||||
PodcastGuid : Guid option
|
||||
|
||||
/// A URL at which information on supporting the podcast may be found (supports permalinks)
|
||||
fundingUrl : string option
|
||||
FundingUrl : string option
|
||||
|
||||
/// The text to be displayed in the funding item within the feed
|
||||
fundingText : string option
|
||||
FundingText : string option
|
||||
|
||||
/// The medium (what the podcast IS, not what it is ABOUT)
|
||||
medium : PodcastMedium option
|
||||
Medium : PodcastMedium option
|
||||
}
|
||||
|
||||
|
||||
/// A custom feed
|
||||
type CustomFeed =
|
||||
{ /// The ID of the custom feed
|
||||
id : CustomFeedId
|
||||
Id : CustomFeedId
|
||||
|
||||
/// The source for the custom feed
|
||||
source : CustomFeedSource
|
||||
Source : CustomFeedSource
|
||||
|
||||
/// The path for the custom feed
|
||||
path : Permalink
|
||||
Path : Permalink
|
||||
|
||||
/// Podcast options, if the feed defines a podcast
|
||||
podcast : PodcastOptions option
|
||||
Podcast : PodcastOptions option
|
||||
}
|
||||
|
||||
/// Functions to support custom feeds
|
||||
@@ -505,10 +505,10 @@ module CustomFeed =
|
||||
|
||||
/// An empty custom feed
|
||||
let empty =
|
||||
{ id = CustomFeedId ""
|
||||
source = Category (CategoryId "")
|
||||
path = Permalink ""
|
||||
podcast = None
|
||||
{ Id = CustomFeedId ""
|
||||
Source = Category (CategoryId "")
|
||||
Path = Permalink ""
|
||||
Podcast = None
|
||||
}
|
||||
|
||||
|
||||
@@ -516,25 +516,25 @@ module CustomFeed =
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type RssOptions =
|
||||
{ /// Whether the site feed of posts is enabled
|
||||
feedEnabled : bool
|
||||
IsFeedEnabled : bool
|
||||
|
||||
/// The name of the file generated for the site feed
|
||||
feedName : string
|
||||
FeedName : string
|
||||
|
||||
/// Override the "posts per page" setting for the site feed
|
||||
itemsInFeed : int option
|
||||
ItemsInFeed : int option
|
||||
|
||||
/// Whether feeds are enabled for all categories
|
||||
categoryEnabled : bool
|
||||
IsCategoryEnabled : bool
|
||||
|
||||
/// Whether feeds are enabled for all tags
|
||||
tagEnabled : bool
|
||||
IsTagEnabled : bool
|
||||
|
||||
/// A copyright string to be placed in all feeds
|
||||
copyright : string option
|
||||
Copyright : string option
|
||||
|
||||
/// Custom feeds for this web log
|
||||
customFeeds: CustomFeed list
|
||||
CustomFeeds: CustomFeed list
|
||||
}
|
||||
|
||||
/// Functions to support RSS options
|
||||
@@ -542,13 +542,13 @@ module RssOptions =
|
||||
|
||||
/// An empty set of RSS options
|
||||
let empty =
|
||||
{ feedEnabled = true
|
||||
feedName = "feed.xml"
|
||||
itemsInFeed = None
|
||||
categoryEnabled = true
|
||||
tagEnabled = true
|
||||
copyright = None
|
||||
customFeeds = []
|
||||
{ IsFeedEnabled = true
|
||||
FeedName = "feed.xml"
|
||||
ItemsInFeed = None
|
||||
IsCategoryEnabled = true
|
||||
IsTagEnabled = true
|
||||
Copyright = None
|
||||
CustomFeeds = []
|
||||
}
|
||||
|
||||
|
||||
@@ -594,10 +594,10 @@ module ThemeAssetId =
|
||||
/// A template for a theme
|
||||
type ThemeTemplate =
|
||||
{ /// The name of the template
|
||||
name : string
|
||||
Name : string
|
||||
|
||||
/// The text of the template
|
||||
text : string
|
||||
Text : string
|
||||
}
|
||||
|
||||
|
||||
@@ -610,13 +610,13 @@ type UploadDestination =
|
||||
module UploadDestination =
|
||||
|
||||
/// Convert an upload destination to its string representation
|
||||
let toString = function Database -> "database" | Disk -> "disk"
|
||||
let toString = function Database -> "Database" | Disk -> "Disk"
|
||||
|
||||
/// Parse an upload destination from its string representation
|
||||
let parse value =
|
||||
match value with
|
||||
| "database" -> Database
|
||||
| "disk" -> Disk
|
||||
| "Database" -> Database
|
||||
| "Disk" -> Disk
|
||||
| it -> invalidOp $"{it} is not a valid upload destination"
|
||||
|
||||
|
||||
|
||||
@@ -76,13 +76,13 @@ type DisplayCustomFeed =
|
||||
/// Create a display version from a custom feed
|
||||
static member fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed =
|
||||
let source =
|
||||
match feed.source with
|
||||
match feed.Source with
|
||||
| Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}"
|
||||
| Tag tag -> $"Tag: {tag}"
|
||||
{ Id = CustomFeedId.toString feed.id
|
||||
{ Id = CustomFeedId.toString feed.Id
|
||||
Source = source
|
||||
Path = Permalink.toString feed.path
|
||||
IsPodcast = Option.isSome feed.podcast
|
||||
Path = Permalink.toString feed.Path
|
||||
IsPodcast = Option.isSome feed.Podcast
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ type DisplayPage =
|
||||
UpdatedOn : DateTime
|
||||
|
||||
/// Whether this page shows as part of the web log's navigation
|
||||
ShowInPageList : bool
|
||||
IsInPageList : bool
|
||||
|
||||
/// Is this the default page?
|
||||
IsDefault : bool
|
||||
@@ -122,33 +122,33 @@ type DisplayPage =
|
||||
|
||||
/// Create a minimal display page (no text or metadata) from a database page
|
||||
static member fromPageMinimal webLog (page : Page) =
|
||||
let pageId = PageId.toString page.id
|
||||
{ Id = pageId
|
||||
AuthorId = WebLogUserId.toString page.authorId
|
||||
Title = page.title
|
||||
Permalink = Permalink.toString page.permalink
|
||||
PublishedOn = page.publishedOn
|
||||
UpdatedOn = page.updatedOn
|
||||
ShowInPageList = page.showInPageList
|
||||
IsDefault = pageId = webLog.defaultPage
|
||||
Text = ""
|
||||
Metadata = []
|
||||
let pageId = PageId.toString page.Id
|
||||
{ Id = pageId
|
||||
AuthorId = WebLogUserId.toString page.AuthorId
|
||||
Title = page.Title
|
||||
Permalink = Permalink.toString page.Permalink
|
||||
PublishedOn = page.PublishedOn
|
||||
UpdatedOn = page.UpdatedOn
|
||||
IsInPageList = page.IsInPageList
|
||||
IsDefault = pageId = webLog.DefaultPage
|
||||
Text = ""
|
||||
Metadata = []
|
||||
}
|
||||
|
||||
/// Create a display page from a database page
|
||||
static member fromPage webLog (page : Page) =
|
||||
let _, extra = WebLog.hostAndPath webLog
|
||||
let pageId = PageId.toString page.id
|
||||
{ Id = pageId
|
||||
AuthorId = WebLogUserId.toString page.authorId
|
||||
Title = page.title
|
||||
Permalink = Permalink.toString page.permalink
|
||||
PublishedOn = page.publishedOn
|
||||
UpdatedOn = page.updatedOn
|
||||
ShowInPageList = page.showInPageList
|
||||
IsDefault = pageId = webLog.defaultPage
|
||||
Text = if extra = "" then page.text else page.text.Replace ("href=\"/", $"href=\"{extra}/")
|
||||
Metadata = page.metadata
|
||||
let pageId = PageId.toString page.Id
|
||||
{ Id = pageId
|
||||
AuthorId = WebLogUserId.toString page.AuthorId
|
||||
Title = page.Title
|
||||
Permalink = Permalink.toString page.Permalink
|
||||
PublishedOn = page.PublishedOn
|
||||
UpdatedOn = page.UpdatedOn
|
||||
IsInPageList = page.IsInPageList
|
||||
IsDefault = pageId = webLog.DefaultPage
|
||||
Text = if extra = "" then page.Text else page.Text.Replace ("href=\"/", $"href=\"{extra}/")
|
||||
Metadata = page.Metadata
|
||||
}
|
||||
|
||||
|
||||
@@ -168,9 +168,9 @@ with
|
||||
|
||||
/// Create a display revision from an actual revision
|
||||
static member fromRevision webLog (rev : Revision) =
|
||||
{ AsOf = rev.asOf
|
||||
AsOfLocal = WebLog.localTime webLog rev.asOf
|
||||
Format = MarkupText.sourceType rev.text
|
||||
{ AsOf = rev.AsOf
|
||||
AsOfLocal = WebLog.localTime webLog rev.AsOf
|
||||
Format = MarkupText.sourceType rev.Text
|
||||
}
|
||||
|
||||
|
||||
@@ -197,12 +197,12 @@ type DisplayUpload =
|
||||
|
||||
/// Create a display uploaded file
|
||||
static member fromUpload webLog source (upload : Upload) =
|
||||
let path = Permalink.toString upload.path
|
||||
let path = Permalink.toString upload.Path
|
||||
let name = Path.GetFileName path
|
||||
{ Id = UploadId.toString upload.id
|
||||
{ Id = UploadId.toString upload.Id
|
||||
Name = name
|
||||
Path = path.Replace (name, "")
|
||||
UpdatedOn = Some (WebLog.localTime webLog upload.updatedOn)
|
||||
UpdatedOn = Some (WebLog.localTime webLog upload.UpdatedOn)
|
||||
Source = UploadDestination.toString source
|
||||
}
|
||||
|
||||
@@ -228,11 +228,11 @@ type EditCategoryModel =
|
||||
|
||||
/// Create an edit model from an existing category
|
||||
static member fromCategory (cat : Category) =
|
||||
{ CategoryId = CategoryId.toString cat.id
|
||||
Name = cat.name
|
||||
Slug = cat.slug
|
||||
Description = defaultArg cat.description ""
|
||||
ParentId = cat.parentId |> Option.map CategoryId.toString |> Option.defaultValue ""
|
||||
{ CategoryId = CategoryId.toString cat.Id
|
||||
Name = cat.Name
|
||||
Slug = cat.Slug
|
||||
Description = defaultArg cat.Description ""
|
||||
ParentId = cat.ParentId |> Option.map CategoryId.toString |> Option.defaultValue ""
|
||||
}
|
||||
|
||||
|
||||
@@ -275,11 +275,11 @@ type EditCustomFeedModel =
|
||||
/// The link to the image for the podcast
|
||||
ImageUrl : string
|
||||
|
||||
/// The category from iTunes under which this podcast is categorized
|
||||
iTunesCategory : string
|
||||
/// The category from Apple Podcasts (iTunes) under which this podcast is categorized
|
||||
AppleCategory : string
|
||||
|
||||
/// A further refinement of the categorization of this podcast (iTunes field / values)
|
||||
iTunesSubcategory : string
|
||||
/// A further refinement of the categorization of this podcast (Apple Podcasts/iTunes field / values)
|
||||
AppleSubcategory : string
|
||||
|
||||
/// The explictness rating (iTunes field)
|
||||
Explicit : string
|
||||
@@ -305,92 +305,122 @@ type EditCustomFeedModel =
|
||||
|
||||
/// An empty custom feed model
|
||||
static member empty =
|
||||
{ Id = ""
|
||||
SourceType = "category"
|
||||
SourceValue = ""
|
||||
Path = ""
|
||||
IsPodcast = false
|
||||
Title = ""
|
||||
Subtitle = ""
|
||||
ItemsInFeed = 25
|
||||
Summary = ""
|
||||
DisplayedAuthor = ""
|
||||
Email = ""
|
||||
ImageUrl = ""
|
||||
iTunesCategory = ""
|
||||
iTunesSubcategory = ""
|
||||
Explicit = "no"
|
||||
DefaultMediaType = "audio/mpeg"
|
||||
MediaBaseUrl = ""
|
||||
FundingUrl = ""
|
||||
FundingText = ""
|
||||
PodcastGuid = ""
|
||||
Medium = ""
|
||||
{ Id = ""
|
||||
SourceType = "category"
|
||||
SourceValue = ""
|
||||
Path = ""
|
||||
IsPodcast = false
|
||||
Title = ""
|
||||
Subtitle = ""
|
||||
ItemsInFeed = 25
|
||||
Summary = ""
|
||||
DisplayedAuthor = ""
|
||||
Email = ""
|
||||
ImageUrl = ""
|
||||
AppleCategory = ""
|
||||
AppleSubcategory = ""
|
||||
Explicit = "no"
|
||||
DefaultMediaType = "audio/mpeg"
|
||||
MediaBaseUrl = ""
|
||||
FundingUrl = ""
|
||||
FundingText = ""
|
||||
PodcastGuid = ""
|
||||
Medium = ""
|
||||
}
|
||||
|
||||
/// Create a model from a custom feed
|
||||
static member fromFeed (feed : CustomFeed) =
|
||||
let rss =
|
||||
{ EditCustomFeedModel.empty with
|
||||
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
|
||||
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
|
||||
}
|
||||
match feed.podcast with
|
||||
match feed.Podcast with
|
||||
| Some p ->
|
||||
{ rss with
|
||||
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
|
||||
|> Option.map (fun it -> it.ToString().ToLowerInvariant ())
|
||||
|> Option.defaultValue ""
|
||||
Medium = p.medium |> Option.map PodcastMedium.toString |> Option.defaultValue ""
|
||||
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
|
||||
AppleCategory = p.AppleCategory
|
||||
AppleSubcategory = defaultArg p.AppleSubcategory ""
|
||||
Explicit = ExplicitRating.toString p.Explicit
|
||||
DefaultMediaType = defaultArg p.DefaultMediaType ""
|
||||
MediaBaseUrl = defaultArg p.MediaBaseUrl ""
|
||||
FundingUrl = defaultArg p.FundingUrl ""
|
||||
FundingText = defaultArg p.FundingText ""
|
||||
PodcastGuid = p.PodcastGuid
|
||||
|> Option.map (fun it -> it.ToString().ToLowerInvariant ())
|
||||
|> Option.defaultValue ""
|
||||
Medium = p.Medium |> Option.map PodcastMedium.toString |> Option.defaultValue ""
|
||||
}
|
||||
| None -> rss
|
||||
|
||||
/// Update a feed with values from this model
|
||||
member this.updateFeed (feed : CustomFeed) =
|
||||
member this.UpdateFeed (feed : CustomFeed) =
|
||||
{ feed with
|
||||
source = if this.SourceType = "tag" then Tag this.SourceValue else Category (CategoryId this.SourceValue)
|
||||
path = Permalink this.Path
|
||||
podcast =
|
||||
Source = if this.SourceType = "tag" then Tag this.SourceValue else Category (CategoryId this.SourceValue)
|
||||
Path = Permalink this.Path
|
||||
Podcast =
|
||||
if this.IsPodcast then
|
||||
Some {
|
||||
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
|
||||
Title = this.Title
|
||||
Subtitle = noneIfBlank this.Subtitle
|
||||
ItemsInFeed = this.ItemsInFeed
|
||||
Summary = this.Summary
|
||||
DisplayedAuthor = this.DisplayedAuthor
|
||||
Email = this.Email
|
||||
ImageUrl = Permalink this.ImageUrl
|
||||
AppleCategory = this.AppleCategory
|
||||
AppleSubcategory = noneIfBlank this.AppleSubcategory
|
||||
Explicit = ExplicitRating.parse this.Explicit
|
||||
DefaultMediaType = noneIfBlank this.DefaultMediaType
|
||||
MediaBaseUrl = noneIfBlank this.MediaBaseUrl
|
||||
PodcastGuid = noneIfBlank this.PodcastGuid |> Option.map Guid.Parse
|
||||
FundingUrl = noneIfBlank this.FundingUrl
|
||||
FundingText = noneIfBlank this.FundingText
|
||||
Medium = noneIfBlank this.Medium |> Option.map PodcastMedium.parse
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// View model for a user to edit their own information
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditMyInfoModel =
|
||||
{ /// The user's first name
|
||||
FirstName : string
|
||||
|
||||
/// The user's last name
|
||||
LastName : string
|
||||
|
||||
/// The user's preferred name
|
||||
PreferredName : string
|
||||
|
||||
/// A new password for the user
|
||||
NewPassword : string
|
||||
|
||||
/// A new password for the user, confirmed
|
||||
NewPasswordConfirm : string
|
||||
}
|
||||
|
||||
/// Create an edit model from a user
|
||||
static member fromUser (user : WebLogUser) =
|
||||
{ FirstName = user.FirstName
|
||||
LastName = user.LastName
|
||||
PreferredName = user.PreferredName
|
||||
NewPassword = ""
|
||||
NewPasswordConfirm = ""
|
||||
}
|
||||
|
||||
|
||||
/// View model to edit a page
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditPageModel =
|
||||
@@ -425,19 +455,19 @@ type EditPageModel =
|
||||
/// 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
|
||||
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
|
||||
{ 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
|
||||
let page = if page.Metadata |> List.isEmpty then { page with Metadata = [ MetaItem.empty ] } else page
|
||||
{ PageId = PageId.toString page.Id
|
||||
Title = page.Title
|
||||
Permalink = Permalink.toString page.Permalink
|
||||
Template = defaultArg page.Template ""
|
||||
IsShownInPageList = page.IsInPageList
|
||||
Source = MarkupText.sourceType latest.Text
|
||||
Text = MarkupText.text latest.Text
|
||||
MetaNames = page.Metadata |> List.map (fun m -> m.Name) |> Array.ofList
|
||||
MetaValues = page.Metadata |> List.map (fun m -> m.Value) |> Array.ofList
|
||||
}
|
||||
|
||||
|
||||
@@ -547,94 +577,94 @@ type EditPostModel =
|
||||
/// 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
|
||||
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
|
||||
let episode = defaultArg post.episode Episode.empty
|
||||
{ PostId = PostId.toString post.id
|
||||
Title = post.title
|
||||
Permalink = Permalink.toString post.permalink
|
||||
Source = MarkupText.sourceType latest.text
|
||||
Text = MarkupText.text latest.text
|
||||
Tags = String.Join (", ", post.tags)
|
||||
Template = defaultArg post.template ""
|
||||
CategoryIds = post.categoryIds |> List.map CategoryId.toString |> Array.ofList
|
||||
Status = PostStatus.toString post.status
|
||||
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.empty ] } else post
|
||||
let episode = defaultArg post.Episode Episode.empty
|
||||
{ PostId = PostId.toString post.Id
|
||||
Title = post.Title
|
||||
Permalink = Permalink.toString post.Permalink
|
||||
Source = MarkupText.sourceType latest.Text
|
||||
Text = MarkupText.text latest.Text
|
||||
Tags = String.Join (", ", post.Tags)
|
||||
Template = defaultArg post.Template ""
|
||||
CategoryIds = post.CategoryIds |> List.map CategoryId.toString |> Array.ofList
|
||||
Status = PostStatus.toString post.Status
|
||||
DoPublish = false
|
||||
MetaNames = post.metadata |> List.map (fun m -> m.name) |> Array.ofList
|
||||
MetaValues = post.metadata |> List.map (fun m -> m.value) |> Array.ofList
|
||||
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
|
||||
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 ""
|
||||
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 ""
|
||||
}
|
||||
|
||||
/// Update a post with values from the submitted form
|
||||
member this.updatePost (post : Post) (revision : Revision) now =
|
||||
member this.UpdatePost (post : Post) (revision : Revision) now =
|
||||
{ post with
|
||||
title = this.Title
|
||||
permalink = Permalink this.Permalink
|
||||
publishedOn = if this.DoPublish then Some now else post.publishedOn
|
||||
updatedOn = now
|
||||
text = MarkupText.toHtml revision.text
|
||||
tags = this.Tags.Split ","
|
||||
Title = this.Title
|
||||
Permalink = Permalink this.Permalink
|
||||
PublishedOn = if this.DoPublish then Some now else post.PublishedOn
|
||||
UpdatedOn = now
|
||||
Text = MarkupText.toHtml revision.Text
|
||||
Tags = this.Tags.Split ","
|
||||
|> Seq.ofArray
|
||||
|> Seq.map (fun it -> it.Trim().ToLower ())
|
||||
|> Seq.filter (fun it -> it <> "")
|
||||
|> Seq.sort
|
||||
|> List.ofSeq
|
||||
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
|
||||
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
|
||||
|> 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 ()}")
|
||||
|> 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 =
|
||||
Revisions = match post.Revisions |> List.tryHead with
|
||||
| Some r when r.Text = revision.Text -> post.Revisions
|
||||
| _ -> revision :: post.Revisions
|
||||
Episode =
|
||||
if this.IsEpisode then
|
||||
Some {
|
||||
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
|
||||
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
|
||||
| Some it when it = 0.0 -> None
|
||||
| Some it -> Some (double it)
|
||||
| None -> None
|
||||
episodeDescription = noneIfBlank this.EpisodeDescription
|
||||
EpisodeDescription = noneIfBlank this.EpisodeDescription
|
||||
}
|
||||
else
|
||||
None
|
||||
@@ -665,23 +695,23 @@ type EditRssModel =
|
||||
|
||||
/// Create an edit model from a set of RSS options
|
||||
static member fromRssOptions (rss : RssOptions) =
|
||||
{ IsFeedEnabled = rss.feedEnabled
|
||||
FeedName = rss.feedName
|
||||
ItemsInFeed = defaultArg rss.itemsInFeed 0
|
||||
IsCategoryEnabled = rss.categoryEnabled
|
||||
IsTagEnabled = rss.tagEnabled
|
||||
Copyright = defaultArg rss.copyright ""
|
||||
{ IsFeedEnabled = rss.IsFeedEnabled
|
||||
FeedName = rss.FeedName
|
||||
ItemsInFeed = defaultArg rss.ItemsInFeed 0
|
||||
IsCategoryEnabled = rss.IsCategoryEnabled
|
||||
IsTagEnabled = rss.IsTagEnabled
|
||||
Copyright = defaultArg rss.Copyright ""
|
||||
}
|
||||
|
||||
/// Update RSS options from values in this mode
|
||||
member this.updateOptions (rss : RssOptions) =
|
||||
member this.UpdateOptions (rss : RssOptions) =
|
||||
{ rss with
|
||||
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
|
||||
IsFeedEnabled = this.IsFeedEnabled
|
||||
FeedName = this.FeedName
|
||||
ItemsInFeed = if this.ItemsInFeed = 0 then None else Some this.ItemsInFeed
|
||||
IsCategoryEnabled = this.IsCategoryEnabled
|
||||
IsTagEnabled = this.IsTagEnabled
|
||||
Copyright = noneIfBlank this.Copyright
|
||||
}
|
||||
|
||||
|
||||
@@ -703,37 +733,9 @@ type EditTagMapModel =
|
||||
|
||||
/// Create an edit model from the tag mapping
|
||||
static member fromMapping (tagMap : TagMap) : EditTagMapModel =
|
||||
{ Id = TagMapId.toString tagMap.id
|
||||
Tag = tagMap.tag
|
||||
UrlValue = tagMap.urlValue
|
||||
}
|
||||
|
||||
|
||||
/// View model to edit a user
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditUserModel =
|
||||
{ /// The user's first name
|
||||
FirstName : string
|
||||
|
||||
/// The user's last name
|
||||
LastName : string
|
||||
|
||||
/// The user's preferred name
|
||||
PreferredName : string
|
||||
|
||||
/// A new password for the user
|
||||
NewPassword : string
|
||||
|
||||
/// A new password for the user, confirmed
|
||||
NewPasswordConfirm : string
|
||||
}
|
||||
/// Create an edit model from a user
|
||||
static member fromUser (user : WebLogUser) =
|
||||
{ FirstName = user.firstName
|
||||
LastName = user.lastName
|
||||
PreferredName = user.preferredName
|
||||
NewPassword = ""
|
||||
NewPasswordConfirm = ""
|
||||
{ Id = TagMapId.toString tagMap.Id
|
||||
Tag = tagMap.Tag
|
||||
UrlValue = tagMap.UrlValue
|
||||
}
|
||||
|
||||
|
||||
@@ -776,20 +778,20 @@ type ManagePermalinksModel =
|
||||
|
||||
/// Create a permalink model from a page
|
||||
static member fromPage (pg : Page) =
|
||||
{ Id = PageId.toString pg.id
|
||||
{ Id = PageId.toString pg.Id
|
||||
Entity = "page"
|
||||
CurrentTitle = pg.title
|
||||
CurrentPermalink = Permalink.toString pg.permalink
|
||||
Prior = pg.priorPermalinks |> List.map Permalink.toString |> Array.ofList
|
||||
CurrentTitle = pg.Title
|
||||
CurrentPermalink = Permalink.toString pg.Permalink
|
||||
Prior = pg.PriorPermalinks |> List.map Permalink.toString |> Array.ofList
|
||||
}
|
||||
|
||||
/// Create a permalink model from a post
|
||||
static member fromPost (post : Post) =
|
||||
{ Id = PostId.toString post.id
|
||||
{ Id = PostId.toString post.Id
|
||||
Entity = "post"
|
||||
CurrentTitle = post.title
|
||||
CurrentPermalink = Permalink.toString post.permalink
|
||||
Prior = post.priorPermalinks |> List.map Permalink.toString |> Array.ofList
|
||||
CurrentTitle = post.Title
|
||||
CurrentPermalink = Permalink.toString post.Permalink
|
||||
Prior = post.PriorPermalinks |> List.map Permalink.toString |> Array.ofList
|
||||
}
|
||||
|
||||
|
||||
@@ -811,18 +813,18 @@ type ManageRevisionsModel =
|
||||
|
||||
/// Create a revision model from a page
|
||||
static member fromPage webLog (pg : Page) =
|
||||
{ Id = PageId.toString pg.id
|
||||
{ Id = PageId.toString pg.Id
|
||||
Entity = "page"
|
||||
CurrentTitle = pg.title
|
||||
Revisions = pg.revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList
|
||||
CurrentTitle = pg.Title
|
||||
Revisions = pg.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList
|
||||
}
|
||||
|
||||
/// Create a revision model from a post
|
||||
static member fromPost webLog (post : Post) =
|
||||
{ Id = PostId.toString post.id
|
||||
{ Id = PostId.toString post.Id
|
||||
Entity = "post"
|
||||
CurrentTitle = post.title
|
||||
Revisions = post.revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList
|
||||
CurrentTitle = post.Title
|
||||
Revisions = post.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList
|
||||
}
|
||||
|
||||
|
||||
@@ -870,18 +872,18 @@ type PostListItem =
|
||||
static member fromPost (webLog : WebLog) (post : Post) =
|
||||
let _, extra = WebLog.hostAndPath webLog
|
||||
let inTZ = WebLog.localTime webLog
|
||||
{ Id = PostId.toString post.id
|
||||
AuthorId = WebLogUserId.toString post.authorId
|
||||
Status = PostStatus.toString post.status
|
||||
Title = post.title
|
||||
Permalink = Permalink.toString post.permalink
|
||||
PublishedOn = post.publishedOn |> Option.map inTZ |> Option.toNullable
|
||||
UpdatedOn = inTZ post.updatedOn
|
||||
Text = if extra = "" then post.text else post.text.Replace ("href=\"/", $"href=\"{extra}/")
|
||||
CategoryIds = post.categoryIds |> List.map CategoryId.toString
|
||||
Tags = post.tags
|
||||
Episode = post.episode
|
||||
Metadata = post.metadata
|
||||
{ Id = PostId.toString post.Id
|
||||
AuthorId = WebLogUserId.toString post.AuthorId
|
||||
Status = PostStatus.toString post.Status
|
||||
Title = post.Title
|
||||
Permalink = Permalink.toString post.Permalink
|
||||
PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable
|
||||
UpdatedOn = inTZ post.UpdatedOn
|
||||
Text = if extra = "" then post.Text else post.Text.Replace ("href=\"/", $"href=\"{extra}/")
|
||||
CategoryIds = post.CategoryIds |> List.map CategoryId.toString
|
||||
Tags = post.Tags
|
||||
Episode = post.Episode
|
||||
Metadata = post.Metadata
|
||||
}
|
||||
|
||||
|
||||
@@ -932,7 +934,7 @@ type SettingsModel =
|
||||
TimeZone : string
|
||||
|
||||
/// The theme to use to display the web log
|
||||
ThemePath : string
|
||||
ThemeId : string
|
||||
|
||||
/// Whether to automatically load htmx
|
||||
AutoHtmx : bool
|
||||
@@ -943,29 +945,29 @@ type SettingsModel =
|
||||
|
||||
/// Create a settings model from a web log
|
||||
static member fromWebLog (webLog : WebLog) =
|
||||
{ Name = webLog.name
|
||||
Slug = webLog.slug
|
||||
Subtitle = defaultArg webLog.subtitle ""
|
||||
DefaultPage = webLog.defaultPage
|
||||
PostsPerPage = webLog.postsPerPage
|
||||
TimeZone = webLog.timeZone
|
||||
ThemePath = webLog.themePath
|
||||
AutoHtmx = webLog.autoHtmx
|
||||
Uploads = UploadDestination.toString webLog.uploads
|
||||
{ Name = webLog.Name
|
||||
Slug = webLog.Slug
|
||||
Subtitle = defaultArg webLog.Subtitle ""
|
||||
DefaultPage = webLog.DefaultPage
|
||||
PostsPerPage = webLog.PostsPerPage
|
||||
TimeZone = webLog.TimeZone
|
||||
ThemeId = ThemeId.toString webLog.ThemeId
|
||||
AutoHtmx = webLog.AutoHtmx
|
||||
Uploads = UploadDestination.toString webLog.Uploads
|
||||
}
|
||||
|
||||
/// Update a web log with settings from the form
|
||||
member this.update (webLog : WebLog) =
|
||||
{ webLog with
|
||||
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
|
||||
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
|
||||
ThemeId = ThemeId this.ThemeId
|
||||
AutoHtmx = this.AutoHtmx
|
||||
Uploads = UploadDestination.parse this.Uploads
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user