Version 2.1 #41
@ -2,6 +2,63 @@
|
|||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module MyWebLog.Data.SQLite.Helpers
|
module MyWebLog.Data.SQLite.Helpers
|
||||||
|
|
||||||
|
/// The table names used in the SQLite implementation
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Table =
|
||||||
|
|
||||||
|
/// Categories
|
||||||
|
[<Literal>]
|
||||||
|
let Category = "category"
|
||||||
|
|
||||||
|
/// Database Version
|
||||||
|
[<Literal>]
|
||||||
|
let DbVersion = "db_version"
|
||||||
|
|
||||||
|
/// Pages
|
||||||
|
[<Literal>]
|
||||||
|
let Page = "page"
|
||||||
|
|
||||||
|
/// Page Revisions
|
||||||
|
[<Literal>]
|
||||||
|
let PageRevision = "page_revision"
|
||||||
|
|
||||||
|
/// Posts
|
||||||
|
[<Literal>]
|
||||||
|
let Post = "post"
|
||||||
|
|
||||||
|
/// Post Comments
|
||||||
|
[<Literal>]
|
||||||
|
let PostComment = "post_comment"
|
||||||
|
|
||||||
|
/// Post Revisions
|
||||||
|
[<Literal>]
|
||||||
|
let PostRevision = "post_revision"
|
||||||
|
|
||||||
|
/// Tag/URL Mappings
|
||||||
|
[<Literal>]
|
||||||
|
let TagMap = "tag_map"
|
||||||
|
|
||||||
|
/// Themes
|
||||||
|
[<Literal>]
|
||||||
|
let Theme = "theme"
|
||||||
|
|
||||||
|
/// Theme Assets
|
||||||
|
[<Literal>]
|
||||||
|
let ThemeAsset = "theme_asset"
|
||||||
|
|
||||||
|
/// Uploads
|
||||||
|
[<Literal>]
|
||||||
|
let Upload = "upload"
|
||||||
|
|
||||||
|
/// Web Logs
|
||||||
|
[<Literal>]
|
||||||
|
let WebLog = "web_log"
|
||||||
|
|
||||||
|
/// Users
|
||||||
|
[<Literal>]
|
||||||
|
let WebLogUser = "web_log_user"
|
||||||
|
|
||||||
|
|
||||||
open System
|
open System
|
||||||
open Microsoft.Data.Sqlite
|
open Microsoft.Data.Sqlite
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
|
@ -27,17 +27,9 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS
|
|||||||
not (List.contains table tables)
|
not (List.contains table tables)
|
||||||
seq {
|
seq {
|
||||||
// Theme tables
|
// Theme tables
|
||||||
if needsTable "theme" then
|
if needsTable Table.Theme then
|
||||||
"CREATE TABLE theme (
|
$"CREATE TABLE {Table.Theme} (data TEXT NOT NULL);
|
||||||
id TEXT PRIMARY KEY,
|
CREATE UNIQUE INDEX idx_{Table.Theme}_key ON {Table.Theme} (data ->> 'Id')";
|
||||||
name TEXT NOT NULL,
|
|
||||||
version TEXT NOT NULL)"
|
|
||||||
if needsTable "theme_template" then
|
|
||||||
"CREATE TABLE theme_template (
|
|
||||||
theme_id TEXT NOT NULL REFERENCES theme (id),
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
template TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (theme_id, name))"
|
|
||||||
if needsTable "theme_asset" then
|
if needsTable "theme_asset" then
|
||||||
"CREATE TABLE theme_asset (
|
"CREATE TABLE theme_asset (
|
||||||
theme_id TEXT NOT NULL REFERENCES theme (id),
|
theme_id TEXT NOT NULL REFERENCES theme (id),
|
||||||
@ -46,139 +38,54 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS
|
|||||||
data BLOB NOT NULL,
|
data BLOB NOT NULL,
|
||||||
PRIMARY KEY (theme_id, path))"
|
PRIMARY KEY (theme_id, path))"
|
||||||
|
|
||||||
// Web log tables
|
// Web log table
|
||||||
if needsTable "web_log" then
|
if needsTable Table.WebLog then
|
||||||
"CREATE TABLE web_log (
|
$"CREATE TABLE {Table.WebLog} (data TEXT NOT NULL);
|
||||||
id TEXT PRIMARY KEY,
|
CREATE UNIQUE INDEX idx_{Table.WebLog}_key ON {Table.WebLog} (data ->> 'Id')"
|
||||||
name TEXT NOT NULL,
|
|
||||||
slug TEXT NOT NULL,
|
|
||||||
subtitle TEXT,
|
|
||||||
default_page TEXT NOT NULL,
|
|
||||||
posts_per_page INTEGER NOT NULL,
|
|
||||||
theme_id TEXT NOT NULL REFERENCES theme (id),
|
|
||||||
url_base TEXT NOT NULL,
|
|
||||||
time_zone TEXT NOT NULL,
|
|
||||||
auto_htmx INTEGER NOT NULL DEFAULT 0,
|
|
||||||
uploads TEXT NOT NULL,
|
|
||||||
is_feed_enabled INTEGER NOT NULL DEFAULT 0,
|
|
||||||
feed_name TEXT NOT NULL,
|
|
||||||
items_in_feed INTEGER,
|
|
||||||
is_category_enabled INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_tag_enabled INTEGER NOT NULL DEFAULT 0,
|
|
||||||
copyright TEXT,
|
|
||||||
redirect_rules TEXT NOT NULL DEFAULT '[]');
|
|
||||||
CREATE INDEX web_log_theme_idx ON web_log (theme_id)"
|
|
||||||
if needsTable "web_log_feed" then
|
|
||||||
"CREATE TABLE web_log_feed (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
web_log_id TEXT NOT NULL REFERENCES web_log (id),
|
|
||||||
source TEXT NOT NULL,
|
|
||||||
path TEXT NOT NULL,
|
|
||||||
podcast TEXT);
|
|
||||||
CREATE INDEX web_log_feed_web_log_idx ON web_log_feed (web_log_id)"
|
|
||||||
|
|
||||||
// Category table
|
// Category table
|
||||||
if needsTable "category" then
|
if needsTable Table.Category then
|
||||||
"CREATE TABLE category (
|
$"CREATE TABLE {Table.Category} (data TEXT NOT NULL);
|
||||||
id TEXT PRIMARY KEY,
|
CREATE UNIQUE INDEX idx_{Table.Category}_key ON {Table.Category} (data -> 'Id');
|
||||||
web_log_id TEXT NOT NULL REFERENCES web_log (id),
|
CREATE INDEX idx_{Table.Category}_web_log ON {Table.Category} (data ->> 'WebLogId')"
|
||||||
name TEXT NOT NULL,
|
|
||||||
slug TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
parent_id TEXT);
|
|
||||||
CREATE INDEX category_web_log_idx ON category (web_log_id)"
|
|
||||||
|
|
||||||
// Web log user table
|
// Web log user table
|
||||||
if needsTable "web_log_user" then
|
if needsTable Table.WebLogUser then
|
||||||
"CREATE TABLE web_log_user (
|
$"CREATE TABLE web_log_user (data TEXT NOT NULL);
|
||||||
id TEXT PRIMARY KEY,
|
CREATE UNIQUE INDEX idx_{Table.WebLogUser}_key ON {Table.WebLogUser} (data ->> 'Id');
|
||||||
web_log_id TEXT NOT NULL REFERENCES web_log (id),
|
CREATE INDEX idx_{Table.WebLogUser}_email ON {Table.WebLogUser} (data ->> 'WebLogId', data ->> 'Email')"
|
||||||
email TEXT NOT NULL,
|
|
||||||
first_name TEXT NOT NULL,
|
|
||||||
last_name TEXT NOT NULL,
|
|
||||||
preferred_name TEXT NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
url TEXT,
|
|
||||||
access_level TEXT NOT NULL,
|
|
||||||
created_on TEXT NOT NULL,
|
|
||||||
last_seen_on TEXT);
|
|
||||||
CREATE INDEX web_log_user_web_log_idx ON web_log_user (web_log_id);
|
|
||||||
CREATE INDEX web_log_user_email_idx ON web_log_user (web_log_id, email)"
|
|
||||||
|
|
||||||
// Page tables
|
// Page tables
|
||||||
if needsTable "page" then
|
if needsTable Table.Page then
|
||||||
"CREATE TABLE page (
|
$"CREATE TABLE {Table.Page} (data TEXT NOT NULL);
|
||||||
id TEXT PRIMARY KEY,
|
CREATE UNIQUE INDEX idx_{Table.Page}_key ON {Table.Page} (data ->> 'Id');
|
||||||
web_log_id TEXT NOT NULL REFERENCES web_log (id),
|
CREATE INDEX idx_{Table.Page}_author ON {Table.Page} (data ->> 'AuthorId');
|
||||||
author_id TEXT NOT NULL REFERENCES web_log_user (id),
|
CREATE INDEX idx_{Table.Page}_permalink ON {Table.Page} (data ->> 'WebLogId', data ->> 'Permalink')"
|
||||||
title TEXT NOT NULL,
|
if needsTable Table.PageRevision then
|
||||||
permalink TEXT NOT NULL,
|
|
||||||
published_on TEXT NOT NULL,
|
|
||||||
updated_on TEXT NOT NULL,
|
|
||||||
is_in_page_list INTEGER NOT NULL DEFAULT 0,
|
|
||||||
template TEXT,
|
|
||||||
page_text TEXT NOT NULL,
|
|
||||||
meta_items TEXT);
|
|
||||||
CREATE INDEX page_web_log_idx ON page (web_log_id);
|
|
||||||
CREATE INDEX page_author_idx ON page (author_id);
|
|
||||||
CREATE INDEX page_permalink_idx ON page (web_log_id, permalink)"
|
|
||||||
if needsTable "page_permalink" then
|
|
||||||
"CREATE TABLE page_permalink (
|
|
||||||
page_id TEXT NOT NULL REFERENCES page (id),
|
|
||||||
permalink TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (page_id, permalink))"
|
|
||||||
if needsTable "page_revision" then
|
|
||||||
"CREATE TABLE page_revision (
|
"CREATE TABLE page_revision (
|
||||||
page_id TEXT NOT NULL REFERENCES page (id),
|
page_id TEXT NOT NULL,
|
||||||
as_of TEXT NOT NULL,
|
as_of TEXT NOT NULL,
|
||||||
revision_text TEXT NOT NULL,
|
revision_text TEXT NOT NULL,
|
||||||
PRIMARY KEY (page_id, as_of))"
|
PRIMARY KEY (page_id, as_of))"
|
||||||
|
|
||||||
// Post tables
|
// Post tables
|
||||||
if needsTable "post" then
|
if needsTable Table.Post then
|
||||||
"CREATE TABLE post (
|
$"CREATE TABLE {Table.Post} (data TEXT NOT NULL);
|
||||||
id TEXT PRIMARY KEY,
|
CREATE UNIQUE INDEX idx_{Table.Post}_key ON {Table.Post} (data ->> 'Id');
|
||||||
web_log_id TEXT NOT NULL REFERENCES web_log (id),
|
CREATE INDEX idx_{Table.Post}_author ON {Table.Post} (data ->> 'AuthorId');
|
||||||
author_id TEXT NOT NULL REFERENCES web_log_user (id),
|
CREATE INDEX idx_{Table.Post}_status ON {Table.Post} (data ->> 'WebLogId', data ->> 'Status', data ->> 'UpdatedOn');
|
||||||
status TEXT NOT NULL,
|
CREATE INDEX idx_{Table.Post}_permalink ON {Table.Post} (data ->> 'WebLogId', data ->> 'Permalink')"
|
||||||
title TEXT NOT NULL,
|
// TODO: index categories by post?
|
||||||
permalink TEXT NOT NULL,
|
if needsTable Table.PostRevision then
|
||||||
published_on TEXT,
|
$"CREATE TABLE {Table.PostRevision} (
|
||||||
updated_on TEXT NOT NULL,
|
post_id TEXT NOT NULL,
|
||||||
template TEXT,
|
|
||||||
post_text TEXT NOT NULL,
|
|
||||||
meta_items TEXT,
|
|
||||||
episode TEXT);
|
|
||||||
CREATE INDEX post_web_log_idx ON post (web_log_id);
|
|
||||||
CREATE INDEX post_author_idx ON post (author_id);
|
|
||||||
CREATE INDEX post_status_idx ON post (web_log_id, status, updated_on);
|
|
||||||
CREATE INDEX post_permalink_idx ON post (web_log_id, permalink)"
|
|
||||||
if needsTable "post_category" then
|
|
||||||
"CREATE TABLE post_category (
|
|
||||||
post_id TEXT NOT NULL REFERENCES post (id),
|
|
||||||
category_id TEXT NOT NULL REFERENCES category (id),
|
|
||||||
PRIMARY KEY (post_id, category_id));
|
|
||||||
CREATE INDEX post_category_category_idx ON post_category (category_id)"
|
|
||||||
if needsTable "post_tag" then
|
|
||||||
"CREATE TABLE post_tag (
|
|
||||||
post_id TEXT NOT NULL REFERENCES post (id),
|
|
||||||
tag TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (post_id, tag))"
|
|
||||||
if needsTable "post_permalink" then
|
|
||||||
"CREATE TABLE post_permalink (
|
|
||||||
post_id TEXT NOT NULL REFERENCES post (id),
|
|
||||||
permalink TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (post_id, permalink))"
|
|
||||||
if needsTable "post_revision" then
|
|
||||||
"CREATE TABLE post_revision (
|
|
||||||
post_id TEXT NOT NULL REFERENCES post (id),
|
|
||||||
as_of TEXT NOT NULL,
|
as_of TEXT NOT NULL,
|
||||||
revision_text TEXT NOT NULL,
|
revision_text TEXT NOT NULL,
|
||||||
PRIMARY KEY (post_id, as_of))"
|
PRIMARY KEY (post_id, as_of))"
|
||||||
if needsTable "post_comment" then
|
if needsTable Table.PostComment then
|
||||||
"CREATE TABLE post_comment (
|
$"CREATE TABLE {Table.PostComment} (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
post_id TEXT NOT NULL REFERENCES post(id),
|
post_id TEXT NOT NULL,
|
||||||
in_reply_to_id TEXT,
|
in_reply_to_id TEXT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
@ -186,32 +93,28 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS
|
|||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
posted_on TEXT NOT NULL,
|
posted_on TEXT NOT NULL,
|
||||||
comment_text TEXT NOT NULL);
|
comment_text TEXT NOT NULL);
|
||||||
CREATE INDEX post_comment_post_idx ON post_comment (post_id)"
|
CREATE INDEX idx_{Table.PostComment}_post ON {Table.PostComment} (post_id)"
|
||||||
|
|
||||||
// Tag map table
|
// Tag map table
|
||||||
if needsTable "tag_map" then
|
if needsTable Table.TagMap then
|
||||||
"CREATE TABLE tag_map (
|
$"CREATE TABLE {Table.TagMap} (data TEXT NOT NULL);
|
||||||
id TEXT PRIMARY KEY,
|
CREATE UNIQUE INDEX idx_{Table.TagMap}_key ON {Table.TagMap} (data ->> 'Id');
|
||||||
web_log_id TEXT NOT NULL REFERENCES web_log (id),
|
CREATE INDEX idx_{Table.TagMap}_tag ON {Table.TagMap} (data ->> 'WebLogId', data ->> 'UrlValue')";
|
||||||
tag TEXT NOT NULL,
|
|
||||||
url_value TEXT NOT NULL);
|
|
||||||
CREATE INDEX tag_map_web_log_idx ON tag_map (web_log_id)"
|
|
||||||
|
|
||||||
// Uploaded file table
|
// Uploaded file table
|
||||||
if needsTable "upload" then
|
if needsTable Table.Upload then
|
||||||
"CREATE TABLE upload (
|
$"CREATE TABLE {Table.Upload} (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
web_log_id TEXT NOT NULL REFERENCES web_log (id),
|
web_log_id TEXT NOT NULL,
|
||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
updated_on TEXT NOT NULL,
|
updated_on TEXT NOT NULL,
|
||||||
data BLOB NOT NULL);
|
data BLOB NOT NULL);
|
||||||
CREATE INDEX upload_web_log_idx ON upload (web_log_id);
|
CREATE INDEX idx_{Table.Upload}_path ON {Table.Upload} (web_log_id, path)"
|
||||||
CREATE INDEX upload_path_idx ON upload (web_log_id, path)"
|
|
||||||
|
|
||||||
// Database version table
|
// Database version table
|
||||||
if needsTable "db_version" then
|
if needsTable Table.DbVersion then
|
||||||
"CREATE TABLE db_version (id TEXT PRIMARY KEY);
|
$"CREATE TABLE {Table.DbVersion} (id TEXT PRIMARY KEY);
|
||||||
INSERT INTO db_version VALUES ('v2')"
|
INSERT INTO {Table.DbVersion} VALUES ('v2.1')"
|
||||||
}
|
}
|
||||||
|> Seq.map (fun sql ->
|
|> Seq.map (fun sql ->
|
||||||
log.LogInformation $"Creating {(sql.Split ' ')[2]} table..."
|
log.LogInformation $"Creating {(sql.Split ' ')[2]} table..."
|
||||||
@ -224,7 +127,7 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS
|
|||||||
/// Set the database version to the specified version
|
/// Set the database version to the specified version
|
||||||
let setDbVersion version = backgroundTask {
|
let setDbVersion version = backgroundTask {
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
cmd.CommandText <- $"DELETE FROM db_version; INSERT INTO db_version VALUES ('%s{version}')"
|
cmd.CommandText <- $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')"
|
||||||
do! write cmd
|
do! write cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -600,7 +503,7 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS
|
|||||||
do! ensureTables ()
|
do! ensureTables ()
|
||||||
|
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
cmd.CommandText <- "SELECT id FROM db_version"
|
cmd.CommandText <- $"SELECT id FROM {Table.DbVersion}"
|
||||||
use! rdr = cmd.ExecuteReaderAsync ()
|
use! rdr = cmd.ExecuteReaderAsync ()
|
||||||
do! migrate (if rdr.Read () then Some (Map.getString "id" rdr) else None)
|
do! migrate (if rdr.Read () then Some (Map.getString "id" rdr) else None)
|
||||||
}
|
}
|
||||||
|
@ -6,405 +6,404 @@ open NodaTime
|
|||||||
|
|
||||||
/// A category under which a post may be identified
|
/// A category under which a post may be identified
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Category =
|
type Category = {
|
||||||
{ /// The ID of the category
|
/// The ID of the category
|
||||||
Id : CategoryId
|
Id : CategoryId
|
||||||
|
|
||||||
/// The ID of the web log to which the category belongs
|
/// The ID of the web log to which the category belongs
|
||||||
WebLogId : WebLogId
|
WebLogId : WebLogId
|
||||||
|
|
||||||
/// The displayed name
|
/// The displayed name
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// The slug (used in category URLs)
|
/// The slug (used in category URLs)
|
||||||
Slug : string
|
Slug : string
|
||||||
|
|
||||||
/// A longer description of the category
|
/// A longer description of the category
|
||||||
Description : string option
|
Description : string option
|
||||||
|
|
||||||
/// The parent ID of this category (if a subcategory)
|
/// The parent ID of this category (if a subcategory)
|
||||||
ParentId : CategoryId option
|
ParentId : CategoryId option
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support categories
|
/// Functions to support categories
|
||||||
module Category =
|
module Category =
|
||||||
|
|
||||||
/// An empty category
|
/// An empty category
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = CategoryId.empty
|
Id = CategoryId.empty
|
||||||
WebLogId = WebLogId.empty
|
WebLogId = WebLogId.empty
|
||||||
Name = ""
|
Name = ""
|
||||||
Slug = ""
|
Slug = ""
|
||||||
Description = None
|
Description = None
|
||||||
ParentId = None
|
ParentId = None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A comment on a post
|
/// A comment on a post
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Comment =
|
type Comment = {
|
||||||
{ /// The ID of the comment
|
/// The ID of the comment
|
||||||
Id : CommentId
|
Id : CommentId
|
||||||
|
|
||||||
/// The ID of the post to which this comment applies
|
/// 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
|
/// The ID of the comment to which this comment is a reply
|
||||||
InReplyToId : CommentId option
|
InReplyToId : CommentId option
|
||||||
|
|
||||||
/// The name of the commentor
|
/// The name of the commentor
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// The e-mail address of the commentor
|
/// The e-mail address of the commentor
|
||||||
Email : string
|
Email : string
|
||||||
|
|
||||||
/// The URL of the commentor's personal website
|
/// The URL of the commentor's personal website
|
||||||
Url : string option
|
Url : string option
|
||||||
|
|
||||||
/// The status of the comment
|
/// The status of the comment
|
||||||
Status : CommentStatus
|
Status : CommentStatus
|
||||||
|
|
||||||
/// When the comment was posted
|
/// When the comment was posted
|
||||||
PostedOn : Instant
|
PostedOn : Instant
|
||||||
|
|
||||||
/// The text of the comment
|
/// The text of the comment
|
||||||
Text : string
|
Text : string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support comments
|
/// Functions to support comments
|
||||||
module Comment =
|
module Comment =
|
||||||
|
|
||||||
/// An empty comment
|
/// An empty comment
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = CommentId.empty
|
Id = CommentId.empty
|
||||||
PostId = PostId.empty
|
PostId = PostId.empty
|
||||||
InReplyToId = None
|
InReplyToId = None
|
||||||
Name = ""
|
Name = ""
|
||||||
Email = ""
|
Email = ""
|
||||||
Url = None
|
Url = None
|
||||||
Status = Pending
|
Status = Pending
|
||||||
PostedOn = Noda.epoch
|
PostedOn = Noda.epoch
|
||||||
Text = ""
|
Text = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A page (text not associated with a date/time)
|
/// A page (text not associated with a date/time)
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Page =
|
type Page = {
|
||||||
{ /// The ID of this page
|
/// The ID of this page
|
||||||
Id : PageId
|
Id : PageId
|
||||||
|
|
||||||
/// The ID of the web log to which this page belongs
|
/// The ID of the web log to which this page belongs
|
||||||
WebLogId : WebLogId
|
WebLogId : WebLogId
|
||||||
|
|
||||||
/// The ID of the author of this page
|
/// The ID of the author of this page
|
||||||
AuthorId : WebLogUserId
|
AuthorId : WebLogUserId
|
||||||
|
|
||||||
/// The title of the page
|
/// The title of the page
|
||||||
Title : string
|
Title : string
|
||||||
|
|
||||||
/// The link at which this page is displayed
|
/// The link at which this page is displayed
|
||||||
Permalink : Permalink
|
Permalink : Permalink
|
||||||
|
|
||||||
/// When this page was published
|
/// When this page was published
|
||||||
PublishedOn : Instant
|
PublishedOn : Instant
|
||||||
|
|
||||||
/// When this page was last updated
|
/// When this page was last updated
|
||||||
UpdatedOn : Instant
|
UpdatedOn : Instant
|
||||||
|
|
||||||
/// Whether this page shows as part of the web log's navigation
|
/// Whether this page shows as part of the web log's navigation
|
||||||
IsInPageList : bool
|
IsInPageList : bool
|
||||||
|
|
||||||
/// The template to use when rendering this page
|
/// The template to use when rendering this page
|
||||||
Template : string option
|
Template : string option
|
||||||
|
|
||||||
/// The current text of the page
|
/// The current text of the page
|
||||||
Text : string
|
Text : string
|
||||||
|
|
||||||
/// Metadata for this page
|
/// Metadata for this page
|
||||||
Metadata : MetaItem list
|
Metadata : MetaItem list
|
||||||
|
|
||||||
/// Permalinks at which this page may have been previously served (useful for migrated content)
|
/// 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 of this page
|
||||||
Revisions : Revision list
|
Revisions : Revision list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support pages
|
/// Functions to support pages
|
||||||
module Page =
|
module Page =
|
||||||
|
|
||||||
/// An empty page
|
/// An empty page
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = PageId.empty
|
Id = PageId.empty
|
||||||
WebLogId = WebLogId.empty
|
WebLogId = WebLogId.empty
|
||||||
AuthorId = WebLogUserId.empty
|
AuthorId = WebLogUserId.empty
|
||||||
Title = ""
|
Title = ""
|
||||||
Permalink = Permalink.empty
|
Permalink = Permalink.empty
|
||||||
PublishedOn = Noda.epoch
|
PublishedOn = Noda.epoch
|
||||||
UpdatedOn = Noda.epoch
|
UpdatedOn = Noda.epoch
|
||||||
IsInPageList = false
|
IsInPageList = false
|
||||||
Template = None
|
Template = None
|
||||||
Text = ""
|
Text = ""
|
||||||
Metadata = []
|
Metadata = []
|
||||||
PriorPermalinks = []
|
PriorPermalinks = []
|
||||||
Revisions = []
|
Revisions = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A web log post
|
/// A web log post
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Post =
|
type Post = {
|
||||||
{ /// The ID of this post
|
/// The ID of this post
|
||||||
Id : PostId
|
Id : PostId
|
||||||
|
|
||||||
/// The ID of the web log to which this post belongs
|
/// The ID of the web log to which this post belongs
|
||||||
WebLogId : WebLogId
|
WebLogId : WebLogId
|
||||||
|
|
||||||
/// The ID of the author of this post
|
/// The ID of the author of this post
|
||||||
AuthorId : WebLogUserId
|
AuthorId : WebLogUserId
|
||||||
|
|
||||||
/// The status
|
/// The status
|
||||||
Status : PostStatus
|
Status : PostStatus
|
||||||
|
|
||||||
/// The title
|
/// The title
|
||||||
Title : string
|
Title : string
|
||||||
|
|
||||||
/// The link at which the post resides
|
/// The link at which the post resides
|
||||||
Permalink : Permalink
|
Permalink : Permalink
|
||||||
|
|
||||||
/// The instant on which the post was originally published
|
/// The instant on which the post was originally published
|
||||||
PublishedOn : Instant option
|
PublishedOn : Instant option
|
||||||
|
|
||||||
/// The instant on which the post was last updated
|
/// The instant on which the post was last updated
|
||||||
UpdatedOn : Instant
|
UpdatedOn : Instant
|
||||||
|
|
||||||
/// The template to use in displaying the post
|
/// 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
|
/// 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
|
/// The Ids of the categories to which this is assigned
|
||||||
CategoryIds : CategoryId list
|
CategoryIds : CategoryId list
|
||||||
|
|
||||||
/// The tags for the post
|
/// The tags for the post
|
||||||
Tags : string list
|
Tags : string list
|
||||||
|
|
||||||
/// Podcast episode information for this post
|
/// Podcast episode information for this post
|
||||||
Episode : Episode option
|
Episode : Episode option
|
||||||
|
|
||||||
/// Metadata for the post
|
/// Metadata for the post
|
||||||
Metadata : MetaItem list
|
Metadata : MetaItem list
|
||||||
|
|
||||||
/// Permalinks at which this post may have been previously served (useful for migrated content)
|
/// 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
|
/// The revisions for this post
|
||||||
Revisions : Revision list
|
Revisions : Revision list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support posts
|
/// Functions to support posts
|
||||||
module Post =
|
module Post =
|
||||||
|
|
||||||
/// An empty post
|
/// An empty post
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = PostId.empty
|
Id = PostId.empty
|
||||||
WebLogId = WebLogId.empty
|
WebLogId = WebLogId.empty
|
||||||
AuthorId = WebLogUserId.empty
|
AuthorId = WebLogUserId.empty
|
||||||
Status = Draft
|
Status = Draft
|
||||||
Title = ""
|
Title = ""
|
||||||
Permalink = Permalink.empty
|
Permalink = Permalink.empty
|
||||||
PublishedOn = None
|
PublishedOn = None
|
||||||
UpdatedOn = Noda.epoch
|
UpdatedOn = Noda.epoch
|
||||||
Text = ""
|
Text = ""
|
||||||
Template = None
|
Template = None
|
||||||
CategoryIds = []
|
CategoryIds = []
|
||||||
Tags = []
|
Tags = []
|
||||||
Episode = None
|
Episode = None
|
||||||
Metadata = []
|
Metadata = []
|
||||||
PriorPermalinks = []
|
PriorPermalinks = []
|
||||||
Revisions = []
|
Revisions = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A mapping between a tag and its URL value, used to translate restricted characters (ex. "#1" -> "number-1")
|
/// A mapping between a tag and its URL value, used to translate restricted characters (ex. "#1" -> "number-1")
|
||||||
type TagMap =
|
type TagMap = {
|
||||||
{ /// The ID of this tag mapping
|
/// The ID of this tag mapping
|
||||||
Id : TagMapId
|
Id : TagMapId
|
||||||
|
|
||||||
/// The ID of the web log to which this tag mapping belongs
|
/// 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
|
/// 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
|
/// The value by which the tag should be linked
|
||||||
UrlValue : string
|
UrlValue : string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support tag mappings
|
/// Functions to support tag mappings
|
||||||
module TagMap =
|
module TagMap =
|
||||||
|
|
||||||
/// An empty tag mapping
|
/// An empty tag mapping
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = TagMapId.empty
|
Id = TagMapId.empty
|
||||||
WebLogId = WebLogId.empty
|
WebLogId = WebLogId.empty
|
||||||
Tag = ""
|
Tag = ""
|
||||||
UrlValue = ""
|
UrlValue = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A theme
|
/// A theme
|
||||||
type Theme =
|
type Theme = {
|
||||||
{ /// The ID / path of the theme
|
/// The ID / path of the theme
|
||||||
Id : ThemeId
|
Id : ThemeId
|
||||||
|
|
||||||
/// A long name of the theme
|
/// A long name of the theme
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// The version of the theme
|
/// The version of the theme
|
||||||
Version : string
|
Version : string
|
||||||
|
|
||||||
/// The templates for this theme
|
/// The templates for this theme
|
||||||
Templates: ThemeTemplate list
|
Templates: ThemeTemplate list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support themes
|
/// Functions to support themes
|
||||||
module Theme =
|
module Theme =
|
||||||
|
|
||||||
/// An empty theme
|
/// An empty theme
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = ThemeId ""
|
Id = ThemeId ""
|
||||||
Name = ""
|
Name = ""
|
||||||
Version = ""
|
Version = ""
|
||||||
Templates = []
|
Templates = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path])
|
/// A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path])
|
||||||
type ThemeAsset =
|
type ThemeAsset = {
|
||||||
{
|
/// The ID of the asset (consists of theme and path)
|
||||||
/// 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)
|
||||||
/// The updated date (set from the file date from the ZIP archive)
|
UpdatedOn : Instant
|
||||||
UpdatedOn : Instant
|
|
||||||
|
/// The data for the asset
|
||||||
/// The data for the asset
|
Data : byte[]
|
||||||
Data : byte[]
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Functions to support theme assets
|
/// Functions to support theme assets
|
||||||
module ThemeAsset =
|
module ThemeAsset =
|
||||||
|
|
||||||
/// An empty theme asset
|
/// An empty theme asset
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = ThemeAssetId (ThemeId "", "")
|
Id = ThemeAssetId (ThemeId "", "")
|
||||||
UpdatedOn = Noda.epoch
|
UpdatedOn = Noda.epoch
|
||||||
Data = [||]
|
Data = [||]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// An uploaded file
|
/// An uploaded file
|
||||||
type Upload =
|
type Upload = {
|
||||||
{ /// The ID of the upload
|
/// The ID of the upload
|
||||||
Id : UploadId
|
Id : UploadId
|
||||||
|
|
||||||
/// The ID of the web log to which this upload belongs
|
/// The ID of the web log to which this upload belongs
|
||||||
WebLogId : WebLogId
|
WebLogId : WebLogId
|
||||||
|
|
||||||
/// The link at which this upload is served
|
/// The link at which this upload is served
|
||||||
Path : Permalink
|
Path : Permalink
|
||||||
|
|
||||||
/// The updated date/time for this upload
|
/// The updated date/time for this upload
|
||||||
UpdatedOn : Instant
|
UpdatedOn : Instant
|
||||||
|
|
||||||
/// The data for the upload
|
/// The data for the upload
|
||||||
Data : byte[]
|
Data : byte[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support uploaded files
|
/// Functions to support uploaded files
|
||||||
module Upload =
|
module Upload =
|
||||||
|
|
||||||
/// An empty upload
|
/// An empty upload
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = UploadId.empty
|
Id = UploadId.empty
|
||||||
WebLogId = WebLogId.empty
|
WebLogId = WebLogId.empty
|
||||||
Path = Permalink.empty
|
Path = Permalink.empty
|
||||||
UpdatedOn = Noda.epoch
|
UpdatedOn = Noda.epoch
|
||||||
Data = [||]
|
Data = [||]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A web log
|
/// A web log
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type WebLog =
|
type WebLog = {
|
||||||
{ /// The ID of the web log
|
/// The ID of the web log
|
||||||
Id : WebLogId
|
Id : WebLogId
|
||||||
|
|
||||||
/// The name of the web log
|
/// The name of the web log
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// The slug of the web log
|
/// The slug of the web log
|
||||||
Slug : string
|
Slug : string
|
||||||
|
|
||||||
/// A subtitle for the web log
|
/// A subtitle for the web log
|
||||||
Subtitle : string option
|
Subtitle : string option
|
||||||
|
|
||||||
/// The default page ("posts" or a page Id)
|
/// The default page ("posts" or a page Id)
|
||||||
DefaultPage : string
|
DefaultPage : string
|
||||||
|
|
||||||
/// The number of posts to display on pages of posts
|
/// The number of posts to display on pages of posts
|
||||||
PostsPerPage : int
|
PostsPerPage : int
|
||||||
|
|
||||||
/// The ID of the theme (also the path within /themes)
|
/// The ID of the theme (also the path within /themes)
|
||||||
ThemeId : ThemeId
|
ThemeId : ThemeId
|
||||||
|
|
||||||
/// The URL base
|
/// The URL base
|
||||||
UrlBase : string
|
UrlBase : string
|
||||||
|
|
||||||
/// The time zone in which dates/times should be displayed
|
/// The time zone in which dates/times should be displayed
|
||||||
TimeZone : string
|
TimeZone : string
|
||||||
|
|
||||||
/// The RSS options for this web log
|
/// The RSS options for this web log
|
||||||
Rss : RssOptions
|
Rss : RssOptions
|
||||||
|
|
||||||
/// Whether to automatically load htmx
|
/// Whether to automatically load htmx
|
||||||
AutoHtmx : bool
|
AutoHtmx : bool
|
||||||
|
|
||||||
/// Where uploads are placed
|
/// Where uploads are placed
|
||||||
Uploads : UploadDestination
|
Uploads : UploadDestination
|
||||||
|
|
||||||
/// Redirect rules for this weblog
|
/// Redirect rules for this weblog
|
||||||
RedirectRules : RedirectRule list
|
RedirectRules : RedirectRule list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support web logs
|
/// Functions to support web logs
|
||||||
module WebLog =
|
module WebLog =
|
||||||
|
|
||||||
/// An empty web log
|
/// An empty web log
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = WebLogId.empty
|
Id = WebLogId.empty
|
||||||
Name = ""
|
Name = ""
|
||||||
Slug = ""
|
Slug = ""
|
||||||
Subtitle = None
|
Subtitle = None
|
||||||
DefaultPage = ""
|
DefaultPage = ""
|
||||||
PostsPerPage = 10
|
PostsPerPage = 10
|
||||||
ThemeId = ThemeId "default"
|
ThemeId = ThemeId "default"
|
||||||
UrlBase = ""
|
UrlBase = ""
|
||||||
TimeZone = ""
|
TimeZone = ""
|
||||||
Rss = RssOptions.empty
|
Rss = RssOptions.empty
|
||||||
AutoHtmx = false
|
AutoHtmx = false
|
||||||
Uploads = Database
|
Uploads = Database
|
||||||
RedirectRules = []
|
RedirectRules = []
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the host (including scheme) and extra path from the URL base
|
/// Get the host (including scheme) and extra path from the URL base
|
||||||
let hostAndPath webLog =
|
let hostAndPath webLog =
|
||||||
let scheme = webLog.UrlBase.Split "://"
|
let scheme = webLog.UrlBase.Split "://"
|
||||||
let host = scheme[1].Split "/"
|
let host = scheme[1].Split "/"
|
||||||
$"{scheme[0]}://{host[0]}", if host.Length > 1 then $"""/{String.Join ("/", host |> Array.skip 1)}""" else ""
|
$"{scheme[0]}://{host[0]}", if host.Length > 1 then $"""/{String.Join("/", host |> Array.skip 1)}""" else ""
|
||||||
|
|
||||||
/// Generate an absolute URL for the given link
|
/// Generate an absolute URL for the given link
|
||||||
let absoluteUrl webLog permalink =
|
let absoluteUrl webLog permalink =
|
||||||
@ -418,71 +417,71 @@ module WebLog =
|
|||||||
/// Convert an Instant (UTC reference) to the web log's local date/time
|
/// Convert an Instant (UTC reference) to the web log's local date/time
|
||||||
let localTime webLog (date : Instant) =
|
let localTime webLog (date : Instant) =
|
||||||
match DateTimeZoneProviders.Tzdb[webLog.TimeZone] with
|
match DateTimeZoneProviders.Tzdb[webLog.TimeZone] with
|
||||||
| null -> date.ToDateTimeUtc ()
|
| null -> date.ToDateTimeUtc()
|
||||||
| tz -> date.InZone(tz).ToDateTimeUnspecified ()
|
| tz -> date.InZone(tz).ToDateTimeUnspecified()
|
||||||
|
|
||||||
|
|
||||||
/// A user of the web log
|
/// A user of the web log
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type WebLogUser =
|
type WebLogUser = {
|
||||||
{ /// The ID of the user
|
/// The ID of the user
|
||||||
Id : WebLogUserId
|
Id : WebLogUserId
|
||||||
|
|
||||||
/// The ID of the web log to which this user belongs
|
/// The ID of the web log to which this user belongs
|
||||||
WebLogId : WebLogId
|
WebLogId : WebLogId
|
||||||
|
|
||||||
/// The user name (e-mail address)
|
/// The user name (e-mail address)
|
||||||
Email : string
|
Email : string
|
||||||
|
|
||||||
/// The user's first name
|
/// The user's first name
|
||||||
FirstName : string
|
FirstName : string
|
||||||
|
|
||||||
/// The user's last name
|
/// The user's last name
|
||||||
LastName : string
|
LastName : string
|
||||||
|
|
||||||
/// The user's preferred name
|
/// The user's preferred name
|
||||||
PreferredName : string
|
PreferredName : string
|
||||||
|
|
||||||
/// The hash of the user's password
|
/// The hash of the user's password
|
||||||
PasswordHash : string
|
PasswordHash : string
|
||||||
|
|
||||||
/// The URL of the user's personal site
|
/// The URL of the user's personal site
|
||||||
Url : string option
|
Url : string option
|
||||||
|
|
||||||
/// The user's access level
|
/// The user's access level
|
||||||
AccessLevel : AccessLevel
|
AccessLevel : AccessLevel
|
||||||
|
|
||||||
/// When the user was created
|
/// When the user was created
|
||||||
CreatedOn : Instant
|
CreatedOn : Instant
|
||||||
|
|
||||||
/// When the user last logged on
|
/// When the user last logged on
|
||||||
LastSeenOn : Instant option
|
LastSeenOn : Instant option
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support web log users
|
/// Functions to support web log users
|
||||||
module WebLogUser =
|
module WebLogUser =
|
||||||
|
|
||||||
/// An empty web log user
|
/// An empty web log user
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = WebLogUserId.empty
|
Id = WebLogUserId.empty
|
||||||
WebLogId = WebLogId.empty
|
WebLogId = WebLogId.empty
|
||||||
Email = ""
|
Email = ""
|
||||||
FirstName = ""
|
FirstName = ""
|
||||||
LastName = ""
|
LastName = ""
|
||||||
PreferredName = ""
|
PreferredName = ""
|
||||||
PasswordHash = ""
|
PasswordHash = ""
|
||||||
Url = None
|
Url = None
|
||||||
AccessLevel = Author
|
AccessLevel = Author
|
||||||
CreatedOn = Noda.epoch
|
CreatedOn = Noda.epoch
|
||||||
LastSeenOn = None
|
LastSeenOn = None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the user's displayed name
|
/// Get the user's displayed name
|
||||||
let displayName user =
|
let displayName user =
|
||||||
let name =
|
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 (+)
|
|> Seq.reduce (+)
|
||||||
name.Trim ()
|
name.Trim()
|
||||||
|
|
||||||
/// Does a user have the required access level?
|
/// Does a user have the required access level?
|
||||||
let hasAccess level user =
|
let hasAccess level user =
|
||||||
|
@ -10,7 +10,7 @@ module private Helpers =
|
|||||||
/// Create a new ID (short GUID)
|
/// Create a new ID (short GUID)
|
||||||
// https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID
|
// https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID
|
||||||
let newId () =
|
let newId () =
|
||||||
Convert.ToBase64String(Guid.NewGuid().ToByteArray ()).Replace('/', '_').Replace('+', '-').Substring (0, 22)
|
Convert.ToBase64String(Guid.NewGuid().ToByteArray ()).Replace('/', '_').Replace('+', '-')[..22]
|
||||||
|
|
||||||
|
|
||||||
/// Functions to support NodaTime manipulation
|
/// Functions to support NodaTime manipulation
|
||||||
@ -21,19 +21,18 @@ module Noda =
|
|||||||
|
|
||||||
/// The Unix epoch
|
/// The Unix epoch
|
||||||
let epoch = Instant.FromUnixTimeSeconds 0L
|
let epoch = Instant.FromUnixTimeSeconds 0L
|
||||||
|
|
||||||
|
|
||||||
/// Truncate an instant to remove fractional seconds
|
/// Truncate an instant to remove fractional seconds
|
||||||
let toSecondsPrecision (value : Instant) =
|
let toSecondsPrecision (value : Instant) =
|
||||||
Instant.FromUnixTimeSeconds (value.ToUnixTimeSeconds ())
|
Instant.FromUnixTimeSeconds(value.ToUnixTimeSeconds())
|
||||||
|
|
||||||
/// The current Instant, with fractional seconds truncated
|
/// The current Instant, with fractional seconds truncated
|
||||||
let now () =
|
let now =
|
||||||
toSecondsPrecision (clock.GetCurrentInstant ())
|
clock.GetCurrentInstant >> toSecondsPrecision
|
||||||
|
|
||||||
/// Convert a date/time to an Instant with whole seconds
|
/// Convert a date/time to an Instant with whole seconds
|
||||||
let fromDateTime (dt : DateTime) =
|
let fromDateTime (dt : DateTime) =
|
||||||
toSecondsPrecision (Instant.FromDateTimeUtc (DateTime (dt.Ticks, DateTimeKind.Utc)))
|
Instant.FromDateTimeUtc(DateTime(dt.Ticks, DateTimeKind.Utc)) |> toSecondsPrecision
|
||||||
|
|
||||||
|
|
||||||
/// A user's access level
|
/// A user's access level
|
||||||
@ -94,7 +93,7 @@ module CategoryId =
|
|||||||
let toString = function CategoryId ci -> ci
|
let toString = function CategoryId ci -> ci
|
||||||
|
|
||||||
/// Create a new category ID
|
/// Create a new category ID
|
||||||
let create () = CategoryId (newId ())
|
let create = newId >> CategoryId
|
||||||
|
|
||||||
|
|
||||||
/// An identifier for a comment
|
/// An identifier for a comment
|
||||||
@ -110,7 +109,7 @@ module CommentId =
|
|||||||
let toString = function CommentId ci -> ci
|
let toString = function CommentId ci -> ci
|
||||||
|
|
||||||
/// Create a new comment ID
|
/// Create a new comment ID
|
||||||
let create () = CommentId (newId ())
|
let create = newId >> CommentId
|
||||||
|
|
||||||
|
|
||||||
/// Statuses for post comments
|
/// Statuses for post comments
|
||||||
@ -134,7 +133,7 @@ module CommentStatus =
|
|||||||
| "Approved" -> Approved
|
| "Approved" -> Approved
|
||||||
| "Pending" -> Pending
|
| "Pending" -> Pending
|
||||||
| "Spam" -> Spam
|
| "Spam" -> Spam
|
||||||
| it -> invalidOp $"{it} is not a valid post status"
|
| it -> invalidArg "status" $"{it} is not a valid comment status"
|
||||||
|
|
||||||
|
|
||||||
/// Valid values for the iTunes explicit rating
|
/// Valid values for the iTunes explicit rating
|
||||||
@ -158,127 +157,127 @@ module ExplicitRating =
|
|||||||
| "yes" -> Yes
|
| "yes" -> Yes
|
||||||
| "no" -> No
|
| "no" -> No
|
||||||
| "clean" -> Clean
|
| "clean" -> Clean
|
||||||
| x -> raise (invalidArg "rating" $"{x} is not a valid explicit rating")
|
| x -> invalidArg "rating" $"{x} is not a valid explicit rating"
|
||||||
|
|
||||||
|
|
||||||
/// A location (specified by Podcast Index)
|
/// A location (specified by Podcast Index)
|
||||||
type Location =
|
type Location = {
|
||||||
{ /// The name of the location (free-form text)
|
/// The name of the location (free-form text)
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// A geographic coordinate string (RFC 5870)
|
/// A geographic coordinate string (RFC 5870)
|
||||||
Geo : string option
|
Geo : string option
|
||||||
|
|
||||||
/// An OpenStreetMap query
|
/// An OpenStreetMap query
|
||||||
Osm : string option
|
Osm : string option
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A chapter in a podcast episode
|
/// A chapter in a podcast episode
|
||||||
type Chapter =
|
type Chapter = {
|
||||||
{ /// The start time for the chapter
|
/// The start time for the chapter
|
||||||
StartTime : Duration
|
StartTime : Duration
|
||||||
|
|
||||||
/// The title for this chapter
|
/// The title for this chapter
|
||||||
Title : string option
|
Title : string option
|
||||||
|
|
||||||
/// A URL for an image for this chapter
|
/// A URL for an image for this chapter
|
||||||
ImageUrl : string option
|
ImageUrl : string option
|
||||||
|
|
||||||
/// Whether this chapter is hidden
|
/// Whether this chapter is hidden
|
||||||
IsHidden : bool option
|
IsHidden : bool option
|
||||||
|
|
||||||
/// The episode end time for the chapter
|
/// The episode end time for the chapter
|
||||||
EndTime : Duration option
|
EndTime : Duration option
|
||||||
|
|
||||||
/// A location that applies to a chapter
|
/// A location that applies to a chapter
|
||||||
Location : Location option
|
Location : Location option
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
open NodaTime.Text
|
open NodaTime.Text
|
||||||
|
|
||||||
/// A podcast episode
|
/// A podcast episode
|
||||||
type Episode =
|
type Episode = {
|
||||||
{ /// The URL to the media file for the episode (may be permalink)
|
/// The URL to the media file for the episode (may be permalink)
|
||||||
Media : string
|
Media : string
|
||||||
|
|
||||||
/// The length of the media file, in bytes
|
/// The length of the media file, in bytes
|
||||||
Length : int64
|
Length : int64
|
||||||
|
|
||||||
/// The duration of the episode
|
/// The duration of the episode
|
||||||
Duration : Duration option
|
Duration : Duration option
|
||||||
|
|
||||||
/// The media type of the file (overrides podcast default if present)
|
/// 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)
|
/// 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
|
/// A subtitle for this episode
|
||||||
Subtitle : string option
|
Subtitle : string option
|
||||||
|
|
||||||
/// This episode's explicit rating (overrides podcast rating if present)
|
/// This episode's explicit rating (overrides podcast rating if present)
|
||||||
Explicit : ExplicitRating option
|
Explicit : ExplicitRating option
|
||||||
|
|
||||||
/// Chapters for this episode
|
/// Chapters for this episode
|
||||||
Chapters : Chapter list option
|
Chapters : Chapter list option
|
||||||
|
|
||||||
/// A link to a chapter file
|
/// A link to a chapter file
|
||||||
ChapterFile : string option
|
ChapterFile : string option
|
||||||
|
|
||||||
/// The MIME type for the chapter file
|
/// The MIME type for the chapter file
|
||||||
ChapterType : string option
|
ChapterType : string option
|
||||||
|
|
||||||
/// The URL for the transcript of the episode (may be permalink)
|
/// The URL for the transcript of the episode (may be permalink)
|
||||||
TranscriptUrl : string option
|
TranscriptUrl : string option
|
||||||
|
|
||||||
/// The MIME type of the transcript
|
/// The MIME type of the transcript
|
||||||
TranscriptType : string option
|
TranscriptType : string option
|
||||||
|
|
||||||
/// The language in which the transcript is written
|
/// 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
|
/// 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)
|
/// The season number (for serialized podcasts)
|
||||||
SeasonNumber : int option
|
SeasonNumber : int option
|
||||||
|
|
||||||
/// A description of the season
|
/// A description of the season
|
||||||
SeasonDescription : string option
|
SeasonDescription : string option
|
||||||
|
|
||||||
/// The episode number
|
/// The episode number
|
||||||
EpisodeNumber : double option
|
EpisodeNumber : double option
|
||||||
|
|
||||||
/// A description of the episode
|
/// A description of the episode
|
||||||
EpisodeDescription : string option
|
EpisodeDescription : string option
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support episodes
|
/// Functions to support episodes
|
||||||
module Episode =
|
module Episode =
|
||||||
|
|
||||||
/// An empty episode
|
/// An empty episode
|
||||||
let empty =
|
let empty = {
|
||||||
{ Media = ""
|
Media = ""
|
||||||
Length = 0L
|
Length = 0L
|
||||||
Duration = None
|
Duration = None
|
||||||
MediaType = None
|
MediaType = None
|
||||||
ImageUrl = None
|
ImageUrl = None
|
||||||
Subtitle = None
|
Subtitle = None
|
||||||
Explicit = None
|
Explicit = None
|
||||||
Chapters = None
|
Chapters = None
|
||||||
ChapterFile = None
|
ChapterFile = None
|
||||||
ChapterType = None
|
ChapterType = None
|
||||||
TranscriptUrl = None
|
TranscriptUrl = None
|
||||||
TranscriptType = None
|
TranscriptType = None
|
||||||
TranscriptLang = None
|
TranscriptLang = None
|
||||||
TranscriptCaptions = None
|
TranscriptCaptions = None
|
||||||
SeasonNumber = None
|
SeasonNumber = None
|
||||||
SeasonDescription = None
|
SeasonDescription = None
|
||||||
EpisodeNumber = None
|
EpisodeNumber = None
|
||||||
EpisodeDescription = None
|
EpisodeDescription = None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a duration for an episode
|
/// Format a duration for an episode
|
||||||
let formatDuration ep =
|
let formatDuration ep =
|
||||||
@ -299,7 +298,7 @@ type MarkupText =
|
|||||||
module MarkupText =
|
module MarkupText =
|
||||||
|
|
||||||
/// Pipeline with most extensions enabled
|
/// Pipeline with most extensions enabled
|
||||||
let private _pipeline = MarkdownPipelineBuilder().UseSmartyPants().UseAdvancedExtensions().UseColorCode().Build ()
|
let private _pipeline = MarkdownPipelineBuilder().UseSmartyPants().UseAdvancedExtensions().UseColorCode().Build()
|
||||||
|
|
||||||
/// Get the source type for the markup text
|
/// Get the source type for the markup text
|
||||||
let sourceType = function Markdown _ -> "Markdown" | Html _ -> "HTML"
|
let sourceType = function Markdown _ -> "Markdown" | Html _ -> "HTML"
|
||||||
@ -311,25 +310,25 @@ module MarkupText =
|
|||||||
let toString it = $"{sourceType it}: {text it}"
|
let toString it = $"{sourceType it}: {text it}"
|
||||||
|
|
||||||
/// Get the HTML representation of the markup text
|
/// Get the HTML representation of the markup text
|
||||||
let toHtml = function Markdown text -> Markdown.ToHtml (text, _pipeline) | Html text -> text
|
let toHtml = function Markdown text -> Markdown.ToHtml(text, _pipeline) | Html text -> text
|
||||||
|
|
||||||
/// Parse a string into a MarkupText instance
|
/// Parse a string into a MarkupText instance
|
||||||
let parse (it : string) =
|
let parse (it : string) =
|
||||||
match it with
|
match it with
|
||||||
| text when text.StartsWith "Markdown: " -> Markdown (text.Substring 10)
|
| text when text.StartsWith "Markdown: " -> Markdown text[10..]
|
||||||
| text when text.StartsWith "HTML: " -> Html (text.Substring 6)
|
| text when text.StartsWith "HTML: " -> Html text[6..]
|
||||||
| text -> invalidOp $"Cannot derive type of text ({text})"
|
| text -> invalidOp $"Cannot derive type of text ({text})"
|
||||||
|
|
||||||
|
|
||||||
/// An item of metadata
|
/// An item of metadata
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type MetaItem =
|
type MetaItem = {
|
||||||
{ /// The name of the metadata value
|
/// The name of the metadata value
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// The metadata value
|
/// The metadata value
|
||||||
Value : string
|
Value : string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support metadata items
|
/// Functions to support metadata items
|
||||||
module MetaItem =
|
module MetaItem =
|
||||||
@ -340,22 +339,20 @@ module MetaItem =
|
|||||||
|
|
||||||
/// A revision of a page or post
|
/// A revision of a page or post
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Revision =
|
type Revision = {
|
||||||
{ /// When this revision was saved
|
/// When this revision was saved
|
||||||
AsOf : Instant
|
AsOf : Instant
|
||||||
|
|
||||||
/// The text of the revision
|
/// The text of the revision
|
||||||
Text : MarkupText
|
Text : MarkupText
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support revisions
|
/// Functions to support revisions
|
||||||
module Revision =
|
module Revision =
|
||||||
|
|
||||||
/// An empty revision
|
/// An empty revision
|
||||||
let empty =
|
let empty =
|
||||||
{ AsOf = Noda.epoch
|
{ AsOf = Noda.epoch; Text = Html "" }
|
||||||
Text = Html ""
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A permanent link
|
/// A permanent link
|
||||||
@ -384,7 +381,7 @@ module PageId =
|
|||||||
let toString = function PageId pi -> pi
|
let toString = function PageId pi -> pi
|
||||||
|
|
||||||
/// Create a new page ID
|
/// Create a new page ID
|
||||||
let create () = PageId (newId ())
|
let create = newId >> PageId
|
||||||
|
|
||||||
|
|
||||||
/// PodcastIndex.org podcast:medium allowed values
|
/// PodcastIndex.org podcast:medium allowed values
|
||||||
@ -421,7 +418,7 @@ module PodcastMedium =
|
|||||||
| "audiobook" -> Audiobook
|
| "audiobook" -> Audiobook
|
||||||
| "newsletter" -> Newsletter
|
| "newsletter" -> Newsletter
|
||||||
| "blog" -> Blog
|
| "blog" -> Blog
|
||||||
| it -> invalidOp $"{it} is not a valid podcast medium"
|
| it -> invalidArg "medium" $"{it} is not a valid podcast medium"
|
||||||
|
|
||||||
|
|
||||||
/// Statuses for posts
|
/// Statuses for posts
|
||||||
@ -442,7 +439,7 @@ module PostStatus =
|
|||||||
match value with
|
match value with
|
||||||
| "Draft" -> Draft
|
| "Draft" -> Draft
|
||||||
| "Published" -> Published
|
| "Published" -> Published
|
||||||
| it -> invalidOp $"{it} is not a valid post status"
|
| it -> invalidArg "status" $"{it} is not a valid post status"
|
||||||
|
|
||||||
|
|
||||||
/// An identifier for a post
|
/// An identifier for a post
|
||||||
@ -458,30 +455,30 @@ module PostId =
|
|||||||
let toString = function PostId pi -> pi
|
let toString = function PostId pi -> pi
|
||||||
|
|
||||||
/// Create a new post ID
|
/// Create a new post ID
|
||||||
let create () = PostId (newId ())
|
let create = newId >> PostId
|
||||||
|
|
||||||
|
|
||||||
/// A redirection for a previously valid URL
|
/// A redirection for a previously valid URL
|
||||||
type RedirectRule =
|
type RedirectRule = {
|
||||||
{ /// The From string or pattern
|
/// The From string or pattern
|
||||||
From : string
|
From : string
|
||||||
|
|
||||||
/// The To string or pattern
|
/// The To string or pattern
|
||||||
To : string
|
To : string
|
||||||
|
|
||||||
/// Whether to use regular expressions on this rule
|
/// Whether to use regular expressions on this rule
|
||||||
IsRegex : bool
|
IsRegex : bool
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support redirect rules
|
/// Functions to support redirect rules
|
||||||
module RedirectRule =
|
module RedirectRule =
|
||||||
|
|
||||||
/// An empty redirect rule
|
/// An empty redirect rule
|
||||||
let empty =
|
let empty = {
|
||||||
{ From = ""
|
From = ""
|
||||||
To = ""
|
To = ""
|
||||||
IsRegex = false
|
IsRegex = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// An identifier for a custom feed
|
/// An identifier for a custom feed
|
||||||
@ -497,7 +494,7 @@ module CustomFeedId =
|
|||||||
let toString = function CustomFeedId pi -> pi
|
let toString = function CustomFeedId pi -> pi
|
||||||
|
|
||||||
/// Create a new custom feed ID
|
/// Create a new custom feed ID
|
||||||
let create () = CustomFeedId (newId ())
|
let create = newId >> CustomFeedId
|
||||||
|
|
||||||
|
|
||||||
/// The source for a custom feed
|
/// The source for a custom feed
|
||||||
@ -525,122 +522,122 @@ module CustomFeedSource =
|
|||||||
|
|
||||||
|
|
||||||
/// Options for a feed that describes a podcast
|
/// Options for a feed that describes a podcast
|
||||||
type PodcastOptions =
|
type PodcastOptions = {
|
||||||
{ /// The title of the podcast
|
/// The title of the podcast
|
||||||
Title : string
|
Title : string
|
||||||
|
|
||||||
/// A subtitle for the podcast
|
/// A subtitle for the podcast
|
||||||
Subtitle : string option
|
Subtitle : string option
|
||||||
|
|
||||||
/// The number of items in the podcast feed
|
/// The number of items in the podcast feed
|
||||||
ItemsInFeed : int
|
ItemsInFeed : int
|
||||||
|
|
||||||
/// A summary of the podcast (iTunes field)
|
/// A summary of the podcast (iTunes field)
|
||||||
Summary : string
|
Summary : string
|
||||||
|
|
||||||
/// The display name of the podcast author (iTunes field)
|
/// 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
|
/// 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
|
/// The link to the image for the podcast
|
||||||
ImageUrl : Permalink
|
ImageUrl : Permalink
|
||||||
|
|
||||||
/// The category from Apple Podcasts (iTunes) under which this podcast is categorized
|
/// The category from Apple Podcasts (iTunes) under which this podcast is categorized
|
||||||
AppleCategory : string
|
AppleCategory : string
|
||||||
|
|
||||||
/// A further refinement of the categorization of this podcast (Apple Podcasts/iTunes field / values)
|
/// A further refinement of the categorization of this podcast (Apple Podcasts/iTunes field / values)
|
||||||
AppleSubcategory : string option
|
AppleSubcategory : string option
|
||||||
|
|
||||||
/// The explictness rating (iTunes field)
|
/// The explictness rating (iTunes field)
|
||||||
Explicit : ExplicitRating
|
Explicit : ExplicitRating
|
||||||
|
|
||||||
/// The default media type for files in this podcast
|
/// 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)
|
/// 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
|
/// A GUID for this podcast
|
||||||
PodcastGuid : Guid option
|
PodcastGuid : Guid option
|
||||||
|
|
||||||
/// A URL at which information on supporting the podcast may be found (supports permalinks)
|
/// 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
|
/// 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)
|
/// The medium (what the podcast IS, not what it is ABOUT)
|
||||||
Medium : PodcastMedium option
|
Medium : PodcastMedium option
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A custom feed
|
/// A custom feed
|
||||||
type CustomFeed =
|
type CustomFeed = {
|
||||||
{ /// The ID of the custom feed
|
/// The ID of the custom feed
|
||||||
Id : CustomFeedId
|
Id : CustomFeedId
|
||||||
|
|
||||||
/// The source for the custom feed
|
/// The source for the custom feed
|
||||||
Source : CustomFeedSource
|
Source : CustomFeedSource
|
||||||
|
|
||||||
/// The path for the custom feed
|
/// The path for the custom feed
|
||||||
Path : Permalink
|
Path : Permalink
|
||||||
|
|
||||||
/// Podcast options, if the feed defines a podcast
|
/// Podcast options, if the feed defines a podcast
|
||||||
Podcast : PodcastOptions option
|
Podcast : PodcastOptions option
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support custom feeds
|
/// Functions to support custom feeds
|
||||||
module CustomFeed =
|
module CustomFeed =
|
||||||
|
|
||||||
/// An empty custom feed
|
/// An empty custom feed
|
||||||
let empty =
|
let empty = {
|
||||||
{ Id = CustomFeedId ""
|
Id = CustomFeedId ""
|
||||||
Source = Category (CategoryId "")
|
Source = Category (CategoryId "")
|
||||||
Path = Permalink ""
|
Path = Permalink ""
|
||||||
Podcast = None
|
Podcast = None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Really Simple Syndication (RSS) options for this web log
|
/// Really Simple Syndication (RSS) options for this web log
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type RssOptions =
|
type RssOptions = {
|
||||||
{ /// Whether the site feed of posts is enabled
|
/// Whether the site feed of posts is enabled
|
||||||
IsFeedEnabled : bool
|
IsFeedEnabled : bool
|
||||||
|
|
||||||
/// The name of the file generated for the site feed
|
/// The name of the file generated for the site feed
|
||||||
FeedName : string
|
FeedName : string
|
||||||
|
|
||||||
/// Override the "posts per page" setting for the site feed
|
/// Override the "posts per page" setting for the site feed
|
||||||
ItemsInFeed : int option
|
ItemsInFeed : int option
|
||||||
|
|
||||||
/// Whether feeds are enabled for all categories
|
/// Whether feeds are enabled for all categories
|
||||||
IsCategoryEnabled : bool
|
IsCategoryEnabled : bool
|
||||||
|
|
||||||
/// Whether feeds are enabled for all tags
|
/// Whether feeds are enabled for all tags
|
||||||
IsTagEnabled : bool
|
IsTagEnabled : bool
|
||||||
|
|
||||||
/// A copyright string to be placed in all feeds
|
/// A copyright string to be placed in all feeds
|
||||||
Copyright : string option
|
Copyright : string option
|
||||||
|
|
||||||
/// Custom feeds for this web log
|
/// Custom feeds for this web log
|
||||||
CustomFeeds: CustomFeed list
|
CustomFeeds: CustomFeed list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support RSS options
|
/// Functions to support RSS options
|
||||||
module RssOptions =
|
module RssOptions =
|
||||||
|
|
||||||
/// An empty set of RSS options
|
/// An empty set of RSS options
|
||||||
let empty =
|
let empty = {
|
||||||
{ IsFeedEnabled = true
|
IsFeedEnabled = true
|
||||||
FeedName = "feed.xml"
|
FeedName = "feed.xml"
|
||||||
ItemsInFeed = None
|
ItemsInFeed = None
|
||||||
IsCategoryEnabled = true
|
IsCategoryEnabled = true
|
||||||
IsTagEnabled = true
|
IsTagEnabled = true
|
||||||
Copyright = None
|
Copyright = None
|
||||||
CustomFeeds = []
|
CustomFeeds = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// An identifier for a tag mapping
|
/// An identifier for a tag mapping
|
||||||
@ -656,7 +653,7 @@ module TagMapId =
|
|||||||
let toString = function TagMapId tmi -> tmi
|
let toString = function TagMapId tmi -> tmi
|
||||||
|
|
||||||
/// Create a new tag mapping ID
|
/// Create a new tag mapping ID
|
||||||
let create () = TagMapId (newId ())
|
let create = newId >> TagMapId
|
||||||
|
|
||||||
|
|
||||||
/// An identifier for a theme (represents its path)
|
/// An identifier for a theme (represents its path)
|
||||||
@ -683,22 +680,20 @@ module ThemeAssetId =
|
|||||||
|
|
||||||
|
|
||||||
/// A template for a theme
|
/// A template for a theme
|
||||||
type ThemeTemplate =
|
type ThemeTemplate = {
|
||||||
{ /// The name of the template
|
/// The name of the template
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// The text of the template
|
/// The text of the template
|
||||||
Text : string
|
Text : string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support theme templates
|
/// Functions to support theme templates
|
||||||
module ThemeTemplate =
|
module ThemeTemplate =
|
||||||
|
|
||||||
/// An empty theme template
|
/// An empty theme template
|
||||||
let empty =
|
let empty =
|
||||||
{ Name = ""
|
{ Name = ""; Text = "" }
|
||||||
Text = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Where uploads should be placed
|
/// Where uploads should be placed
|
||||||
@ -717,7 +712,7 @@ module UploadDestination =
|
|||||||
match value with
|
match value with
|
||||||
| "Database" -> Database
|
| "Database" -> Database
|
||||||
| "Disk" -> Disk
|
| "Disk" -> Disk
|
||||||
| it -> invalidOp $"{it} is not a valid upload destination"
|
| it -> invalidArg "destination" $"{it} is not a valid upload destination"
|
||||||
|
|
||||||
|
|
||||||
/// An identifier for an upload
|
/// An identifier for an upload
|
||||||
@ -733,7 +728,7 @@ module UploadId =
|
|||||||
let toString = function UploadId ui -> ui
|
let toString = function UploadId ui -> ui
|
||||||
|
|
||||||
/// Create a new upload ID
|
/// Create a new upload ID
|
||||||
let create () = UploadId (newId ())
|
let create = newId >> UploadId
|
||||||
|
|
||||||
|
|
||||||
/// An identifier for a web log
|
/// An identifier for a web log
|
||||||
@ -749,7 +744,7 @@ module WebLogId =
|
|||||||
let toString = function WebLogId wli -> wli
|
let toString = function WebLogId wli -> wli
|
||||||
|
|
||||||
/// Create a new web log ID
|
/// Create a new web log ID
|
||||||
let create () = WebLogId (newId ())
|
let create = newId >> WebLogId
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -766,6 +761,6 @@ module WebLogUserId =
|
|||||||
let toString = function WebLogUserId wli -> wli
|
let toString = function WebLogUserId wli -> wli
|
||||||
|
|
||||||
/// Create a new web log user ID
|
/// Create a new web log user ID
|
||||||
let create () = WebLogUserId (newId ())
|
let create = newId >> WebLogUserId
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ module private Helpers =
|
|||||||
|
|
||||||
/// Create a string option if a string is blank
|
/// Create a string option if a string is blank
|
||||||
let noneIfBlank (it : string) =
|
let noneIfBlank (it : string) =
|
||||||
match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed
|
match (defaultArg (Option.ofObj it) "").Trim() with "" -> None | trimmed -> Some trimmed
|
||||||
|
|
||||||
|
|
||||||
/// Helper functions that are needed outside this file
|
/// Helper functions that are needed outside this file
|
||||||
@ -26,67 +26,70 @@ module PublicHelpers =
|
|||||||
|
|
||||||
/// The model used to display the admin dashboard
|
/// The model used to display the admin dashboard
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DashboardModel =
|
type DashboardModel = {
|
||||||
{ /// The number of published posts
|
/// The number of published posts
|
||||||
Posts : int
|
Posts : int
|
||||||
|
|
||||||
/// The number of post drafts
|
/// The number of post drafts
|
||||||
Drafts : int
|
Drafts : int
|
||||||
|
|
||||||
/// The number of pages
|
/// The number of pages
|
||||||
Pages : int
|
Pages : int
|
||||||
|
|
||||||
/// The number of pages in the page list
|
/// The number of pages in the page list
|
||||||
ListedPages : int
|
ListedPages : int
|
||||||
|
|
||||||
/// The number of categories
|
/// The number of categories
|
||||||
Categories : int
|
Categories : int
|
||||||
|
|
||||||
/// The top-level categories
|
/// The top-level categories
|
||||||
TopLevelCategories : int
|
TopLevelCategories : int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Details about a category, used to display category lists
|
/// Details about a category, used to display category lists
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DisplayCategory =
|
type DisplayCategory = {
|
||||||
{ /// The ID of the category
|
/// The ID of the category
|
||||||
Id : string
|
Id : string
|
||||||
|
|
||||||
/// The slug for the category
|
/// The slug for the category
|
||||||
Slug : string
|
Slug : string
|
||||||
|
|
||||||
/// The name of the category
|
/// The name of the category
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// A description of the category
|
/// A description of the category
|
||||||
Description : string option
|
Description : string option
|
||||||
|
|
||||||
/// The parent category names for this (sub)category
|
/// The parent category names for this (sub)category
|
||||||
ParentNames : string[]
|
ParentNames : string[]
|
||||||
|
|
||||||
/// The number of posts in this category
|
/// The number of posts in this category
|
||||||
PostCount : int
|
PostCount : int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A display version of a custom feed definition
|
/// A display version of a custom feed definition
|
||||||
type DisplayCustomFeed =
|
type DisplayCustomFeed = {
|
||||||
{ /// The ID of the custom feed
|
/// The ID of the custom feed
|
||||||
Id : string
|
Id : string
|
||||||
|
|
||||||
/// The source of the custom feed
|
/// The source of the custom feed
|
||||||
Source : string
|
Source : string
|
||||||
|
|
||||||
/// The relative path at which the custom feed is served
|
/// The relative path at which the custom feed is served
|
||||||
Path : string
|
Path : string
|
||||||
|
|
||||||
/// Whether this custom feed is for a podcast
|
/// Whether this custom feed is for a podcast
|
||||||
IsPodcast : bool
|
IsPodcast : bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Support functions for custom feed displays
|
||||||
|
module DisplayCustomFeed =
|
||||||
|
|
||||||
/// Create a display version from a custom feed
|
/// Create a display version from a custom feed
|
||||||
static member fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed =
|
let fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed =
|
||||||
let source =
|
let source =
|
||||||
match feed.Source with
|
match feed.Source with
|
||||||
| Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}"
|
| Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}"
|
||||||
@ -133,7 +136,7 @@ type DisplayPage =
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a minimal display page (no text or metadata) from a database page
|
/// Create a minimal display page (no text or metadata) from a database page
|
||||||
static member fromPageMinimal webLog (page : Page) =
|
static member FromPageMinimal webLog (page : Page) =
|
||||||
let pageId = PageId.toString page.Id
|
let pageId = PageId.toString page.Id
|
||||||
{ Id = pageId
|
{ Id = pageId
|
||||||
AuthorId = WebLogUserId.toString page.AuthorId
|
AuthorId = WebLogUserId.toString page.AuthorId
|
||||||
@ -148,7 +151,7 @@ type DisplayPage =
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a display page from a database page
|
/// Create a display page from a database page
|
||||||
static member fromPage webLog (page : Page) =
|
static member FromPage webLog (page : Page) =
|
||||||
let _, extra = WebLog.hostAndPath webLog
|
let _, extra = WebLog.hostAndPath webLog
|
||||||
let pageId = PageId.toString page.Id
|
let pageId = PageId.toString page.Id
|
||||||
{ Id = pageId
|
{ Id = pageId
|
||||||
@ -166,20 +169,22 @@ type DisplayPage =
|
|||||||
|
|
||||||
/// Information about a revision used for display
|
/// Information about a revision used for display
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DisplayRevision =
|
type DisplayRevision = {
|
||||||
{ /// The as-of date/time for the revision
|
/// The as-of date/time for the revision
|
||||||
AsOf : DateTime
|
AsOf : DateTime
|
||||||
|
|
||||||
/// The as-of date/time for the revision in the web log's local time zone
|
/// The as-of date/time for the revision in the web log's local time zone
|
||||||
AsOfLocal : DateTime
|
AsOfLocal : DateTime
|
||||||
|
|
||||||
/// The format of the text of the revision
|
/// The format of the text of the revision
|
||||||
Format : string
|
Format : string
|
||||||
}
|
}
|
||||||
with
|
|
||||||
|
|
||||||
|
/// Functions to support displaying revisions
|
||||||
|
module DisplayRevision =
|
||||||
|
|
||||||
/// Create a display revision from an actual revision
|
/// Create a display revision from an actual revision
|
||||||
static member fromRevision webLog (rev : Revision) =
|
let fromRevision webLog (rev : Revision) =
|
||||||
{ AsOf = rev.AsOf.ToDateTimeUtc ()
|
{ AsOf = rev.AsOf.ToDateTimeUtc ()
|
||||||
AsOfLocal = WebLog.localTime webLog rev.AsOf
|
AsOfLocal = WebLog.localTime webLog rev.AsOf
|
||||||
Format = MarkupText.sourceType rev.Text
|
Format = MarkupText.sourceType rev.Text
|
||||||
@ -190,29 +195,31 @@ open System.IO
|
|||||||
|
|
||||||
/// Information about a theme used for display
|
/// Information about a theme used for display
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DisplayTheme =
|
type DisplayTheme = {
|
||||||
{ /// The ID / path slug of the theme
|
/// The ID / path slug of the theme
|
||||||
Id : string
|
Id : string
|
||||||
|
|
||||||
/// The name of the theme
|
/// The name of the theme
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// The version of the theme
|
/// The version of the theme
|
||||||
Version : string
|
Version : string
|
||||||
|
|
||||||
/// How many templates are contained in the theme
|
/// How many templates are contained in the theme
|
||||||
TemplateCount : int
|
TemplateCount : int
|
||||||
|
|
||||||
/// Whether the theme is in use by any web logs
|
/// Whether the theme is in use by any web logs
|
||||||
IsInUse : bool
|
IsInUse : bool
|
||||||
|
|
||||||
/// Whether the theme .zip file exists on the filesystem
|
/// Whether the theme .zip file exists on the filesystem
|
||||||
IsOnDisk : bool
|
IsOnDisk : bool
|
||||||
}
|
}
|
||||||
with
|
|
||||||
|
/// Functions to support displaying themes
|
||||||
|
module DisplayTheme =
|
||||||
|
|
||||||
/// Create a display theme from a theme
|
/// Create a display theme from a theme
|
||||||
static member fromTheme inUseFunc (theme : Theme) =
|
let fromTheme inUseFunc (theme : Theme) =
|
||||||
{ Id = ThemeId.toString theme.Id
|
{ Id = ThemeId.toString theme.Id
|
||||||
Name = theme.Name
|
Name = theme.Name
|
||||||
Version = theme.Version
|
Version = theme.Version
|
||||||
@ -224,25 +231,28 @@ with
|
|||||||
|
|
||||||
/// Information about an uploaded file used for display
|
/// Information about an uploaded file used for display
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DisplayUpload =
|
type DisplayUpload = {
|
||||||
{ /// The ID of the uploaded file
|
/// The ID of the uploaded file
|
||||||
Id : string
|
Id : string
|
||||||
|
|
||||||
/// The name of the uploaded file
|
/// The name of the uploaded file
|
||||||
Name : string
|
Name : string
|
||||||
|
|
||||||
/// The path at which the file is served
|
/// The path at which the file is served
|
||||||
Path : string
|
Path : string
|
||||||
|
|
||||||
/// The date/time the file was updated
|
/// The date/time the file was updated
|
||||||
UpdatedOn : DateTime option
|
UpdatedOn : DateTime option
|
||||||
|
|
||||||
/// The source for this file (created from UploadDestination DU)
|
/// The source for this file (created from UploadDestination DU)
|
||||||
Source : string
|
Source : string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Functions to support displaying uploads
|
||||||
|
module DisplayUpload =
|
||||||
|
|
||||||
/// Create a display uploaded file
|
/// Create a display uploaded file
|
||||||
static member fromUpload webLog source (upload : Upload) =
|
let fromUpload webLog source (upload : Upload) =
|
||||||
let path = Permalink.toString upload.Path
|
let path = Permalink.toString upload.Path
|
||||||
let name = Path.GetFileName path
|
let name = Path.GetFileName path
|
||||||
{ Id = UploadId.toString upload.Id
|
{ Id = UploadId.toString upload.Id
|
||||||
@ -255,37 +265,40 @@ type DisplayUpload =
|
|||||||
|
|
||||||
/// View model to display a user's information
|
/// View model to display a user's information
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DisplayUser =
|
type DisplayUser = {
|
||||||
{ /// The ID of the user
|
/// The ID of the user
|
||||||
Id : string
|
Id : string
|
||||||
|
|
||||||
/// The user name (e-mail address)
|
/// The user name (e-mail address)
|
||||||
Email : string
|
Email : string
|
||||||
|
|
||||||
/// The user's first name
|
/// The user's first name
|
||||||
FirstName : string
|
FirstName : string
|
||||||
|
|
||||||
/// The user's last name
|
/// The user's last name
|
||||||
LastName : string
|
LastName : string
|
||||||
|
|
||||||
/// The user's preferred name
|
/// The user's preferred name
|
||||||
PreferredName : string
|
PreferredName : string
|
||||||
|
|
||||||
/// The URL of the user's personal site
|
/// The URL of the user's personal site
|
||||||
Url : string
|
Url : string
|
||||||
|
|
||||||
/// The user's access level
|
/// The user's access level
|
||||||
AccessLevel : string
|
AccessLevel : string
|
||||||
|
|
||||||
/// When the user was created
|
/// When the user was created
|
||||||
CreatedOn : DateTime
|
CreatedOn : DateTime
|
||||||
|
|
||||||
/// When the user last logged on
|
/// When the user last logged on
|
||||||
LastSeenOn : Nullable<DateTime>
|
LastSeenOn : Nullable<DateTime>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Functions to support displaying a user's information
|
||||||
|
module DisplayUser =
|
||||||
|
|
||||||
/// Construct a displayed user from a web log user
|
/// Construct a displayed user from a web log user
|
||||||
static member fromUser webLog (user : WebLogUser) =
|
let fromUser webLog (user : WebLogUser) =
|
||||||
{ Id = WebLogUserId.toString user.Id
|
{ Id = WebLogUserId.toString user.Id
|
||||||
Email = user.Email
|
Email = user.Email
|
||||||
FirstName = user.FirstName
|
FirstName = user.FirstName
|
||||||
|
@ -131,7 +131,7 @@ module PageListCache =
|
|||||||
let private fillPages (webLog : WebLog) pages =
|
let private fillPages (webLog : WebLog) pages =
|
||||||
_cache[webLog.Id] <-
|
_cache[webLog.Id] <-
|
||||||
pages
|
pages
|
||||||
|> List.map (fun pg -> DisplayPage.fromPage webLog { pg with Text = "" })
|
|> List.map (fun pg -> DisplayPage.FromPage webLog { pg with Text = "" })
|
||||||
|> Array.ofList
|
|> Array.ofList
|
||||||
|
|
||||||
/// Are there pages cached for this web log?
|
/// Are there pages cached for this web log?
|
||||||
|
@ -15,7 +15,7 @@ let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||||||
|> addToHash "pages" (pages
|
|> addToHash "pages" (pages
|
||||||
|> Seq.ofList
|
|> Seq.ofList
|
||||||
|> Seq.truncate 25
|
|> Seq.truncate 25
|
||||||
|> Seq.map (DisplayPage.fromPageMinimal ctx.WebLog)
|
|> Seq.map (DisplayPage.FromPageMinimal ctx.WebLog)
|
||||||
|> List.ofSeq)
|
|> List.ofSeq)
|
||||||
|> addToHash "page_nbr" pageNbr
|
|> addToHash "page_nbr" pageNbr
|
||||||
|> addToHash "prev_page" (if pageNbr = 2 then "" else $"/page/{pageNbr - 1}")
|
|> addToHash "prev_page" (if pageNbr = 2 then "" else $"/page/{pageNbr - 1}")
|
||||||
|
@ -200,7 +200,7 @@ let home : HttpHandler = fun next ctx -> task {
|
|||||||
| Some page ->
|
| Some page ->
|
||||||
return!
|
return!
|
||||||
hashForPage page.Title
|
hashForPage page.Title
|
||||||
|> addToHash "page" (DisplayPage.fromPage webLog page)
|
|> addToHash "page" (DisplayPage.FromPage webLog page)
|
||||||
|> addToHash ViewContext.IsHome true
|
|> addToHash ViewContext.IsHome true
|
||||||
|> themedView (defaultArg page.Template "single-page") next ctx
|
|> themedView (defaultArg page.Template "single-page") next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
|
@ -40,7 +40,7 @@ module CatchAll =
|
|||||||
debug (fun () -> "Found page by permalink")
|
debug (fun () -> "Found page by permalink")
|
||||||
yield fun next ctx ->
|
yield fun next ctx ->
|
||||||
hashForPage page.Title
|
hashForPage page.Title
|
||||||
|> addToHash "page" (DisplayPage.fromPage webLog page)
|
|> addToHash "page" (DisplayPage.FromPage webLog page)
|
||||||
|> addToHash ViewContext.IsPage true
|
|> addToHash ViewContext.IsPage true
|
||||||
|> themedView (defaultArg page.Template "single-page") next ctx
|
|> themedView (defaultArg page.Template "single-page") next ctx
|
||||||
| None -> ()
|
| None -> ()
|
||||||
|
Loading…
Reference in New Issue
Block a user