Version 2, ready for beta
This commit was merged in pull request #1.
This commit is contained in:
427
src/MyWebLog.Domain/DataTypes.fs
Normal file
427
src/MyWebLog.Domain/DataTypes.fs
Normal file
@@ -0,0 +1,427 @@
|
||||
namespace MyWebLog
|
||||
|
||||
open System
|
||||
open MyWebLog
|
||||
|
||||
/// A category under which a post may be identified
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Category =
|
||||
{ /// The ID of the category
|
||||
id : CategoryId
|
||||
|
||||
/// The ID of the web log to which the category belongs
|
||||
webLogId : WebLogId
|
||||
|
||||
/// The displayed name
|
||||
name : string
|
||||
|
||||
/// The slug (used in category URLs)
|
||||
slug : string
|
||||
|
||||
/// A longer description of the category
|
||||
description : string option
|
||||
|
||||
/// The parent ID of this category (if a subcategory)
|
||||
parentId : CategoryId option
|
||||
}
|
||||
|
||||
/// Functions to support categories
|
||||
module Category =
|
||||
|
||||
/// An empty category
|
||||
let empty =
|
||||
{ id = CategoryId.empty
|
||||
webLogId = WebLogId.empty
|
||||
name = ""
|
||||
slug = ""
|
||||
description = None
|
||||
parentId = None
|
||||
}
|
||||
|
||||
|
||||
/// A comment on a post
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Comment =
|
||||
{ /// The ID of the comment
|
||||
id : CommentId
|
||||
|
||||
/// The ID of the post to which this comment applies
|
||||
postId : PostId
|
||||
|
||||
/// The ID of the comment to which this comment is a reply
|
||||
inReplyToId : CommentId option
|
||||
|
||||
/// The name of the commentor
|
||||
name : string
|
||||
|
||||
/// The e-mail address of the commentor
|
||||
email : string
|
||||
|
||||
/// The URL of the commentor's personal website
|
||||
url : string option
|
||||
|
||||
/// The status of the comment
|
||||
status : CommentStatus
|
||||
|
||||
/// When the comment was posted
|
||||
postedOn : DateTime
|
||||
|
||||
/// The text of the comment
|
||||
text : string
|
||||
}
|
||||
|
||||
/// Functions to support comments
|
||||
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 = ""
|
||||
}
|
||||
|
||||
|
||||
/// A page (text not associated with a date/time)
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Page =
|
||||
{ /// The ID of this page
|
||||
id : PageId
|
||||
|
||||
/// The ID of the web log to which this page belongs
|
||||
webLogId : WebLogId
|
||||
|
||||
/// The ID of the author of this page
|
||||
authorId : WebLogUserId
|
||||
|
||||
/// The title of the page
|
||||
title : string
|
||||
|
||||
/// The link at which this page is displayed
|
||||
permalink : Permalink
|
||||
|
||||
/// When this page was published
|
||||
publishedOn : DateTime
|
||||
|
||||
/// When this page was last updated
|
||||
updatedOn : DateTime
|
||||
|
||||
/// Whether this page shows as part of the web log's navigation
|
||||
showInPageList : bool
|
||||
|
||||
/// The template to use when rendering this page
|
||||
template : string option
|
||||
|
||||
/// The current text of the page
|
||||
text : string
|
||||
|
||||
/// Metadata for this page
|
||||
metadata : MetaItem list
|
||||
|
||||
/// Permalinks at which this page may have been previously served (useful for migrated content)
|
||||
priorPermalinks : Permalink list
|
||||
|
||||
/// Revisions of this page
|
||||
revisions : Revision list
|
||||
}
|
||||
|
||||
/// Functions to support pages
|
||||
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 = []
|
||||
}
|
||||
|
||||
|
||||
/// A web log post
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Post =
|
||||
{ /// The ID of this post
|
||||
id : PostId
|
||||
|
||||
/// The ID of the web log to which this post belongs
|
||||
webLogId : WebLogId
|
||||
|
||||
/// The ID of the author of this post
|
||||
authorId : WebLogUserId
|
||||
|
||||
/// The status
|
||||
status : PostStatus
|
||||
|
||||
/// The title
|
||||
title : string
|
||||
|
||||
/// The link at which the post resides
|
||||
permalink : Permalink
|
||||
|
||||
/// The instant on which the post was originally published
|
||||
publishedOn : DateTime option
|
||||
|
||||
/// The instant on which the post was last updated
|
||||
updatedOn : DateTime
|
||||
|
||||
/// The template to use in displaying the post
|
||||
template : string option
|
||||
|
||||
/// The text of the post in HTML (ready to display) format
|
||||
text : string
|
||||
|
||||
/// The Ids of the categories to which this is assigned
|
||||
categoryIds : CategoryId list
|
||||
|
||||
/// The tags for the post
|
||||
tags : string list
|
||||
|
||||
/// Metadata for the post
|
||||
metadata : MetaItem list
|
||||
|
||||
/// Permalinks at which this post may have been previously served (useful for migrated content)
|
||||
priorPermalinks : Permalink list
|
||||
|
||||
/// The revisions for this post
|
||||
revisions : Revision list
|
||||
}
|
||||
|
||||
/// Functions to support posts
|
||||
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 = []
|
||||
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
|
||||
|
||||
/// The ID of the web log to which this tag mapping belongs
|
||||
webLogId : WebLogId
|
||||
|
||||
/// The tag which should be mapped to a different value in links
|
||||
tag : string
|
||||
|
||||
/// The value by which the tag should be linked
|
||||
urlValue : string
|
||||
}
|
||||
|
||||
/// Functions to support tag mappings
|
||||
module TagMap =
|
||||
|
||||
/// An empty tag mapping
|
||||
let empty =
|
||||
{ id = TagMapId.empty
|
||||
webLogId = WebLogId.empty
|
||||
tag = ""
|
||||
urlValue = ""
|
||||
}
|
||||
|
||||
|
||||
/// A theme
|
||||
type Theme =
|
||||
{ /// The ID / path of the theme
|
||||
id : ThemeId
|
||||
|
||||
/// A long name of the theme
|
||||
name : string
|
||||
|
||||
/// The version of the theme
|
||||
version : string
|
||||
|
||||
/// The templates for this theme
|
||||
templates: ThemeTemplate list
|
||||
}
|
||||
|
||||
/// Functions to support themes
|
||||
module Theme =
|
||||
|
||||
/// An empty theme
|
||||
let empty =
|
||||
{ id = ThemeId ""
|
||||
name = ""
|
||||
version = ""
|
||||
templates = []
|
||||
}
|
||||
|
||||
|
||||
/// A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path])
|
||||
type ThemeAsset =
|
||||
{
|
||||
/// The ID of the asset (consists of theme and path)
|
||||
id : ThemeAssetId
|
||||
|
||||
/// The updated date (set from the file date from the ZIP archive)
|
||||
updatedOn : DateTime
|
||||
|
||||
/// The data for the asset
|
||||
data : byte[]
|
||||
}
|
||||
|
||||
|
||||
/// A web log
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type WebLog =
|
||||
{ /// The ID of the web log
|
||||
id : WebLogId
|
||||
|
||||
/// The name of the web log
|
||||
name : string
|
||||
|
||||
/// A subtitle for the web log
|
||||
subtitle : string option
|
||||
|
||||
/// The default page ("posts" or a page Id)
|
||||
defaultPage : string
|
||||
|
||||
/// The number of posts to display on pages of posts
|
||||
postsPerPage : int
|
||||
|
||||
/// The path of the theme (within /themes)
|
||||
themePath : string
|
||||
|
||||
/// The URL base
|
||||
urlBase : string
|
||||
|
||||
/// The time zone in which dates/times should be displayed
|
||||
timeZone : string
|
||||
|
||||
/// The RSS options for this web log
|
||||
rss : RssOptions
|
||||
|
||||
/// Whether to automatically load htmx
|
||||
autoHtmx : bool
|
||||
}
|
||||
|
||||
/// Functions to support web logs
|
||||
module WebLog =
|
||||
|
||||
/// An empty web log
|
||||
let empty =
|
||||
{ id = WebLogId.empty
|
||||
name = ""
|
||||
subtitle = None
|
||||
defaultPage = ""
|
||||
postsPerPage = 10
|
||||
themePath = "default"
|
||||
urlBase = ""
|
||||
timeZone = ""
|
||||
rss = RssOptions.empty
|
||||
autoHtmx = false
|
||||
}
|
||||
|
||||
/// Get the host (including scheme) and extra path from the URL base
|
||||
let hostAndPath webLog =
|
||||
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}"
|
||||
|
||||
/// Generate a relative URL for the given link
|
||||
let relativeUrl webLog permalink =
|
||||
let _, leadPath = hostAndPath webLog
|
||||
$"{leadPath}/{Permalink.toString permalink}"
|
||||
|
||||
/// 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)
|
||||
|
||||
/// Convert a date/time in the web log's local date/time to UTC
|
||||
let utcTime webLog (date : DateTime) =
|
||||
TimeZoneInfo.ConvertTimeToUtc
|
||||
(DateTime (date.Ticks, DateTimeKind.Unspecified), TimeZoneInfo.FindSystemTimeZoneById webLog.timeZone)
|
||||
|
||||
|
||||
/// A user of the web log
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type WebLogUser =
|
||||
{ /// The ID of the user
|
||||
id : WebLogUserId
|
||||
|
||||
/// The ID of the web log to which this user belongs
|
||||
webLogId : WebLogId
|
||||
|
||||
/// The user name (e-mail address)
|
||||
userName : string
|
||||
|
||||
/// The user's first name
|
||||
firstName : string
|
||||
|
||||
/// The user's last name
|
||||
lastName : string
|
||||
|
||||
/// The user's preferred name
|
||||
preferredName : string
|
||||
|
||||
/// The hash of the user's password
|
||||
passwordHash : string
|
||||
|
||||
/// Salt used to calculate the user's password hash
|
||||
salt : Guid
|
||||
|
||||
/// The URL of the user's personal site
|
||||
url : string option
|
||||
|
||||
/// The user's authorization level
|
||||
authorizationLevel : AuthorizationLevel
|
||||
}
|
||||
|
||||
/// Functions to support web log users
|
||||
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
|
||||
authorizationLevel = User
|
||||
}
|
||||
|
||||
/// Get the user's displayed name
|
||||
let displayName user =
|
||||
let name =
|
||||
seq { match user.preferredName with "" -> user.firstName | n -> n; " "; user.lastName }
|
||||
|> Seq.reduce (+)
|
||||
name.Trim ()
|
||||
20
src/MyWebLog.Domain/MyWebLog.Domain.fsproj
Normal file
20
src/MyWebLog.Domain/MyWebLog.Domain.fsproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="SupportTypes.fs" />
|
||||
<Compile Include="DataTypes.fs" />
|
||||
<Compile Include="ViewModels.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Markdig" Version="0.30.2" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
<PackageReference Include="Markdown.ColorCode" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
483
src/MyWebLog.Domain/SupportTypes.fs
Normal file
483
src/MyWebLog.Domain/SupportTypes.fs
Normal file
@@ -0,0 +1,483 @@
|
||||
namespace MyWebLog
|
||||
|
||||
open System
|
||||
|
||||
/// Support functions for domain definition
|
||||
[<AutoOpen>]
|
||||
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)
|
||||
|
||||
|
||||
/// An identifier for a category
|
||||
type CategoryId = CategoryId of string
|
||||
|
||||
/// Functions to support category IDs
|
||||
module CategoryId =
|
||||
|
||||
/// An empty category ID
|
||||
let empty = CategoryId ""
|
||||
|
||||
/// Convert a category ID to a string
|
||||
let toString = function CategoryId ci -> ci
|
||||
|
||||
/// Create a new category ID
|
||||
let create () = CategoryId (newId ())
|
||||
|
||||
|
||||
/// An identifier for a comment
|
||||
type CommentId = CommentId of string
|
||||
|
||||
/// Functions to support comment IDs
|
||||
module CommentId =
|
||||
|
||||
/// An empty comment ID
|
||||
let empty = CommentId ""
|
||||
|
||||
/// Convert a comment ID to a string
|
||||
let toString = function CommentId ci -> ci
|
||||
|
||||
/// Create a new comment ID
|
||||
let create () = CommentId (newId ())
|
||||
|
||||
|
||||
/// Statuses for post comments
|
||||
type CommentStatus =
|
||||
/// The comment is approved
|
||||
| Approved
|
||||
/// The comment has yet to be approved
|
||||
| Pending
|
||||
/// The comment was unsolicited and unwelcome
|
||||
| Spam
|
||||
|
||||
/// Functions to support post comment statuses
|
||||
module CommentStatus =
|
||||
|
||||
/// Convert a comment status to a string
|
||||
let toString = function Approved -> "Approved" | Pending -> "Pending" | Spam -> "Spam"
|
||||
|
||||
/// Parse a string into a comment status
|
||||
let parse value =
|
||||
match value with
|
||||
| "Approved" -> Approved
|
||||
| "Pending" -> Pending
|
||||
| "Spam" -> Spam
|
||||
| it -> invalidOp $"{it} is not a valid post status"
|
||||
|
||||
|
||||
open Markdig
|
||||
open Markdown.ColorCode
|
||||
|
||||
/// Types of markup text
|
||||
type MarkupText =
|
||||
/// Markdown text
|
||||
| Markdown of string
|
||||
/// HTML text
|
||||
| Html of string
|
||||
|
||||
/// Functions to support markup text
|
||||
module MarkupText =
|
||||
|
||||
/// Pipeline with most extensions enabled
|
||||
let private _pipeline = MarkdownPipelineBuilder().UseSmartyPants().UseAdvancedExtensions().UseColorCode().Build ()
|
||||
|
||||
/// Get the source type for the markup text
|
||||
let sourceType = function Markdown _ -> "Markdown" | Html _ -> "HTML"
|
||||
|
||||
/// Get the raw text, regardless of type
|
||||
let text = function Markdown text -> text | Html text -> text
|
||||
|
||||
/// Get the string representation of the markup text
|
||||
let toString it = $"{sourceType it}: {text it}"
|
||||
|
||||
/// Get the HTML representation of the markup text
|
||||
let toHtml = function Markdown text -> Markdown.ToHtml (text, _pipeline) | Html text -> text
|
||||
|
||||
/// Parse a string into a MarkupText instance
|
||||
let parse (it : string) =
|
||||
match it with
|
||||
| text when text.StartsWith "Markdown: " -> Markdown (text.Substring 10)
|
||||
| text when text.StartsWith "HTML: " -> Html (text.Substring 6)
|
||||
| text -> invalidOp $"Cannot derive type of text ({text})"
|
||||
|
||||
|
||||
/// An item of metadata
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type MetaItem =
|
||||
{ /// The name of the metadata value
|
||||
name : string
|
||||
|
||||
/// The metadata value
|
||||
value : string
|
||||
}
|
||||
|
||||
/// Functions to support metadata items
|
||||
module MetaItem =
|
||||
|
||||
/// An empty metadata item
|
||||
let empty =
|
||||
{ name = ""; value = "" }
|
||||
|
||||
|
||||
/// A revision of a page or post
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Revision =
|
||||
{ /// When this revision was saved
|
||||
asOf : DateTime
|
||||
|
||||
/// The text of the revision
|
||||
text : MarkupText
|
||||
}
|
||||
|
||||
/// Functions to support revisions
|
||||
module Revision =
|
||||
|
||||
/// An empty revision
|
||||
let empty =
|
||||
{ asOf = DateTime.UtcNow
|
||||
text = Html ""
|
||||
}
|
||||
|
||||
|
||||
/// A permanent link
|
||||
type Permalink = Permalink of string
|
||||
|
||||
/// Functions to support permalinks
|
||||
module Permalink =
|
||||
|
||||
/// An empty permalink
|
||||
let empty = Permalink ""
|
||||
|
||||
/// Convert a permalink to a string
|
||||
let toString = function Permalink p -> p
|
||||
|
||||
|
||||
/// An identifier for a page
|
||||
type PageId = PageId of string
|
||||
|
||||
/// Functions to support page IDs
|
||||
module PageId =
|
||||
|
||||
/// An empty page ID
|
||||
let empty = PageId ""
|
||||
|
||||
/// Convert a page ID to a string
|
||||
let toString = function PageId pi -> pi
|
||||
|
||||
/// Create a new page ID
|
||||
let create () = PageId (newId ())
|
||||
|
||||
|
||||
/// Statuses for posts
|
||||
type PostStatus =
|
||||
/// The post should not be publicly available
|
||||
| Draft
|
||||
/// The post is publicly viewable
|
||||
| Published
|
||||
|
||||
/// Functions to support post statuses
|
||||
module PostStatus =
|
||||
|
||||
/// Convert a post status to a string
|
||||
let toString = function Draft -> "Draft" | Published -> "Published"
|
||||
|
||||
/// Parse a string into a post status
|
||||
let parse value =
|
||||
match value with
|
||||
| "Draft" -> Draft
|
||||
| "Published" -> Published
|
||||
| it -> invalidOp $"{it} is not a valid post status"
|
||||
|
||||
|
||||
/// An identifier for a post
|
||||
type PostId = PostId of string
|
||||
|
||||
/// Functions to support post IDs
|
||||
module PostId =
|
||||
|
||||
/// An empty post ID
|
||||
let empty = PostId ""
|
||||
|
||||
/// Convert a post ID to a string
|
||||
let toString = function PostId pi -> pi
|
||||
|
||||
/// Create a new post ID
|
||||
let create () = PostId (newId ())
|
||||
|
||||
|
||||
/// An identifier for a custom feed
|
||||
type CustomFeedId = CustomFeedId of string
|
||||
|
||||
/// Functions to support custom feed IDs
|
||||
module CustomFeedId =
|
||||
|
||||
/// An empty custom feed ID
|
||||
let empty = CustomFeedId ""
|
||||
|
||||
/// Convert a custom feed ID to a string
|
||||
let toString = function CustomFeedId pi -> pi
|
||||
|
||||
/// Create a new custom feed ID
|
||||
let create () = CustomFeedId (newId ())
|
||||
|
||||
|
||||
/// The source for a custom feed
|
||||
type CustomFeedSource =
|
||||
/// A feed based on a particular category
|
||||
| Category of CategoryId
|
||||
/// A feed based on a particular tag
|
||||
| Tag of string
|
||||
|
||||
/// Functions to support feed sources
|
||||
module CustomFeedSource =
|
||||
/// Create a string version of a feed source
|
||||
let toString : CustomFeedSource -> string =
|
||||
function
|
||||
| Category (CategoryId catId) -> $"category:{catId}"
|
||||
| Tag tag -> $"tag:{tag}"
|
||||
|
||||
/// Parse a feed source from its string version
|
||||
let parse : string -> CustomFeedSource =
|
||||
let value (it : string) = it.Split(":").[1]
|
||||
function
|
||||
| source when source.StartsWith "category:" -> (value >> CategoryId >> Category) source
|
||||
| source when source.StartsWith "tag:" -> (value >> Tag) source
|
||||
| source -> invalidArg "feedSource" $"{source} is not a valid feed source"
|
||||
|
||||
|
||||
/// Valid values for the iTunes explicit rating
|
||||
type ExplicitRating =
|
||||
| Yes
|
||||
| No
|
||||
| Clean
|
||||
|
||||
/// Functions to support iTunes explicit ratings
|
||||
module ExplicitRating =
|
||||
/// Convert an explicit rating to a string
|
||||
let toString : ExplicitRating -> string =
|
||||
function
|
||||
| Yes -> "yes"
|
||||
| No -> "no"
|
||||
| Clean -> "clean"
|
||||
|
||||
/// Parse a string into an explicit rating
|
||||
let parse : string -> ExplicitRating =
|
||||
function
|
||||
| "yes" -> Yes
|
||||
| "no" -> No
|
||||
| "clean" -> Clean
|
||||
| x -> raise (invalidArg "rating" $"{x} is not a valid explicit rating")
|
||||
|
||||
|
||||
/// Options for a feed that describes a podcast
|
||||
type PodcastOptions =
|
||||
{ /// The title of the podcast
|
||||
title : string
|
||||
|
||||
/// A subtitle for the podcast
|
||||
subtitle : string option
|
||||
|
||||
/// The number of items in the podcast feed
|
||||
itemsInFeed : int
|
||||
|
||||
/// A summary of the podcast (iTunes field)
|
||||
summary : string
|
||||
|
||||
/// The display name of the podcast author (iTunes field)
|
||||
displayedAuthor : string
|
||||
|
||||
/// The e-mail address of the user who registered the podcast at iTunes
|
||||
email : string
|
||||
|
||||
/// The link to the image for the podcast
|
||||
imageUrl : Permalink
|
||||
|
||||
/// The category from iTunes under which this podcast is categorized
|
||||
iTunesCategory : string
|
||||
|
||||
/// A further refinement of the categorization of this podcast (iTunes field / values)
|
||||
iTunesSubcategory : string option
|
||||
|
||||
/// The explictness rating (iTunes field)
|
||||
explicit : ExplicitRating
|
||||
|
||||
/// The default media type for files in this podcast
|
||||
defaultMediaType : string option
|
||||
|
||||
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
|
||||
mediaBaseUrl : string option
|
||||
}
|
||||
|
||||
|
||||
/// A custom feed
|
||||
type CustomFeed =
|
||||
{ /// The ID of the custom feed
|
||||
id : CustomFeedId
|
||||
|
||||
/// The source for the custom feed
|
||||
source : CustomFeedSource
|
||||
|
||||
/// The path for the custom feed
|
||||
path : Permalink
|
||||
|
||||
/// Podcast options, if the feed defines a podcast
|
||||
podcast : PodcastOptions option
|
||||
}
|
||||
|
||||
/// Functions to support custom feeds
|
||||
module CustomFeed =
|
||||
|
||||
/// An empty custom feed
|
||||
let empty =
|
||||
{ id = CustomFeedId ""
|
||||
source = Category (CategoryId "")
|
||||
path = Permalink ""
|
||||
podcast = None
|
||||
}
|
||||
|
||||
|
||||
/// Really Simple Syndication (RSS) options for this web log
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type RssOptions =
|
||||
{ /// Whether the site feed of posts is enabled
|
||||
feedEnabled : bool
|
||||
|
||||
/// The name of the file generated for the site feed
|
||||
feedName : string
|
||||
|
||||
/// Override the "posts per page" setting for the site feed
|
||||
itemsInFeed : int option
|
||||
|
||||
/// Whether feeds are enabled for all categories
|
||||
categoryEnabled : bool
|
||||
|
||||
/// Whether feeds are enabled for all tags
|
||||
tagEnabled : bool
|
||||
|
||||
/// A copyright string to be placed in all feeds
|
||||
copyright : string option
|
||||
|
||||
/// Custom feeds for this web log
|
||||
customFeeds: CustomFeed list
|
||||
}
|
||||
|
||||
/// Functions to support RSS options
|
||||
module RssOptions =
|
||||
|
||||
/// An empty set of RSS options
|
||||
let empty =
|
||||
{ feedEnabled = true
|
||||
feedName = "feed.xml"
|
||||
itemsInFeed = None
|
||||
categoryEnabled = true
|
||||
tagEnabled = true
|
||||
copyright = None
|
||||
customFeeds = []
|
||||
}
|
||||
|
||||
|
||||
/// An identifier for a tag mapping
|
||||
type TagMapId = TagMapId of string
|
||||
|
||||
/// Functions to support tag mapping IDs
|
||||
module TagMapId =
|
||||
|
||||
/// An empty tag mapping ID
|
||||
let empty = TagMapId ""
|
||||
|
||||
/// Convert a tag mapping ID to a string
|
||||
let toString = function TagMapId tmi -> tmi
|
||||
|
||||
/// Create a new tag mapping ID
|
||||
let create () = TagMapId (newId ())
|
||||
|
||||
|
||||
/// An identifier for a theme (represents its path)
|
||||
type ThemeId = ThemeId of string
|
||||
|
||||
/// Functions to support theme IDs
|
||||
module ThemeId =
|
||||
let toString = function ThemeId ti -> ti
|
||||
|
||||
|
||||
/// An identifier for a theme asset
|
||||
type ThemeAssetId = ThemeAssetId of ThemeId * string
|
||||
|
||||
/// Functions to support theme asset IDs
|
||||
module ThemeAssetId =
|
||||
|
||||
/// Convert a theme asset ID into a path string
|
||||
let toString = function ThemeAssetId (ThemeId theme, asset) -> $"{theme}/{asset}"
|
||||
|
||||
/// Convert a string into a theme asset ID
|
||||
let ofString (it : string) =
|
||||
let themeIdx = it.IndexOf "/"
|
||||
ThemeAssetId (ThemeId it[..(themeIdx - 1)], it[(themeIdx + 1)..])
|
||||
|
||||
|
||||
/// A template for a theme
|
||||
type ThemeTemplate =
|
||||
{ /// The name of the template
|
||||
name : string
|
||||
|
||||
/// The text of the template
|
||||
text : string
|
||||
}
|
||||
|
||||
|
||||
/// An identifier for a web log
|
||||
type WebLogId = WebLogId of string
|
||||
|
||||
/// Functions to support web log IDs
|
||||
module WebLogId =
|
||||
|
||||
/// An empty web log ID
|
||||
let empty = WebLogId ""
|
||||
|
||||
/// Convert a web log ID to a string
|
||||
let toString = function WebLogId wli -> wli
|
||||
|
||||
/// Create a new web log ID
|
||||
let create () = WebLogId (newId ())
|
||||
|
||||
|
||||
/// A level of authorization for a given web log
|
||||
type AuthorizationLevel =
|
||||
/// <summary>The user may administer all aspects of a web log</summary>
|
||||
| Administrator
|
||||
/// <summary>The user is a known user of a web log</summary>
|
||||
| User
|
||||
|
||||
/// Functions to support authorization levels
|
||||
module AuthorizationLevel =
|
||||
|
||||
/// Convert an authorization level to a string
|
||||
let toString = function Administrator -> "Administrator" | User -> "User"
|
||||
|
||||
/// Parse a string into an authorization level
|
||||
let parse value =
|
||||
match value with
|
||||
| "Administrator" -> Administrator
|
||||
| "User" -> User
|
||||
| it -> invalidOp $"{it} is not a valid authorization level"
|
||||
|
||||
|
||||
/// An identifier for a web log user
|
||||
type WebLogUserId = WebLogUserId of string
|
||||
|
||||
/// Functions to support web log user IDs
|
||||
module WebLogUserId =
|
||||
|
||||
/// An empty web log user ID
|
||||
let empty = WebLogUserId ""
|
||||
|
||||
/// Convert a web log user ID to a string
|
||||
let toString = function WebLogUserId wli -> wli
|
||||
|
||||
/// Create a new web log user ID
|
||||
let create () = WebLogUserId (newId ())
|
||||
|
||||
|
||||
740
src/MyWebLog.Domain/ViewModels.fs
Normal file
740
src/MyWebLog.Domain/ViewModels.fs
Normal file
@@ -0,0 +1,740 @@
|
||||
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) =
|
||||
match it.Trim () with "" -> None | trimmed -> Some trimmed
|
||||
|
||||
|
||||
/// Details about a category, used to display category lists
|
||||
[<NoComparison; NoEquality>]
|
||||
type DisplayCategory =
|
||||
{ /// The ID of the category
|
||||
id : string
|
||||
|
||||
/// The slug for the category
|
||||
slug : string
|
||||
|
||||
/// The name of the category
|
||||
name : string
|
||||
|
||||
/// A description of the category
|
||||
description : string option
|
||||
|
||||
/// The parent category names for this (sub)category
|
||||
parentNames : string[]
|
||||
|
||||
/// The number of posts in this category
|
||||
postCount : int
|
||||
}
|
||||
|
||||
|
||||
/// A display version of a custom feed definition
|
||||
type DisplayCustomFeed =
|
||||
{ /// The ID of the custom feed
|
||||
id : string
|
||||
|
||||
/// The source of the custom feed
|
||||
source : string
|
||||
|
||||
/// The relative path at which the custom feed is served
|
||||
path : string
|
||||
|
||||
/// Whether this custom feed is for a podcast
|
||||
isPodcast : bool
|
||||
}
|
||||
|
||||
/// Create a display version from a custom feed
|
||||
static member fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed =
|
||||
let source =
|
||||
match feed.source with
|
||||
| Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.id = catId)).name}"
|
||||
| Tag tag -> $"Tag: {tag}"
|
||||
{ id = CustomFeedId.toString feed.id
|
||||
source = source
|
||||
path = Permalink.toString feed.path
|
||||
isPodcast = Option.isSome feed.podcast
|
||||
}
|
||||
|
||||
|
||||
/// Details about a page used to display page lists
|
||||
[<NoComparison; NoEquality>]
|
||||
type DisplayPage =
|
||||
{ /// The ID of this page
|
||||
id : string
|
||||
|
||||
/// The title of the page
|
||||
title : string
|
||||
|
||||
/// The link at which this page is displayed
|
||||
permalink : string
|
||||
|
||||
/// When this page was published
|
||||
publishedOn : DateTime
|
||||
|
||||
/// When this page was last updated
|
||||
updatedOn : DateTime
|
||||
|
||||
/// Whether this page shows as part of the web log's navigation
|
||||
showInPageList : bool
|
||||
|
||||
/// Is this the default page?
|
||||
isDefault : bool
|
||||
|
||||
/// The text of the page
|
||||
text : string
|
||||
|
||||
/// The metadata for the page
|
||||
metadata : MetaItem list
|
||||
}
|
||||
|
||||
/// 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
|
||||
title = page.title
|
||||
permalink = Permalink.toString page.permalink
|
||||
publishedOn = page.publishedOn
|
||||
updatedOn = page.updatedOn
|
||||
showInPageList = page.showInPageList
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
/// The model used to display the admin dashboard
|
||||
[<NoComparison; NoEquality>]
|
||||
type DashboardModel =
|
||||
{ /// The number of published posts
|
||||
posts : int
|
||||
|
||||
/// The number of post drafts
|
||||
drafts : int
|
||||
|
||||
/// The number of pages
|
||||
pages : int
|
||||
|
||||
/// The number of pages in the page list
|
||||
listedPages : int
|
||||
|
||||
/// The number of categories
|
||||
categories : int
|
||||
|
||||
/// The top-level categories
|
||||
topLevelCategories : int
|
||||
}
|
||||
|
||||
|
||||
/// View model for editing categories
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditCategoryModel =
|
||||
{ /// The ID of the category being edited
|
||||
categoryId : string
|
||||
|
||||
/// The name of the category
|
||||
name : string
|
||||
|
||||
/// The category's URL slug
|
||||
slug : string
|
||||
|
||||
/// A description of the category (optional)
|
||||
description : string
|
||||
|
||||
/// The ID of the category for which this is a subcategory (optional)
|
||||
parentId : string
|
||||
}
|
||||
|
||||
/// 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 ""
|
||||
}
|
||||
|
||||
|
||||
/// View model to edit a custom RSS feed
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditCustomFeedModel =
|
||||
{ /// The ID of the feed being editing
|
||||
id : string
|
||||
|
||||
/// The type of source for this feed ("category" or "tag")
|
||||
sourceType : string
|
||||
|
||||
/// The category ID or tag on which this feed is based
|
||||
sourceValue : string
|
||||
|
||||
/// The relative path at which this feed is served
|
||||
path : string
|
||||
|
||||
/// Whether this feed defines a podcast
|
||||
isPodcast : bool
|
||||
|
||||
/// The title of the podcast
|
||||
title : string
|
||||
|
||||
/// A subtitle for the podcast
|
||||
subtitle : string
|
||||
|
||||
/// The number of items in the podcast feed
|
||||
itemsInFeed : int
|
||||
|
||||
/// A summary of the podcast (iTunes field)
|
||||
summary : string
|
||||
|
||||
/// The display name of the podcast author (iTunes field)
|
||||
displayedAuthor : string
|
||||
|
||||
/// The e-mail address of the user who registered the podcast at iTunes
|
||||
email : string
|
||||
|
||||
/// The link to the image for the podcast
|
||||
imageUrl : string
|
||||
|
||||
/// The category from iTunes under which this podcast is categorized
|
||||
itunesCategory : string
|
||||
|
||||
/// A further refinement of the categorization of this podcast (iTunes field / values)
|
||||
itunesSubcategory : string
|
||||
|
||||
/// The explictness rating (iTunes field)
|
||||
explicit : string
|
||||
|
||||
/// The default media type for files in this podcast
|
||||
defaultMediaType : string
|
||||
|
||||
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
|
||||
mediaBaseUrl : string
|
||||
}
|
||||
|
||||
/// 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 = ""
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
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 ""
|
||||
}
|
||||
| None -> rss
|
||||
|
||||
/// Update a feed with values from this model
|
||||
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 =
|
||||
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
|
||||
}
|
||||
else
|
||||
None
|
||||
}
|
||||
|
||||
/// View model to edit a page
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditPageModel =
|
||||
{ /// The ID of the page being edited
|
||||
pageId : string
|
||||
|
||||
/// The title of the page
|
||||
title : string
|
||||
|
||||
/// The permalink for the page
|
||||
permalink : string
|
||||
|
||||
/// The template to use to display the page
|
||||
template : string
|
||||
|
||||
/// Whether this page is shown in the page list
|
||||
isShownInPageList : bool
|
||||
|
||||
/// The source format for the text
|
||||
source : string
|
||||
|
||||
/// The text of the page
|
||||
text : string
|
||||
|
||||
/// Names of metadata items
|
||||
metaNames : string[]
|
||||
|
||||
/// Values of metadata items
|
||||
metaValues : string[]
|
||||
}
|
||||
|
||||
/// 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
|
||||
{ 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
|
||||
}
|
||||
|
||||
|
||||
/// View model to edit a post
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditPostModel =
|
||||
{ /// The ID of the post being edited
|
||||
postId : string
|
||||
|
||||
/// The title of the post
|
||||
title : string
|
||||
|
||||
/// The permalink for the post
|
||||
permalink : string
|
||||
|
||||
/// The source format for the text
|
||||
source : string
|
||||
|
||||
/// The text of the post
|
||||
text : string
|
||||
|
||||
/// The tags for the post
|
||||
tags : string
|
||||
|
||||
/// The template used to display the post
|
||||
template : string
|
||||
|
||||
/// The category IDs for the post
|
||||
categoryIds : string[]
|
||||
|
||||
/// The post status
|
||||
status : string
|
||||
|
||||
/// Whether this post should be published
|
||||
doPublish : bool
|
||||
|
||||
/// Names of metadata items
|
||||
metaNames : string[]
|
||||
|
||||
/// Values of metadata items
|
||||
metaValues : string[]
|
||||
|
||||
/// Whether to override the published date/time
|
||||
setPublished : bool
|
||||
|
||||
/// The published date/time to override
|
||||
pubOverride : Nullable<DateTime>
|
||||
|
||||
/// Whether all revisions should be purged and the override date set as the updated date as well
|
||||
setUpdated : bool
|
||||
}
|
||||
/// 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
|
||||
{ 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
|
||||
}
|
||||
|
||||
|
||||
/// View model to edit RSS settings
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditRssModel =
|
||||
{ /// Whether the site feed of posts is enabled
|
||||
feedEnabled : bool
|
||||
|
||||
/// The name of the file generated for the site feed
|
||||
feedName : string
|
||||
|
||||
/// Override the "posts per page" setting for the site feed
|
||||
itemsInFeed : int
|
||||
|
||||
/// Whether feeds are enabled for all categories
|
||||
categoryEnabled : bool
|
||||
|
||||
/// Whether feeds are enabled for all tags
|
||||
tagEnabled : bool
|
||||
|
||||
/// A copyright string to be placed in all feeds
|
||||
copyright : string
|
||||
}
|
||||
|
||||
/// Create an edit model from a set of RSS options
|
||||
static member fromRssOptions (rss : RssOptions) =
|
||||
{ feedEnabled = rss.feedEnabled
|
||||
feedName = rss.feedName
|
||||
itemsInFeed = defaultArg rss.itemsInFeed 0
|
||||
categoryEnabled = rss.categoryEnabled
|
||||
tagEnabled = rss.tagEnabled
|
||||
copyright = defaultArg rss.copyright ""
|
||||
}
|
||||
|
||||
/// Update RSS options from values in this mode
|
||||
member this.updateOptions (rss : RssOptions) =
|
||||
{ rss with
|
||||
feedEnabled = this.feedEnabled
|
||||
feedName = this.feedName
|
||||
itemsInFeed = if this.itemsInFeed = 0 then None else Some this.itemsInFeed
|
||||
categoryEnabled = this.categoryEnabled
|
||||
tagEnabled = this.tagEnabled
|
||||
copyright = noneIfBlank this.copyright
|
||||
}
|
||||
|
||||
|
||||
/// View model to edit a tag mapping
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditTagMapModel =
|
||||
{ /// The ID of the tag mapping being edited
|
||||
id : string
|
||||
|
||||
/// The tag being mapped to a different link value
|
||||
tag : string
|
||||
|
||||
/// The link value for the tag
|
||||
urlValue : string
|
||||
}
|
||||
|
||||
/// Whether this is a new tag mapping
|
||||
member this.isNew = this.id = "new"
|
||||
|
||||
/// 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 = ""
|
||||
}
|
||||
|
||||
|
||||
/// The model to use to allow a user to log on
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type LogOnModel =
|
||||
{ /// The user's e-mail address
|
||||
emailAddress : string
|
||||
|
||||
/// The user's password
|
||||
password : string
|
||||
|
||||
/// Where the user should be redirected once they have logged on
|
||||
returnTo : string option
|
||||
}
|
||||
|
||||
/// An empty log on model
|
||||
static member empty =
|
||||
{ emailAddress = ""; password = ""; returnTo = None }
|
||||
|
||||
|
||||
/// View model to manage permalinks
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type ManagePermalinksModel =
|
||||
{ /// The ID for the entity being edited
|
||||
id : string
|
||||
|
||||
/// The type of entity being edited ("page" or "post")
|
||||
entity : string
|
||||
|
||||
/// The current title of the page or post
|
||||
currentTitle : string
|
||||
|
||||
/// The current permalink of the page or post
|
||||
currentPermalink : string
|
||||
|
||||
/// The prior permalinks for the page or post
|
||||
prior : string[]
|
||||
}
|
||||
|
||||
/// Create a permalink model from a page
|
||||
static member fromPage (pg : Page) =
|
||||
{ id = PageId.toString pg.id
|
||||
entity = "page"
|
||||
currentTitle = pg.title
|
||||
currentPermalink = Permalink.toString pg.permalink
|
||||
prior = pg.priorPermalinks |> List.map Permalink.toString |> Array.ofList
|
||||
}
|
||||
|
||||
/// Create a permalink model from a post
|
||||
static member fromPost (post : Post) =
|
||||
{ id = PostId.toString post.id
|
||||
entity = "post"
|
||||
currentTitle = post.title
|
||||
currentPermalink = Permalink.toString post.permalink
|
||||
prior = post.priorPermalinks |> List.map Permalink.toString |> Array.ofList
|
||||
}
|
||||
|
||||
|
||||
/// View model for posts in a list
|
||||
[<NoComparison; NoEquality>]
|
||||
type PostListItem =
|
||||
{ /// The ID of the post
|
||||
id : string
|
||||
|
||||
/// The ID of the user who authored the post
|
||||
authorId : string
|
||||
|
||||
/// The status of the post
|
||||
status : string
|
||||
|
||||
/// The title of the post
|
||||
title : string
|
||||
|
||||
/// The permalink for the post
|
||||
permalink : string
|
||||
|
||||
/// When this post was published
|
||||
publishedOn : Nullable<DateTime>
|
||||
|
||||
/// When this post was last updated
|
||||
updatedOn : DateTime
|
||||
|
||||
/// The text of the post
|
||||
text : string
|
||||
|
||||
/// The IDs of the categories for this post
|
||||
categoryIds : string list
|
||||
|
||||
/// Tags for the post
|
||||
tags : string list
|
||||
|
||||
/// Metadata for the post
|
||||
metadata : MetaItem list
|
||||
}
|
||||
|
||||
/// 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
|
||||
{ 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
|
||||
metadata = post.metadata
|
||||
}
|
||||
|
||||
|
||||
/// View model for displaying posts
|
||||
type PostDisplay =
|
||||
{ /// The posts to be displayed
|
||||
posts : PostListItem[]
|
||||
|
||||
/// Author ID -> name lookup
|
||||
authors : MetaItem list
|
||||
|
||||
/// A subtitle for the page
|
||||
subtitle : string option
|
||||
|
||||
/// The link to view newer (more recent) posts
|
||||
newerLink : string option
|
||||
|
||||
/// The name of the next newer post (single-post only)
|
||||
newerName : string option
|
||||
|
||||
/// The link to view older (less recent) posts
|
||||
olderLink : string option
|
||||
|
||||
/// The name of the next older post (single-post only)
|
||||
olderName : string option
|
||||
}
|
||||
|
||||
|
||||
/// View model for editing web log settings
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type SettingsModel =
|
||||
{ /// The name of the web log
|
||||
name : string
|
||||
|
||||
/// The subtitle of the web log
|
||||
subtitle : string
|
||||
|
||||
/// The default page
|
||||
defaultPage : string
|
||||
|
||||
/// How many posts should appear on index pages
|
||||
postsPerPage : int
|
||||
|
||||
/// The time zone in which dates/times should be displayed
|
||||
timeZone : string
|
||||
|
||||
/// The theme to use to display the web log
|
||||
themePath : string
|
||||
|
||||
/// Whether to automatically load htmx
|
||||
autoHtmx : bool
|
||||
}
|
||||
|
||||
/// Create a settings model from a web log
|
||||
static member fromWebLog (webLog : WebLog) =
|
||||
{ name = webLog.name
|
||||
subtitle = defaultArg webLog.subtitle ""
|
||||
defaultPage = webLog.defaultPage
|
||||
postsPerPage = webLog.postsPerPage
|
||||
timeZone = webLog.timeZone
|
||||
themePath = webLog.themePath
|
||||
autoHtmx = webLog.autoHtmx
|
||||
}
|
||||
|
||||
/// Update a web log with settings from the form
|
||||
member this.update (webLog : WebLog) =
|
||||
{ webLog with
|
||||
name = this.name
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type UserMessage =
|
||||
{ /// The level of the message
|
||||
level : string
|
||||
|
||||
/// The message
|
||||
message : string
|
||||
|
||||
/// Further details about the message
|
||||
detail : string option
|
||||
}
|
||||
|
||||
/// Functions to support user messages
|
||||
module UserMessage =
|
||||
|
||||
/// An empty user message (use one of the others for pre-filled level)
|
||||
let empty = { level = ""; message = ""; detail = None }
|
||||
|
||||
/// A blank success message
|
||||
let success = { empty with level = "success" }
|
||||
|
||||
/// A blank informational message
|
||||
let info = { empty with level = "primary" }
|
||||
|
||||
/// A blank warning message
|
||||
let warning = { empty with level = "warning" }
|
||||
|
||||
/// A blank error message
|
||||
let error = { empty with level = "danger" }
|
||||
Reference in New Issue
Block a user