Add user created and last seen on (#19)

- Updated view models / interfaces per F# naming guidelines
This commit is contained in:
Daniel J. Summers 2022-07-17 23:10:30 -04:00
parent e0a03bfca9
commit 5fb3a73dcf
39 changed files with 1234 additions and 1203 deletions

View File

@ -9,269 +9,272 @@ open MyWebLog.ViewModels
type ICategoryData = type ICategoryData =
/// Add a category /// Add a category
abstract member add : Category -> Task<unit> abstract member Add : Category -> Task<unit>
/// Count all categories for the given web log /// Count all categories for the given web log
abstract member countAll : WebLogId -> Task<int> abstract member CountAll : WebLogId -> Task<int>
/// Count all top-level categories for the given web log /// Count all top-level categories for the given web log
abstract member countTopLevel : WebLogId -> Task<int> abstract member CountTopLevel : WebLogId -> Task<int>
/// Delete a category (also removes it from posts) /// Delete a category (also removes it from posts)
abstract member delete : CategoryId -> WebLogId -> Task<bool> abstract member Delete : CategoryId -> WebLogId -> Task<bool>
/// Find all categories for a web log, sorted alphabetically and grouped by hierarchy /// Find all categories for a web log, sorted alphabetically and grouped by hierarchy
abstract member findAllForView : WebLogId -> Task<DisplayCategory[]> abstract member FindAllForView : WebLogId -> Task<DisplayCategory[]>
/// Find a category by its ID /// Find a category by its ID
abstract member findById : CategoryId -> WebLogId -> Task<Category option> abstract member FindById : CategoryId -> WebLogId -> Task<Category option>
/// Find all categories for the given web log /// Find all categories for the given web log
abstract member findByWebLog : WebLogId -> Task<Category list> abstract member FindByWebLog : WebLogId -> Task<Category list>
/// Restore categories from a backup /// Restore categories from a backup
abstract member restore : Category list -> Task<unit> abstract member Restore : Category list -> Task<unit>
/// Update a category (slug, name, description, and parent ID) /// Update a category (slug, name, description, and parent ID)
abstract member update : Category -> Task<unit> abstract member Update : Category -> Task<unit>
/// Data functions to support manipulating pages /// Data functions to support manipulating pages
type IPageData = type IPageData =
/// Add a page /// Add a page
abstract member add : Page -> Task<unit> abstract member Add : Page -> Task<unit>
/// Get all pages for the web log (excluding meta items, text, revisions, and prior permalinks) /// Get all pages for the web log (excluding meta items, text, revisions, and prior permalinks)
abstract member all : WebLogId -> Task<Page list> abstract member All : WebLogId -> Task<Page list>
/// Count all pages for the given web log /// Count all pages for the given web log
abstract member countAll : WebLogId -> Task<int> abstract member CountAll : WebLogId -> Task<int>
/// Count pages marked as "show in page list" for the given web log /// Count pages marked as "show in page list" for the given web log
abstract member countListed : WebLogId -> Task<int> abstract member CountListed : WebLogId -> Task<int>
/// Delete a page /// Delete a page
abstract member delete : PageId -> WebLogId -> Task<bool> abstract member Delete : PageId -> WebLogId -> Task<bool>
/// Find a page by its ID (excluding revisions and prior permalinks) /// Find a page by its ID (excluding revisions and prior permalinks)
abstract member findById : PageId -> WebLogId -> Task<Page option> abstract member FindById : PageId -> WebLogId -> Task<Page option>
/// Find a page by its permalink (excluding revisions and prior permalinks) /// Find a page by its permalink (excluding revisions and prior permalinks)
abstract member findByPermalink : Permalink -> WebLogId -> Task<Page option> abstract member FindByPermalink : Permalink -> WebLogId -> Task<Page option>
/// Find the current permalink for a page from a list of prior permalinks /// Find the current permalink for a page from a list of prior permalinks
abstract member findCurrentPermalink : Permalink list -> WebLogId -> Task<Permalink option> abstract member FindCurrentPermalink : Permalink list -> WebLogId -> Task<Permalink option>
/// Find a page by its ID (including revisions and prior permalinks) /// Find a page by its ID (including revisions and prior permalinks)
abstract member findFullById : PageId -> WebLogId -> Task<Page option> abstract member FindFullById : PageId -> WebLogId -> Task<Page option>
/// Find all pages for the given web log (including revisions and prior permalinks) /// Find all pages for the given web log (including revisions and prior permalinks)
abstract member findFullByWebLog : WebLogId -> Task<Page list> abstract member FindFullByWebLog : WebLogId -> Task<Page list>
/// Find pages marked as "show in page list" for the given web log (excluding text, revisions, and prior permalinks) /// Find pages marked as "show in page list" for the given web log (excluding text, revisions, and prior permalinks)
abstract member findListed : WebLogId -> Task<Page list> abstract member FindListed : WebLogId -> Task<Page list>
/// Find a page of pages (displayed in admin section) (excluding meta items, revisions and prior permalinks) /// Find a page of pages (displayed in admin section) (excluding meta items, revisions and prior permalinks)
abstract member findPageOfPages : WebLogId -> pageNbr : int -> Task<Page list> abstract member FindPageOfPages : WebLogId -> pageNbr : int -> Task<Page list>
/// Restore pages from a backup /// Restore pages from a backup
abstract member restore : Page list -> Task<unit> abstract member Restore : Page list -> Task<unit>
/// Update a page /// Update a page
abstract member update : Page -> Task<unit> abstract member Update : Page -> Task<unit>
/// Update the prior permalinks for the given page /// Update the prior permalinks for the given page
abstract member updatePriorPermalinks : PageId -> WebLogId -> Permalink list -> Task<bool> abstract member UpdatePriorPermalinks : PageId -> WebLogId -> Permalink list -> Task<bool>
/// Data functions to support manipulating posts /// Data functions to support manipulating posts
type IPostData = type IPostData =
/// Add a post /// Add a post
abstract member add : Post -> Task<unit> abstract member Add : Post -> Task<unit>
/// Count posts by their status /// Count posts by their status
abstract member countByStatus : PostStatus -> WebLogId -> Task<int> abstract member CountByStatus : PostStatus -> WebLogId -> Task<int>
/// Delete a post /// Delete a post
abstract member delete : PostId -> WebLogId -> Task<bool> abstract member Delete : PostId -> WebLogId -> Task<bool>
/// Find a post by its ID (excluding revisions and prior permalinks) /// Find a post by its ID (excluding revisions and prior permalinks)
abstract member findById : PostId -> WebLogId -> Task<Post option> abstract member FindById : PostId -> WebLogId -> Task<Post option>
/// Find a post by its permalink (excluding revisions and prior permalinks) /// Find a post by its permalink (excluding revisions and prior permalinks)
abstract member findByPermalink : Permalink -> WebLogId -> Task<Post option> abstract member FindByPermalink : Permalink -> WebLogId -> Task<Post option>
/// Find the current permalink for a post from a list of prior permalinks /// Find the current permalink for a post from a list of prior permalinks
abstract member findCurrentPermalink : Permalink list -> WebLogId -> Task<Permalink option> abstract member FindCurrentPermalink : Permalink list -> WebLogId -> Task<Permalink option>
/// Find a post by its ID (including revisions and prior permalinks) /// Find a post by its ID (including revisions and prior permalinks)
abstract member findFullById : PostId -> WebLogId -> Task<Post option> abstract member FindFullById : PostId -> WebLogId -> Task<Post option>
/// Find all posts for the given web log (including revisions and prior permalinks) /// Find all posts for the given web log (including revisions and prior permalinks)
abstract member findFullByWebLog : WebLogId -> Task<Post list> abstract member FindFullByWebLog : WebLogId -> Task<Post list>
/// Find posts to be displayed on a category list page (excluding revisions and prior permalinks) /// Find posts to be displayed on a category list page (excluding revisions and prior permalinks)
abstract member findPageOfCategorizedPosts : abstract member FindPageOfCategorizedPosts :
WebLogId -> CategoryId list -> pageNbr : int -> postsPerPage : int -> Task<Post list> WebLogId -> CategoryId list -> pageNbr : int -> postsPerPage : int -> Task<Post list>
/// Find posts to be displayed on an admin page (excluding revisions and prior permalinks) /// Find posts to be displayed on an admin page (excluding revisions and prior permalinks)
abstract member findPageOfPosts : WebLogId -> pageNbr : int -> postsPerPage : int -> Task<Post list> abstract member FindPageOfPosts : WebLogId -> pageNbr : int -> postsPerPage : int -> Task<Post list>
/// Find posts to be displayed on a page (excluding revisions and prior permalinks) /// Find posts to be displayed on a page (excluding revisions and prior permalinks)
abstract member findPageOfPublishedPosts : WebLogId -> pageNbr : int -> postsPerPage : int -> Task<Post list> abstract member FindPageOfPublishedPosts : WebLogId -> pageNbr : int -> postsPerPage : int -> Task<Post list>
/// Find posts to be displayed on a tag list page (excluding revisions and prior permalinks) /// Find posts to be displayed on a tag list page (excluding revisions and prior permalinks)
abstract member findPageOfTaggedPosts : abstract member FindPageOfTaggedPosts :
WebLogId -> tag : string -> pageNbr : int -> postsPerPage : int -> Task<Post list> WebLogId -> tag : string -> pageNbr : int -> postsPerPage : int -> Task<Post list>
/// Find the next older and newer post for the given published date/time (excluding revisions and prior permalinks) /// Find the next older and newer post for the given published date/time (excluding revisions and prior permalinks)
abstract member findSurroundingPosts : WebLogId -> publishedOn : DateTime -> Task<Post option * Post option> abstract member FindSurroundingPosts : WebLogId -> publishedOn : DateTime -> Task<Post option * Post option>
/// Restore posts from a backup /// Restore posts from a backup
abstract member restore : Post list -> Task<unit> abstract member Restore : Post list -> Task<unit>
/// Update a post /// Update a post
abstract member update : Post -> Task<unit> abstract member Update : Post -> Task<unit>
/// Update the prior permalinks for a post /// Update the prior permalinks for a post
abstract member updatePriorPermalinks : PostId -> WebLogId -> Permalink list -> Task<bool> abstract member UpdatePriorPermalinks : PostId -> WebLogId -> Permalink list -> Task<bool>
/// Functions to manipulate tag mappings /// Functions to manipulate tag mappings
type ITagMapData = type ITagMapData =
/// Delete a tag mapping /// Delete a tag mapping
abstract member delete : TagMapId -> WebLogId -> Task<bool> abstract member Delete : TagMapId -> WebLogId -> Task<bool>
/// Find a tag mapping by its ID /// Find a tag mapping by its ID
abstract member findById : TagMapId -> WebLogId -> Task<TagMap option> abstract member FindById : TagMapId -> WebLogId -> Task<TagMap option>
/// Find a tag mapping by its URL value /// Find a tag mapping by its URL value
abstract member findByUrlValue : string -> WebLogId -> Task<TagMap option> abstract member FindByUrlValue : string -> WebLogId -> Task<TagMap option>
/// Retrieve all tag mappings for the given web log /// Retrieve all tag mappings for the given web log
abstract member findByWebLog : WebLogId -> Task<TagMap list> abstract member FindByWebLog : WebLogId -> Task<TagMap list>
/// Find tag mappings for the given tags /// Find tag mappings for the given tags
abstract member findMappingForTags : tags : string list -> WebLogId -> Task<TagMap list> abstract member FindMappingForTags : tags : string list -> WebLogId -> Task<TagMap list>
/// Restore tag mappings from a backup /// Restore tag mappings from a backup
abstract member restore : TagMap list -> Task<unit> abstract member Restore : TagMap list -> Task<unit>
/// Save a tag mapping (insert or update) /// Save a tag mapping (insert or update)
abstract member save : TagMap -> Task<unit> abstract member Save : TagMap -> Task<unit>
/// Functions to manipulate themes /// Functions to manipulate themes
type IThemeData = type IThemeData =
/// Retrieve all themes (except "admin") /// Retrieve all themes (except "admin")
abstract member all : unit -> Task<Theme list> abstract member All : unit -> Task<Theme list>
/// Find a theme by its ID /// Find a theme by its ID
abstract member findById : ThemeId -> Task<Theme option> abstract member FindById : ThemeId -> Task<Theme option>
/// Find a theme by its ID (excluding the text of its templates) /// Find a theme by its ID (excluding the text of its templates)
abstract member findByIdWithoutText : ThemeId -> Task<Theme option> abstract member FindByIdWithoutText : ThemeId -> Task<Theme option>
/// Save a theme (insert or update) /// Save a theme (insert or update)
abstract member save : Theme -> Task<unit> abstract member Save : Theme -> Task<unit>
/// Functions to manipulate theme assets /// Functions to manipulate theme assets
type IThemeAssetData = type IThemeAssetData =
/// Retrieve all theme assets (excluding data) /// Retrieve all theme assets (excluding data)
abstract member all : unit -> Task<ThemeAsset list> abstract member All : unit -> Task<ThemeAsset list>
/// Delete all theme assets for the given theme /// Delete all theme assets for the given theme
abstract member deleteByTheme : ThemeId -> Task<unit> abstract member DeleteByTheme : ThemeId -> Task<unit>
/// Find a theme asset by its ID /// Find a theme asset by its ID
abstract member findById : ThemeAssetId -> Task<ThemeAsset option> abstract member FindById : ThemeAssetId -> Task<ThemeAsset option>
/// Find all assets for the given theme (excludes data) /// Find all assets for the given theme (excludes data)
abstract member findByTheme : ThemeId -> Task<ThemeAsset list> abstract member FindByTheme : ThemeId -> Task<ThemeAsset list>
/// Find all assets for the given theme (includes data) /// Find all assets for the given theme (includes data)
abstract member findByThemeWithData : ThemeId -> Task<ThemeAsset list> abstract member FindByThemeWithData : ThemeId -> Task<ThemeAsset list>
/// Save a theme asset (insert or update) /// Save a theme asset (insert or update)
abstract member save : ThemeAsset -> Task<unit> abstract member Save : ThemeAsset -> Task<unit>
/// Functions to manipulate uploaded files /// Functions to manipulate uploaded files
type IUploadData = type IUploadData =
/// Add an uploaded file /// Add an uploaded file
abstract member add : Upload -> Task<unit> abstract member Add : Upload -> Task<unit>
/// Delete an uploaded file /// Delete an uploaded file
abstract member delete : UploadId -> WebLogId -> Task<Result<string, string>> abstract member Delete : UploadId -> WebLogId -> Task<Result<string, string>>
/// Find an uploaded file by its path for the given web log /// Find an uploaded file by its path for the given web log
abstract member findByPath : string -> WebLogId -> Task<Upload option> abstract member FindByPath : string -> WebLogId -> Task<Upload option>
/// Find all uploaded files for a web log (excludes data) /// Find all uploaded files for a web log (excludes data)
abstract member findByWebLog : WebLogId -> Task<Upload list> abstract member FindByWebLog : WebLogId -> Task<Upload list>
/// Find all uploaded files for a web log /// Find all uploaded files for a web log
abstract member findByWebLogWithData : WebLogId -> Task<Upload list> abstract member FindByWebLogWithData : WebLogId -> Task<Upload list>
/// Restore uploaded files from a backup /// Restore uploaded files from a backup
abstract member restore : Upload list -> Task<unit> abstract member Restore : Upload list -> Task<unit>
/// Functions to manipulate web logs /// Functions to manipulate web logs
type IWebLogData = type IWebLogData =
/// Add a web log /// Add a web log
abstract member add : WebLog -> Task<unit> abstract member Add : WebLog -> Task<unit>
/// Retrieve all web logs /// Retrieve all web logs
abstract member all : unit -> Task<WebLog list> abstract member All : unit -> Task<WebLog list>
/// Delete a web log, including categories, tag mappings, posts/comments, and pages /// Delete a web log, including categories, tag mappings, posts/comments, and pages
abstract member delete : WebLogId -> Task<unit> abstract member Delete : WebLogId -> Task<unit>
/// Find a web log by its host (URL base) /// Find a web log by its host (URL base)
abstract member findByHost : string -> Task<WebLog option> abstract member FindByHost : string -> Task<WebLog option>
/// Find a web log by its ID /// Find a web log by its ID
abstract member findById : WebLogId -> Task<WebLog option> abstract member FindById : WebLogId -> Task<WebLog option>
/// Update RSS options for a web log /// Update RSS options for a web log
abstract member updateRssOptions : WebLog -> Task<unit> abstract member UpdateRssOptions : WebLog -> Task<unit>
/// Update web log settings (from the settings page) /// Update web log settings (from the settings page)
abstract member updateSettings : WebLog -> Task<unit> abstract member UpdateSettings : WebLog -> Task<unit>
/// Functions to manipulate web log users /// Functions to manipulate web log users
type IWebLogUserData = type IWebLogUserData =
/// Add a web log user /// Add a web log user
abstract member add : WebLogUser -> Task<unit> abstract member Add : WebLogUser -> Task<unit>
/// Find a web log user by their e-mail address /// Find a web log user by their e-mail address
abstract member findByEmail : email : string -> WebLogId -> Task<WebLogUser option> abstract member FindByEmail : email : string -> WebLogId -> Task<WebLogUser option>
/// Find a web log user by their ID /// Find a web log user by their ID
abstract member findById : WebLogUserId -> WebLogId -> Task<WebLogUser option> abstract member FindById : WebLogUserId -> WebLogId -> Task<WebLogUser option>
/// Find all web log users for the given web log /// Find all web log users for the given web log
abstract member findByWebLog : WebLogId -> Task<WebLogUser list> abstract member FindByWebLog : WebLogId -> Task<WebLogUser list>
/// Get a user ID -> name dictionary for the given user IDs /// Get a user ID -> name dictionary for the given user IDs
abstract member findNames : WebLogId -> WebLogUserId list -> Task<MetaItem list> abstract member FindNames : WebLogId -> WebLogUserId list -> Task<MetaItem list>
/// Restore users from a backup /// Restore users from a backup
abstract member restore : WebLogUser list -> Task<unit> abstract member Restore : WebLogUser list -> Task<unit>
/// Set a user's last seen date/time to now
abstract member SetLastSeen : WebLogUserId -> WebLogId -> Task<unit>
/// Update a web log user /// Update a web log user
abstract member update : WebLogUser -> Task<unit> abstract member Update : WebLogUser -> Task<unit>
/// Data interface required for a myWebLog data implementation /// Data interface required for a myWebLog data implementation
@ -305,5 +308,5 @@ type IData =
abstract member WebLogUser : IWebLogUserData abstract member WebLogUser : IWebLogUserData
/// Do any required start up data checks /// Do any required start up data checks
abstract member startUp : unit -> Task<unit> abstract member StartUp : unit -> Task<unit>

View File

@ -66,6 +66,7 @@ module private RethinkHelpers =
let objList<'T> (objects : 'T list) = objects |> List.map (fun it -> it :> obj) let objList<'T> (objects : 'T list) = objects |> List.map (fun it -> it :> obj)
open System
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog.ViewModels open MyWebLog.ViewModels
open RethinkDb.Driver.FSharp open RethinkDb.Driver.FSharp
@ -158,20 +159,20 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.Category = { member _.Category = {
new ICategoryData with new ICategoryData with
member _.add cat = rethink { member _.Add cat = rethink {
withTable Table.Category withTable Table.Category
insert cat insert cat
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member _.countAll webLogId = rethink<int> { member _.CountAll webLogId = rethink<int> {
withTable Table.Category withTable Table.Category
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
count count
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.countTopLevel webLogId = rethink<int> { member _.CountTopLevel webLogId = rethink<int> {
withTable Table.Category withTable Table.Category
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
filter "parentId" None filter "parentId" None
@ -179,7 +180,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findAllForView webLogId = backgroundTask { member _.FindAllForView webLogId = backgroundTask {
let! cats = rethink<Category list> { let! cats = rethink<Category list> {
withTable Table.Category withTable Table.Category
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
@ -193,9 +194,9 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
// Parent category post counts include posts in subcategories // Parent category post counts include posts in subcategories
let catIds = let catIds =
ordered ordered
|> Seq.filter (fun cat -> cat.parentNames |> Array.contains it.name) |> Seq.filter (fun cat -> cat.ParentNames |> Array.contains it.Name)
|> Seq.map (fun cat -> cat.id :> obj) |> Seq.map (fun cat -> cat.Id :> obj)
|> Seq.append (Seq.singleton it.id) |> Seq.append (Seq.singleton it.Id)
|> List.ofSeq |> List.ofSeq
let! count = rethink<int> { let! count = rethink<int> {
withTable Table.Post withTable Table.Post
@ -205,22 +206,22 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
count count
result; withRetryDefault conn result; withRetryDefault conn
} }
return it.id, count return it.Id, count
}) })
|> Task.WhenAll |> Task.WhenAll
return return
ordered ordered
|> Seq.map (fun cat -> |> Seq.map (fun cat ->
{ cat with { cat with
postCount = counts PostCount = counts
|> Array.tryFind (fun c -> fst c = cat.id) |> Array.tryFind (fun c -> fst c = cat.Id)
|> Option.map snd |> Option.map snd
|> Option.defaultValue 0 |> Option.defaultValue 0
}) })
|> Array.ofSeq |> Array.ofSeq
} }
member _.findById catId webLogId = member _.FindById catId webLogId =
rethink<Category> { rethink<Category> {
withTable Table.Category withTable Table.Category
get catId get catId
@ -228,14 +229,14 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> verifyWebLog webLogId (fun c -> c.webLogId) <| conn |> verifyWebLog webLogId (fun c -> c.webLogId) <| conn
member _.findByWebLog webLogId = rethink<Category list> { member _.FindByWebLog webLogId = rethink<Category list> {
withTable Table.Category withTable Table.Category
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
result; withRetryDefault conn result; withRetryDefault conn
} }
member this.delete catId webLogId = backgroundTask { member this.Delete catId webLogId = backgroundTask {
match! this.findById catId webLogId with match! this.FindById catId webLogId with
| Some _ -> | Some _ ->
// Delete the category off all posts where it is assigned // Delete the category off all posts where it is assigned
do! rethink { do! rethink {
@ -256,7 +257,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
| None -> return false | None -> return false
} }
member _.restore cats = backgroundTask { member _.Restore cats = backgroundTask {
for batch in cats |> List.chunkBySize restoreBatchSize do for batch in cats |> List.chunkBySize restoreBatchSize do
do! rethink { do! rethink {
withTable Table.Category withTable Table.Category
@ -265,7 +266,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
} }
member _.update cat = rethink { member _.Update cat = rethink {
withTable Table.Category withTable Table.Category
get cat.id get cat.id
update [ "name", cat.name :> obj update [ "name", cat.name :> obj
@ -280,13 +281,13 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.Page = { member _.Page = {
new IPageData with new IPageData with
member _.add page = rethink { member _.Add page = rethink {
withTable Table.Page withTable Table.Page
insert page insert page
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member _.all webLogId = rethink<Page list> { member _.All webLogId = rethink<Page list> {
withTable Table.Page withTable Table.Page
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
without [ "text"; "metadata"; "revisions"; "priorPermalinks" ] without [ "text"; "metadata"; "revisions"; "priorPermalinks" ]
@ -294,14 +295,14 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.countAll webLogId = rethink<int> { member _.CountAll webLogId = rethink<int> {
withTable Table.Page withTable Table.Page
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
count count
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.countListed webLogId = rethink<int> { member _.CountListed webLogId = rethink<int> {
withTable Table.Page withTable Table.Page
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
filter "showInPageList" true filter "showInPageList" true
@ -309,7 +310,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.delete pageId webLogId = backgroundTask { member _.Delete pageId webLogId = backgroundTask {
let! result = rethink<Model.Result> { let! result = rethink<Model.Result> {
withTable Table.Page withTable Table.Page
getAll [ pageId ] getAll [ pageId ]
@ -320,7 +321,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
return result.Deleted > 0UL return result.Deleted > 0UL
} }
member _.findById pageId webLogId = member _.FindById pageId webLogId =
rethink<Page> { rethink<Page> {
withTable Table.Page withTable Table.Page
get pageId get pageId
@ -329,7 +330,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> verifyWebLog webLogId (fun it -> it.webLogId) <| conn |> verifyWebLog webLogId (fun it -> it.webLogId) <| conn
member _.findByPermalink permalink webLogId = member _.FindByPermalink permalink webLogId =
rethink<Page list> { rethink<Page list> {
withTable Table.Page withTable Table.Page
getAll [ r.Array (webLogId, permalink) ] (nameof permalink) getAll [ r.Array (webLogId, permalink) ] (nameof permalink)
@ -339,7 +340,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> tryFirst <| conn |> tryFirst <| conn
member _.findCurrentPermalink permalinks webLogId = backgroundTask { member _.FindCurrentPermalink permalinks webLogId = backgroundTask {
let! result = let! result =
(rethink<Page list> { (rethink<Page list> {
withTable Table.Page withTable Table.Page
@ -353,7 +354,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
return result |> Option.map (fun pg -> pg.permalink) return result |> Option.map (fun pg -> pg.permalink)
} }
member _.findFullById pageId webLogId = member _.FindFullById pageId webLogId =
rethink<Page> { rethink<Page> {
withTable Table.Page withTable Table.Page
get pageId get pageId
@ -361,13 +362,13 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> verifyWebLog webLogId (fun it -> it.webLogId) <| conn |> verifyWebLog webLogId (fun it -> it.webLogId) <| conn
member _.findFullByWebLog webLogId = rethink<Page> { member _.FindFullByWebLog webLogId = rethink<Page> {
withTable Table.Page withTable Table.Page
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
resultCursor; withRetryCursorDefault; toList conn resultCursor; withRetryCursorDefault; toList conn
} }
member _.findListed webLogId = rethink<Page list> { member _.FindListed webLogId = rethink<Page list> {
withTable Table.Page withTable Table.Page
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
filter [ "showInPageList", true :> obj ] filter [ "showInPageList", true :> obj ]
@ -376,7 +377,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findPageOfPages webLogId pageNbr = rethink<Page list> { member _.FindPageOfPages webLogId pageNbr = rethink<Page list> {
withTable Table.Page withTable Table.Page
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
without [ "metadata"; "priorPermalinks"; "revisions" ] without [ "metadata"; "priorPermalinks"; "revisions" ]
@ -386,7 +387,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.restore pages = backgroundTask { member _.Restore pages = backgroundTask {
for batch in pages |> List.chunkBySize restoreBatchSize do for batch in pages |> List.chunkBySize restoreBatchSize do
do! rethink { do! rethink {
withTable Table.Page withTable Table.Page
@ -395,7 +396,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
} }
member _.update page = rethink { member _.Update page = rethink {
withTable Table.Page withTable Table.Page
get page.id get page.id
update [ update [
@ -412,8 +413,8 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member this.updatePriorPermalinks pageId webLogId permalinks = backgroundTask { member this.UpdatePriorPermalinks pageId webLogId permalinks = backgroundTask {
match! this.findById pageId webLogId with match! this.FindById pageId webLogId with
| Some _ -> | Some _ ->
do! rethink { do! rethink {
withTable Table.Page withTable Table.Page
@ -429,13 +430,13 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.Post = { member _.Post = {
new IPostData with new IPostData with
member _.add post = rethink { member _.Add post = rethink {
withTable Table.Post withTable Table.Post
insert post insert post
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member _.countByStatus status webLogId = rethink<int> { member _.CountByStatus status webLogId = rethink<int> {
withTable Table.Post withTable Table.Post
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
filter "status" status filter "status" status
@ -443,7 +444,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.delete postId webLogId = backgroundTask { member _.Delete postId webLogId = backgroundTask {
let! result = rethink<Model.Result> { let! result = rethink<Model.Result> {
withTable Table.Post withTable Table.Post
getAll [ postId ] getAll [ postId ]
@ -454,7 +455,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
return result.Deleted > 0UL return result.Deleted > 0UL
} }
member _.findById postId webLogId = member _.FindById postId webLogId =
rethink<Post> { rethink<Post> {
withTable Table.Post withTable Table.Post
get postId get postId
@ -463,7 +464,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> verifyWebLog webLogId (fun p -> p.webLogId) <| conn |> verifyWebLog webLogId (fun p -> p.webLogId) <| conn
member _.findByPermalink permalink webLogId = member _.FindByPermalink permalink webLogId =
rethink<Post list> { rethink<Post list> {
withTable Table.Post withTable Table.Post
getAll [ r.Array (webLogId, permalink) ] (nameof permalink) getAll [ r.Array (webLogId, permalink) ] (nameof permalink)
@ -473,7 +474,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> tryFirst <| conn |> tryFirst <| conn
member _.findFullById postId webLogId = member _.FindFullById postId webLogId =
rethink<Post> { rethink<Post> {
withTable Table.Post withTable Table.Post
get postId get postId
@ -481,7 +482,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> verifyWebLog webLogId (fun p -> p.webLogId) <| conn |> verifyWebLog webLogId (fun p -> p.webLogId) <| conn
member _.findCurrentPermalink permalinks webLogId = backgroundTask { member _.FindCurrentPermalink permalinks webLogId = backgroundTask {
let! result = let! result =
(rethink<Post list> { (rethink<Post list> {
withTable Table.Post withTable Table.Post
@ -495,13 +496,13 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
return result |> Option.map (fun post -> post.permalink) return result |> Option.map (fun post -> post.permalink)
} }
member _.findFullByWebLog webLogId = rethink<Post> { member _.FindFullByWebLog webLogId = rethink<Post> {
withTable Table.Post withTable Table.Post
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
resultCursor; withRetryCursorDefault; toList conn resultCursor; withRetryCursorDefault; toList conn
} }
member _.findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = rethink<Post list> { member _.FindPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = rethink<Post list> {
withTable Table.Post withTable Table.Post
getAll (objList categoryIds) "categoryIds" getAll (objList categoryIds) "categoryIds"
filter "webLogId" webLogId filter "webLogId" webLogId
@ -514,7 +515,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findPageOfPosts webLogId pageNbr postsPerPage = rethink<Post list> { member _.FindPageOfPosts webLogId pageNbr postsPerPage = rethink<Post list> {
withTable Table.Post withTable Table.Post
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
without [ "priorPermalinks"; "revisions" ] without [ "priorPermalinks"; "revisions" ]
@ -524,7 +525,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findPageOfPublishedPosts webLogId pageNbr postsPerPage = rethink<Post list> { member _.FindPageOfPublishedPosts webLogId pageNbr postsPerPage = rethink<Post list> {
withTable Table.Post withTable Table.Post
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
filter "status" Published filter "status" Published
@ -535,7 +536,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findPageOfTaggedPosts webLogId tag pageNbr postsPerPage = rethink<Post list> { member _.FindPageOfTaggedPosts webLogId tag pageNbr postsPerPage = rethink<Post list> {
withTable Table.Post withTable Table.Post
getAll [ tag ] "tags" getAll [ tag ] "tags"
filter "webLogId" webLogId filter "webLogId" webLogId
@ -547,7 +548,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findSurroundingPosts webLogId publishedOn = backgroundTask { member _.FindSurroundingPosts webLogId publishedOn = backgroundTask {
let! older = let! older =
rethink<Post list> { rethink<Post list> {
withTable Table.Post withTable Table.Post
@ -573,7 +574,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
return older, newer return older, newer
} }
member _.restore pages = backgroundTask { member _.Restore pages = backgroundTask {
for batch in pages |> List.chunkBySize restoreBatchSize do for batch in pages |> List.chunkBySize restoreBatchSize do
do! rethink { do! rethink {
withTable Table.Post withTable Table.Post
@ -582,14 +583,14 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
} }
member _.update post = rethink { member _.Update post = rethink {
withTable Table.Post withTable Table.Post
get post.id get post.id
replace post replace post
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member _.updatePriorPermalinks postId webLogId permalinks = backgroundTask { member _.UpdatePriorPermalinks postId webLogId permalinks = backgroundTask {
match! ( match! (
rethink<Post> { rethink<Post> {
withTable Table.Post withTable Table.Post
@ -613,7 +614,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.TagMap = { member _.TagMap = {
new ITagMapData with new ITagMapData with
member _.delete tagMapId webLogId = backgroundTask { member _.Delete tagMapId webLogId = backgroundTask {
let! result = rethink<Model.Result> { let! result = rethink<Model.Result> {
withTable Table.TagMap withTable Table.TagMap
getAll [ tagMapId ] getAll [ tagMapId ]
@ -624,7 +625,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
return result.Deleted > 0UL return result.Deleted > 0UL
} }
member _.findById tagMapId webLogId = member _.FindById tagMapId webLogId =
rethink<TagMap> { rethink<TagMap> {
withTable Table.TagMap withTable Table.TagMap
get tagMapId get tagMapId
@ -632,7 +633,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> verifyWebLog webLogId (fun tm -> tm.webLogId) <| conn |> verifyWebLog webLogId (fun tm -> tm.webLogId) <| conn
member _.findByUrlValue urlValue webLogId = member _.FindByUrlValue urlValue webLogId =
rethink<TagMap list> { rethink<TagMap list> {
withTable Table.TagMap withTable Table.TagMap
getAll [ r.Array (webLogId, urlValue) ] "webLogAndUrl" getAll [ r.Array (webLogId, urlValue) ] "webLogAndUrl"
@ -641,20 +642,20 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> tryFirst <| conn |> tryFirst <| conn
member _.findByWebLog webLogId = rethink<TagMap list> { member _.FindByWebLog webLogId = rethink<TagMap list> {
withTable Table.TagMap withTable Table.TagMap
between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ())) [ Index "webLogAndTag" ] between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ())) [ Index "webLogAndTag" ]
orderBy "tag" orderBy "tag"
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findMappingForTags tags webLogId = rethink<TagMap list> { member _.FindMappingForTags tags webLogId = rethink<TagMap list> {
withTable Table.TagMap withTable Table.TagMap
getAll (tags |> List.map (fun tag -> r.Array (webLogId, tag) :> obj)) "webLogAndTag" getAll (tags |> List.map (fun tag -> r.Array (webLogId, tag) :> obj)) "webLogAndTag"
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.restore tagMaps = backgroundTask { member _.Restore tagMaps = backgroundTask {
for batch in tagMaps |> List.chunkBySize restoreBatchSize do for batch in tagMaps |> List.chunkBySize restoreBatchSize do
do! rethink { do! rethink {
withTable Table.TagMap withTable Table.TagMap
@ -663,7 +664,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
} }
member _.save tagMap = rethink { member _.Save tagMap = rethink {
withTable Table.TagMap withTable Table.TagMap
get tagMap.id get tagMap.id
replace tagMap replace tagMap
@ -674,7 +675,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.Theme = { member _.Theme = {
new IThemeData with new IThemeData with
member _.all () = rethink<Theme list> { member _.All () = rethink<Theme list> {
withTable Table.Theme withTable Table.Theme
filter (fun row -> row["id"].Ne "admin" :> obj) filter (fun row -> row["id"].Ne "admin" :> obj)
without [ "templates" ] without [ "templates" ]
@ -682,20 +683,20 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findById themeId = rethink<Theme> { member _.FindById themeId = rethink<Theme> {
withTable Table.Theme withTable Table.Theme
get themeId get themeId
resultOption; withRetryOptionDefault conn resultOption; withRetryOptionDefault conn
} }
member _.findByIdWithoutText themeId = rethink<Theme> { member _.FindByIdWithoutText themeId = rethink<Theme> {
withTable Table.Theme withTable Table.Theme
get themeId get themeId
merge (fun row -> r.HashMap ("templates", row["templates"].Without [| "text" |])) merge (fun row -> r.HashMap ("templates", row["templates"].Without [| "text" |]))
resultOption; withRetryOptionDefault conn resultOption; withRetryOptionDefault conn
} }
member _.save theme = rethink { member _.Save theme = rethink {
withTable Table.Theme withTable Table.Theme
get theme.id get theme.id
replace theme replace theme
@ -706,39 +707,39 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.ThemeAsset = { member _.ThemeAsset = {
new IThemeAssetData with new IThemeAssetData with
member _.all () = rethink<ThemeAsset list> { member _.All () = rethink<ThemeAsset list> {
withTable Table.ThemeAsset withTable Table.ThemeAsset
without [ "data" ] without [ "data" ]
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.deleteByTheme themeId = rethink { member _.DeleteByTheme themeId = rethink {
withTable Table.ThemeAsset withTable Table.ThemeAsset
filter (matchAssetByThemeId themeId) filter (matchAssetByThemeId themeId)
delete delete
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member _.findById assetId = rethink<ThemeAsset> { member _.FindById assetId = rethink<ThemeAsset> {
withTable Table.ThemeAsset withTable Table.ThemeAsset
get assetId get assetId
resultOption; withRetryOptionDefault conn resultOption; withRetryOptionDefault conn
} }
member _.findByTheme themeId = rethink<ThemeAsset list> { member _.FindByTheme themeId = rethink<ThemeAsset list> {
withTable Table.ThemeAsset withTable Table.ThemeAsset
filter (matchAssetByThemeId themeId) filter (matchAssetByThemeId themeId)
without [ "data" ] without [ "data" ]
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findByThemeWithData themeId = rethink<ThemeAsset> { member _.FindByThemeWithData themeId = rethink<ThemeAsset> {
withTable Table.ThemeAsset withTable Table.ThemeAsset
filter (matchAssetByThemeId themeId) filter (matchAssetByThemeId themeId)
resultCursor; withRetryCursorDefault; toList conn resultCursor; withRetryCursorDefault; toList conn
} }
member _.save asset = rethink { member _.Save asset = rethink {
withTable Table.ThemeAsset withTable Table.ThemeAsset
get asset.id get asset.id
replace asset replace asset
@ -749,13 +750,13 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.Upload = { member _.Upload = {
new IUploadData with new IUploadData with
member _.add upload = rethink { member _.Add upload = rethink {
withTable Table.Upload withTable Table.Upload
insert upload insert upload
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member _.delete uploadId webLogId = backgroundTask { member _.Delete uploadId webLogId = backgroundTask {
let! upload = let! upload =
rethink<Upload> { rethink<Upload> {
withTable Table.Upload withTable Table.Upload
@ -775,7 +776,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
| None -> return Result.Error $"Upload ID {UploadId.toString uploadId} not found" | None -> return Result.Error $"Upload ID {UploadId.toString uploadId} not found"
} }
member _.findByPath path webLogId = member _.FindByPath path webLogId =
rethink<Upload> { rethink<Upload> {
withTable Table.Upload withTable Table.Upload
getAll [ r.Array (webLogId, path) ] "webLogAndPath" getAll [ r.Array (webLogId, path) ] "webLogAndPath"
@ -783,7 +784,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> tryFirst <| conn |> tryFirst <| conn
member _.findByWebLog webLogId = rethink<Upload> { member _.FindByWebLog webLogId = rethink<Upload> {
withTable Table.Upload withTable Table.Upload
between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ())) between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ()))
[ Index "webLogAndPath" ] [ Index "webLogAndPath" ]
@ -791,14 +792,14 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
resultCursor; withRetryCursorDefault; toList conn resultCursor; withRetryCursorDefault; toList conn
} }
member _.findByWebLogWithData webLogId = rethink<Upload> { member _.FindByWebLogWithData webLogId = rethink<Upload> {
withTable Table.Upload withTable Table.Upload
between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ())) between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ()))
[ Index "webLogAndPath" ] [ Index "webLogAndPath" ]
resultCursor; withRetryCursorDefault; toList conn resultCursor; withRetryCursorDefault; toList conn
} }
member _.restore uploads = backgroundTask { member _.Restore uploads = backgroundTask {
// Files can be large; we'll do 5 at a time // Files can be large; we'll do 5 at a time
for batch in uploads |> List.chunkBySize 5 do for batch in uploads |> List.chunkBySize 5 do
do! rethink { do! rethink {
@ -812,18 +813,18 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.WebLog = { member _.WebLog = {
new IWebLogData with new IWebLogData with
member _.add webLog = rethink { member _.Add webLog = rethink {
withTable Table.WebLog withTable Table.WebLog
insert webLog insert webLog
write; withRetryOnce; ignoreResult conn write; withRetryOnce; ignoreResult conn
} }
member _.all () = rethink<WebLog list> { member _.All () = rethink<WebLog list> {
withTable Table.WebLog withTable Table.WebLog
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.delete webLogId = backgroundTask { member _.Delete webLogId = backgroundTask {
// Comments should be deleted by post IDs // Comments should be deleted by post IDs
let! thePostIds = rethink<{| id : string |} list> { let! thePostIds = rethink<{| id : string |} list> {
withTable Table.Post withTable Table.Post
@ -870,7 +871,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
} }
member _.findByHost url = member _.FindByHost url =
rethink<WebLog list> { rethink<WebLog list> {
withTable Table.WebLog withTable Table.WebLog
getAll [ url ] "urlBase" getAll [ url ] "urlBase"
@ -879,20 +880,20 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> tryFirst <| conn |> tryFirst <| conn
member _.findById webLogId = rethink<WebLog> { member _.FindById webLogId = rethink<WebLog> {
withTable Table.WebLog withTable Table.WebLog
get webLogId get webLogId
resultOption; withRetryOptionDefault conn resultOption; withRetryOptionDefault conn
} }
member _.updateRssOptions webLog = rethink { member _.UpdateRssOptions webLog = rethink {
withTable Table.WebLog withTable Table.WebLog
get webLog.id get webLog.id
update [ "rss", webLog.rss :> obj ] update [ "rss", webLog.rss :> obj ]
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member _.updateSettings webLog = rethink { member _.UpdateSettings webLog = rethink {
withTable Table.WebLog withTable Table.WebLog
get webLog.id get webLog.id
update [ update [
@ -913,13 +914,13 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.WebLogUser = { member _.WebLogUser = {
new IWebLogUserData with new IWebLogUserData with
member _.add user = rethink { member _.Add user = rethink {
withTable Table.WebLogUser withTable Table.WebLogUser
insert user insert user
write; withRetryDefault; ignoreResult conn write; withRetryDefault; ignoreResult conn
} }
member _.findByEmail email webLogId = member _.FindByEmail email webLogId =
rethink<WebLogUser list> { rethink<WebLogUser list> {
withTable Table.WebLogUser withTable Table.WebLogUser
getAll [ r.Array (webLogId, email) ] "logOn" getAll [ r.Array (webLogId, email) ] "logOn"
@ -928,7 +929,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> tryFirst <| conn |> tryFirst <| conn
member _.findById userId webLogId = member _.FindById userId webLogId =
rethink<WebLogUser> { rethink<WebLogUser> {
withTable Table.WebLogUser withTable Table.WebLogUser
get userId get userId
@ -936,13 +937,13 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
|> verifyWebLog webLogId (fun u -> u.webLogId) <| conn |> verifyWebLog webLogId (fun u -> u.webLogId) <| conn
member _.findByWebLog webLogId = rethink<WebLogUser list> { member _.FindByWebLog webLogId = rethink<WebLogUser list> {
withTable Table.WebLogUser withTable Table.WebLogUser
getAll [ webLogId ] (nameof webLogId) getAll [ webLogId ] (nameof webLogId)
result; withRetryDefault conn result; withRetryDefault conn
} }
member _.findNames webLogId userIds = backgroundTask { member _.FindNames webLogId userIds = backgroundTask {
let! users = rethink<WebLogUser list> { let! users = rethink<WebLogUser list> {
withTable Table.WebLogUser withTable Table.WebLogUser
getAll (objList userIds) getAll (objList userIds)
@ -954,7 +955,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|> List.map (fun u -> { name = WebLogUserId.toString u.id; value = WebLogUser.displayName u }) |> List.map (fun u -> { name = WebLogUserId.toString u.id; value = WebLogUser.displayName u })
} }
member _.restore users = backgroundTask { member _.Restore users = backgroundTask {
for batch in users |> List.chunkBySize restoreBatchSize do for batch in users |> List.chunkBySize restoreBatchSize do
do! rethink { do! rethink {
withTable Table.WebLogUser withTable Table.WebLogUser
@ -963,7 +964,19 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
} }
member _.update user = rethink { member this.SetLastSeen userId webLogId = backgroundTask {
match! this.FindById userId webLogId with
| Some _ ->
do! rethink {
withTable Table.WebLogUser
get userId
update [ "lastSeenOn", DateTime.UtcNow :> obj ]
write; withRetryOnce; ignoreResult conn
}
| None -> ()
}
member _.Update user = rethink {
withTable Table.WebLogUser withTable Table.WebLogUser
get user.id get user.id
update [ update [
@ -978,7 +991,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
} }
} }
member _.startUp () = backgroundTask { member _.StartUp () = backgroundTask {
let! dbs = rethink<string list> { dbList; result; withRetryOnce conn } let! dbs = rethink<string list> { dbList; result; withRetryOnce conn }
if not (dbs |> List.contains config.Database) then if not (dbs |> List.contains config.Database) then
log.LogInformation $"Creating database {config.Database}..." log.LogInformation $"Creating database {config.Database}..."

View File

@ -301,6 +301,8 @@ module Map =
salt = getGuid "salt" rdr salt = getGuid "salt" rdr
url = tryString "url" rdr url = tryString "url" rdr
accessLevel = AccessLevel.parse (getString "access_level" rdr) accessLevel = AccessLevel.parse (getString "access_level" rdr)
createdOn = getDateTime "created_on" rdr
lastSeenOn = tryDateTime "last_seen_on" rdr
} }
/// Add a possibly-missing parameter, substituting null for None /// Add a possibly-missing parameter, substituting null for None

View File

@ -78,24 +78,24 @@ type SQLiteCategoryData (conn : SqliteConnection) =
AND p.status = 'Published' AND p.status = 'Published'
AND pc.category_id IN (""" AND pc.category_id IN ("""
ordered ordered
|> Seq.filter (fun cat -> cat.parentNames |> Array.contains it.name) |> Seq.filter (fun cat -> cat.ParentNames |> Array.contains it.Name)
|> Seq.map (fun cat -> cat.id) |> Seq.map (fun cat -> cat.Id)
|> Seq.append (Seq.singleton it.id) |> Seq.append (Seq.singleton it.Id)
|> Seq.iteri (fun idx item -> |> Seq.iteri (fun idx item ->
if idx > 0 then cmd.CommandText <- $"{cmd.CommandText}, " if idx > 0 then cmd.CommandText <- $"{cmd.CommandText}, "
cmd.CommandText <- $"{cmd.CommandText}@catId{idx}" cmd.CommandText <- $"{cmd.CommandText}@catId{idx}"
cmd.Parameters.AddWithValue ($"@catId{idx}", item) |> ignore) cmd.Parameters.AddWithValue ($"@catId{idx}", item) |> ignore)
cmd.CommandText <- $"{cmd.CommandText})" cmd.CommandText <- $"{cmd.CommandText})"
let! postCount = count cmd let! postCount = count cmd
return it.id, postCount return it.Id, postCount
}) })
|> Task.WhenAll |> Task.WhenAll
return return
ordered ordered
|> Seq.map (fun cat -> |> Seq.map (fun cat ->
{ cat with { cat with
postCount = counts PostCount = counts
|> Array.tryFind (fun c -> fst c = cat.id) |> Array.tryFind (fun c -> fst c = cat.Id)
|> Option.map snd |> Option.map snd
|> Option.defaultValue 0 |> Option.defaultValue 0
}) })
@ -163,12 +163,12 @@ type SQLiteCategoryData (conn : SqliteConnection) =
} }
interface ICategoryData with interface ICategoryData with
member _.add cat = add cat member _.Add cat = add cat
member _.countAll webLogId = countAll webLogId member _.CountAll webLogId = countAll webLogId
member _.countTopLevel webLogId = countTopLevel webLogId member _.CountTopLevel webLogId = countTopLevel webLogId
member _.findAllForView webLogId = findAllForView webLogId member _.FindAllForView webLogId = findAllForView webLogId
member _.findById catId webLogId = findById catId webLogId member _.FindById catId webLogId = findById catId webLogId
member _.findByWebLog webLogId = findByWebLog webLogId member _.FindByWebLog webLogId = findByWebLog webLogId
member _.delete catId webLogId = delete catId webLogId member _.Delete catId webLogId = delete catId webLogId
member _.restore cats = restore cats member _.Restore cats = restore cats
member _.update cat = update cat member _.Update cat = update cat

View File

@ -349,18 +349,18 @@ type SQLitePageData (conn : SqliteConnection) =
} }
interface IPageData with interface IPageData with
member _.add page = add page member _.Add page = add page
member _.all webLogId = all webLogId member _.All webLogId = all webLogId
member _.countAll webLogId = countAll webLogId member _.CountAll webLogId = countAll webLogId
member _.countListed webLogId = countListed webLogId member _.CountListed webLogId = countListed webLogId
member _.delete pageId webLogId = delete pageId webLogId member _.Delete pageId webLogId = delete pageId webLogId
member _.findById pageId webLogId = findById pageId webLogId member _.FindById pageId webLogId = findById pageId webLogId
member _.findByPermalink permalink webLogId = findByPermalink permalink webLogId member _.FindByPermalink permalink webLogId = findByPermalink permalink webLogId
member _.findCurrentPermalink permalinks webLogId = findCurrentPermalink permalinks webLogId member _.FindCurrentPermalink permalinks webLogId = findCurrentPermalink permalinks webLogId
member _.findFullById pageId webLogId = findFullById pageId webLogId member _.FindFullById pageId webLogId = findFullById pageId webLogId
member _.findFullByWebLog webLogId = findFullByWebLog webLogId member _.FindFullByWebLog webLogId = findFullByWebLog webLogId
member _.findListed webLogId = findListed webLogId member _.FindListed webLogId = findListed webLogId
member _.findPageOfPages webLogId pageNbr = findPageOfPages webLogId pageNbr member _.FindPageOfPages webLogId pageNbr = findPageOfPages webLogId pageNbr
member _.restore pages = restore pages member _.Restore pages = restore pages
member _.update page = update page member _.Update page = update page
member _.updatePriorPermalinks pageId webLogId permalinks = updatePriorPermalinks pageId webLogId permalinks member _.UpdatePriorPermalinks pageId webLogId permalinks = updatePriorPermalinks pageId webLogId permalinks

View File

@ -571,22 +571,22 @@ type SQLitePostData (conn : SqliteConnection) =
} }
interface IPostData with interface IPostData with
member _.add post = add post member _.Add post = add post
member _.countByStatus status webLogId = countByStatus status webLogId member _.CountByStatus status webLogId = countByStatus status webLogId
member _.delete postId webLogId = delete postId webLogId member _.Delete postId webLogId = delete postId webLogId
member _.findById postId webLogId = findById postId webLogId member _.FindById postId webLogId = findById postId webLogId
member _.findByPermalink permalink webLogId = findByPermalink permalink webLogId member _.FindByPermalink permalink webLogId = findByPermalink permalink webLogId
member _.findCurrentPermalink permalinks webLogId = findCurrentPermalink permalinks webLogId member _.FindCurrentPermalink permalinks webLogId = findCurrentPermalink permalinks webLogId
member _.findFullById postId webLogId = findFullById postId webLogId member _.FindFullById postId webLogId = findFullById postId webLogId
member _.findFullByWebLog webLogId = findFullByWebLog webLogId member _.FindFullByWebLog webLogId = findFullByWebLog webLogId
member _.findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = member _.FindPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage =
findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage
member _.findPageOfPosts webLogId pageNbr postsPerPage = findPageOfPosts webLogId pageNbr postsPerPage member _.FindPageOfPosts webLogId pageNbr postsPerPage = findPageOfPosts webLogId pageNbr postsPerPage
member _.findPageOfPublishedPosts webLogId pageNbr postsPerPage = member _.FindPageOfPublishedPosts webLogId pageNbr postsPerPage =
findPageOfPublishedPosts webLogId pageNbr postsPerPage findPageOfPublishedPosts webLogId pageNbr postsPerPage
member _.findPageOfTaggedPosts webLogId tag pageNbr postsPerPage = member _.FindPageOfTaggedPosts webLogId tag pageNbr postsPerPage =
findPageOfTaggedPosts webLogId tag pageNbr postsPerPage findPageOfTaggedPosts webLogId tag pageNbr postsPerPage
member _.findSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn member _.FindSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn
member _.restore posts = restore posts member _.Restore posts = restore posts
member _.update post = update post member _.Update post = update post
member _.updatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks member _.UpdatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks

View File

@ -99,10 +99,10 @@ type SQLiteTagMapData (conn : SqliteConnection) =
} }
interface ITagMapData with interface ITagMapData with
member _.delete tagMapId webLogId = delete tagMapId webLogId member _.Delete tagMapId webLogId = delete tagMapId webLogId
member _.findById tagMapId webLogId = findById tagMapId webLogId member _.FindById tagMapId webLogId = findById tagMapId webLogId
member _.findByUrlValue urlValue webLogId = findByUrlValue urlValue webLogId member _.FindByUrlValue urlValue webLogId = findByUrlValue urlValue webLogId
member _.findByWebLog webLogId = findByWebLog webLogId member _.FindByWebLog webLogId = findByWebLog webLogId
member _.findMappingForTags tags webLogId = findMappingForTags tags webLogId member _.FindMappingForTags tags webLogId = findMappingForTags tags webLogId
member _.save tagMap = save tagMap member _.Save tagMap = save tagMap
member this.restore tagMaps = restore tagMaps member this.Restore tagMaps = restore tagMaps

View File

@ -101,10 +101,10 @@ type SQLiteThemeData (conn : SqliteConnection) =
} }
interface IThemeData with interface IThemeData with
member _.all () = all () member _.All () = all ()
member _.findById themeId = findById themeId member _.FindById themeId = findById themeId
member _.findByIdWithoutText themeId = findByIdWithoutText themeId member _.FindByIdWithoutText themeId = findByIdWithoutText themeId
member _.save theme = save theme member _.Save theme = save theme
open System.IO open System.IO
@ -199,9 +199,9 @@ type SQLiteThemeAssetData (conn : SqliteConnection) =
} }
interface IThemeAssetData with interface IThemeAssetData with
member _.all () = all () member _.All () = all ()
member _.deleteByTheme themeId = deleteByTheme themeId member _.DeleteByTheme themeId = deleteByTheme themeId
member _.findById assetId = findById assetId member _.FindById assetId = findById assetId
member _.findByTheme themeId = findByTheme themeId member _.FindByTheme themeId = findByTheme themeId
member _.findByThemeWithData themeId = findByThemeWithData themeId member _.FindByThemeWithData themeId = findByThemeWithData themeId
member _.save asset = save asset member _.Save asset = save asset

View File

@ -92,10 +92,10 @@ type SQLiteUploadData (conn : SqliteConnection) =
} }
interface IUploadData with interface IUploadData with
member _.add upload = add upload member _.Add upload = add upload
member _.delete uploadId webLogId = delete uploadId webLogId member _.Delete uploadId webLogId = delete uploadId webLogId
member _.findByPath path webLogId = findByPath path webLogId member _.FindByPath path webLogId = findByPath path webLogId
member _.findByWebLog webLogId = findByWebLog webLogId member _.FindByWebLog webLogId = findByWebLog webLogId
member _.findByWebLogWithData webLogId = findByWebLogWithData webLogId member _.FindByWebLogWithData webLogId = findByWebLogWithData webLogId
member _.restore uploads = restore uploads member _.Restore uploads = restore uploads

View File

@ -325,10 +325,10 @@ type SQLiteWebLogData (conn : SqliteConnection) =
} }
interface IWebLogData with interface IWebLogData with
member _.add webLog = add webLog member _.Add webLog = add webLog
member _.all () = all () member _.All () = all ()
member _.delete webLogId = delete webLogId member _.Delete webLogId = delete webLogId
member _.findByHost url = findByHost url member _.FindByHost url = findByHost url
member _.findById webLogId = findById webLogId member _.FindById webLogId = findById webLogId
member _.updateSettings webLog = updateSettings webLog member _.UpdateSettings webLog = updateSettings webLog
member _.updateRssOptions webLog = updateRssOptions webLog member _.UpdateRssOptions webLog = updateRssOptions webLog

View File

@ -1,5 +1,6 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open System
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
@ -21,6 +22,8 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
cmd.Parameters.AddWithValue ("@salt", user.salt) cmd.Parameters.AddWithValue ("@salt", user.salt)
cmd.Parameters.AddWithValue ("@url", maybe user.url) cmd.Parameters.AddWithValue ("@url", maybe user.url)
cmd.Parameters.AddWithValue ("@accessLevel", AccessLevel.toString user.accessLevel) cmd.Parameters.AddWithValue ("@accessLevel", AccessLevel.toString user.accessLevel)
cmd.Parameters.AddWithValue ("@createdOn", user.createdOn)
cmd.Parameters.AddWithValue ("@lastSeenOn", maybe user.lastSeenOn)
] |> ignore ] |> ignore
// IMPLEMENTATION FUNCTIONS // IMPLEMENTATION FUNCTIONS
@ -31,10 +34,10 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
cmd.CommandText <- """ cmd.CommandText <- """
INSERT INTO web_log_user ( INSERT INTO web_log_user (
id, web_log_id, user_name, first_name, last_name, preferred_name, password_hash, salt, url, id, web_log_id, user_name, first_name, last_name, preferred_name, password_hash, salt, url,
access_level access_level, created_on, last_seen_on
) VALUES ( ) VALUES (
@id, @webLogId, @userName, @firstName, @lastName, @preferredName, @passwordHash, @salt, @url, @id, @webLogId, @userName, @firstName, @lastName, @preferredName, @passwordHash, @salt, @url,
@accessLevel @accessLevel, @createdOn, @lastSeenOn
)""" )"""
addWebLogUserParameters cmd user addWebLogUserParameters cmd user
do! write cmd do! write cmd
@ -91,6 +94,22 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
do! add user do! add user
} }
/// Set a user's last seen date/time to now
let setLastSeen userId webLogId = backgroundTask {
use cmd = conn.CreateCommand ()
cmd.CommandText <- """
UPDATE web_log_user
SET last_seen_on = @lastSeenOn
WHERE id = @id
AND web_log_id = @webLogId"""
addWebLogId cmd webLogId
[ cmd.Parameters.AddWithValue ("@id", WebLogUserId.toString userId)
cmd.Parameters.AddWithValue ("@lastSeenOn", DateTime.UtcNow)
] |> ignore
let! _ = cmd.ExecuteNonQueryAsync ()
()
}
/// Update a user /// Update a user
let update user = backgroundTask { let update user = backgroundTask {
use cmd = conn.CreateCommand () use cmd = conn.CreateCommand ()
@ -103,7 +122,9 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
password_hash = @passwordHash, password_hash = @passwordHash,
salt = @salt, salt = @salt,
url = @url, url = @url,
access_level = @accessLevel access_level = @accessLevel,
created_on = @createdOn,
last_seen_on = @lastSeenOn
WHERE id = @id WHERE id = @id
AND web_log_id = @webLogId""" AND web_log_id = @webLogId"""
addWebLogUserParameters cmd user addWebLogUserParameters cmd user
@ -111,10 +132,11 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
} }
interface IWebLogUserData with interface IWebLogUserData with
member _.add user = add user member _.Add user = add user
member _.findByEmail email webLogId = findByEmail email webLogId member _.FindByEmail email webLogId = findByEmail email webLogId
member _.findById userId webLogId = findById userId webLogId member _.FindById userId webLogId = findById userId webLogId
member _.findByWebLog webLogId = findByWebLog webLogId member _.FindByWebLog webLogId = findByWebLog webLogId
member _.findNames webLogId userIds = findNames webLogId userIds member _.FindNames webLogId userIds = findNames webLogId userIds
member this.restore users = restore users member _.Restore users = restore users
member _.update user = update user member _.SetLastSeen userId webLogId = setLastSeen userId webLogId
member _.Update user = update user

View File

@ -40,7 +40,7 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>) =
member _.WebLog = SQLiteWebLogData conn member _.WebLog = SQLiteWebLogData conn
member _.WebLogUser = SQLiteWebLogUserData conn member _.WebLogUser = SQLiteWebLogUserData conn
member _.startUp () = backgroundTask { member _.StartUp () = backgroundTask {
use cmd = conn.CreateCommand () use cmd = conn.CreateCommand ()
@ -174,7 +174,9 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>) =
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
salt TEXT NOT NULL, salt TEXT NOT NULL,
url TEXT, url TEXT,
access_level TEXT NOT NULL); access_level TEXT NOT NULL,
created_on TEXT NOT NULL,
last_seen_on TEXT NOT NULL);
CREATE INDEX web_log_user_web_log_idx ON web_log_user (web_log_id); CREATE INDEX web_log_user_web_log_idx ON web_log_user (web_log_id);
CREATE INDEX web_log_user_user_name_idx ON web_log_user (web_log_id, user_name)""" CREATE INDEX web_log_user_user_name_idx ON web_log_user (web_log_id, user_name)"""
do! write cmd do! write cmd

View File

@ -9,13 +9,13 @@ open MyWebLog.ViewModels
let rec orderByHierarchy (cats : Category list) parentId slugBase parentNames = seq { let rec orderByHierarchy (cats : Category list) parentId slugBase parentNames = seq {
for cat in cats |> List.filter (fun c -> c.parentId = parentId) do for cat in cats |> List.filter (fun c -> c.parentId = parentId) do
let fullSlug = (match slugBase with Some it -> $"{it}/" | None -> "") + cat.slug let fullSlug = (match slugBase with Some it -> $"{it}/" | None -> "") + cat.slug
{ id = CategoryId.toString cat.id { Id = CategoryId.toString cat.id
slug = fullSlug Slug = fullSlug
name = cat.name Name = cat.name
description = cat.description Description = cat.description
parentNames = Array.ofList parentNames ParentNames = Array.ofList parentNames
// Post counts are filled on a second pass // Post counts are filled on a second pass
postCount = 0 PostCount = 0
} }
yield! orderByHierarchy cats (Some cat.id) (Some fullSlug) ([ cat.name ] |> List.append parentNames) yield! orderByHierarchy cats (Some cat.id) (Some fullSlug) ([ cat.name ] |> List.append parentNames)
} }

View File

@ -438,6 +438,12 @@ type WebLogUser =
/// The user's access level /// The user's access level
accessLevel : AccessLevel accessLevel : AccessLevel
/// When the user was created
createdOn : DateTime
/// When the user last logged on
lastSeenOn : DateTime option
} }
/// Functions to support web log users /// Functions to support web log users
@ -455,6 +461,8 @@ module WebLogUser =
salt = Guid.Empty salt = Guid.Empty
url = None url = None
accessLevel = Author accessLevel = Author
createdOn = DateTime.UnixEpoch
lastSeenOn = None
} }
/// Get the user's displayed name /// Get the user's displayed name

File diff suppressed because it is too large Load Diff

View File

@ -77,7 +77,7 @@ module WebLogCache =
/// Fill the web log cache from the database /// Fill the web log cache from the database
let fill (data : IData) = backgroundTask { let fill (data : IData) = backgroundTask {
let! webLogs = data.WebLog.all () let! webLogs = data.WebLog.All ()
_cache <- webLogs _cache <- webLogs
} }
@ -99,7 +99,7 @@ module PageListCache =
/// Update the pages for the current web log /// Update the pages for the current web log
let update (ctx : HttpContext) = backgroundTask { let update (ctx : HttpContext) = backgroundTask {
let webLog = ctx.WebLog let webLog = ctx.WebLog
let! pages = ctx.Data.Page.findListed webLog.id let! pages = ctx.Data.Page.FindListed webLog.id
_cache[webLog.urlBase] <- _cache[webLog.urlBase] <-
pages pages
|> List.map (fun pg -> DisplayPage.fromPage webLog { pg with text = "" }) |> List.map (fun pg -> DisplayPage.fromPage webLog { pg with text = "" })
@ -123,7 +123,7 @@ module CategoryCache =
/// Update the cache with fresh data /// Update the cache with fresh data
let update (ctx : HttpContext) = backgroundTask { let update (ctx : HttpContext) = backgroundTask {
let! cats = ctx.Data.Category.findAllForView ctx.WebLog.id let! cats = ctx.Data.Category.FindAllForView ctx.WebLog.id
_cache[ctx.WebLog.urlBase] <- cats _cache[ctx.WebLog.urlBase] <- cats
} }
@ -147,7 +147,7 @@ module TemplateCache =
match _cache.ContainsKey templatePath with match _cache.ContainsKey templatePath with
| true -> () | true -> ()
| false -> | false ->
match! data.Theme.findById (ThemeId themeId) with match! data.Theme.FindById (ThemeId themeId) with
| Some theme -> | Some theme ->
let mutable text = (theme.templates |> List.find (fun t -> t.name = templateName)).text let mutable text = (theme.templates |> List.find (fun t -> t.name = templateName)).text
while hasInclude.IsMatch text do while hasInclude.IsMatch text do
@ -178,13 +178,13 @@ module ThemeAssetCache =
/// Refresh the list of assets for the given theme /// Refresh the list of assets for the given theme
let refreshTheme themeId (data : IData) = backgroundTask { let refreshTheme themeId (data : IData) = backgroundTask {
let! assets = data.ThemeAsset.findByTheme themeId let! assets = data.ThemeAsset.FindByTheme themeId
_cache[themeId] <- assets |> List.map (fun a -> match a.id with ThemeAssetId (_, path) -> path) _cache[themeId] <- assets |> List.map (fun a -> match a.id with ThemeAssetId (_, path) -> path)
} }
/// Fill the theme asset cache /// Fill the theme asset cache
let fill (data : IData) = backgroundTask { let fill (data : IData) = backgroundTask {
let! assets = data.ThemeAsset.all () let! assets = data.ThemeAsset.All ()
for asset in assets do for asset in assets do
let (ThemeAssetId (themeId, path)) = asset.id let (ThemeAssetId (themeId, path)) = asset.id
if not (_cache.ContainsKey themeId) then _cache[themeId] <- [] if not (_cache.ContainsKey themeId) then _cache[themeId] <- []

View File

@ -8,9 +8,11 @@ open DotLiquid
open Giraffe.ViewEngine open Giraffe.ViewEngine
open MyWebLog.ViewModels open MyWebLog.ViewModels
/// Get the current web log from the DotLiquid context /// Extensions on the DotLiquid Context object
let webLog (ctx : Context) = type Context with
ctx.Environments[0].["web_log"] :?> WebLog
/// Get the current web log from the DotLiquid context
member this.WebLog = this.Environments[0].["web_log"] :?> WebLog
/// Does an asset exist for the current theme? /// Does an asset exist for the current theme?
let assetExists fileName (webLog : WebLog) = let assetExists fileName (webLog : WebLog) =
@ -20,12 +22,12 @@ let assetExists fileName (webLog : WebLog) =
let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) = let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) =
match item with match item with
| :? String as link -> Some link | :? String as link -> Some link
| :? DisplayPage as page -> Some page.permalink | :? DisplayPage as page -> Some page.Permalink
| :? PostListItem as post -> Some post.permalink | :? PostListItem as post -> Some post.Permalink
| :? DropProxy as proxy -> Option.ofObj proxy["permalink"] |> Option.map string | :? DropProxy as proxy -> Option.ofObj proxy["permalink"] |> Option.map string
| _ -> None | _ -> None
|> function |> function
| Some link -> linkFunc (webLog ctx) (Permalink link) | Some link -> linkFunc ctx.WebLog (Permalink link)
| None -> $"alert('unknown item type {item.GetType().Name}')" | None -> $"alert('unknown item type {item.GetType().Name}')"
@ -39,11 +41,11 @@ type AbsoluteLinkFilter () =
type CategoryLinkFilter () = type CategoryLinkFilter () =
static member CategoryLink (ctx : Context, catObj : obj) = static member CategoryLink (ctx : Context, catObj : obj) =
match catObj with match catObj with
| :? DisplayCategory as cat -> Some cat.slug | :? DisplayCategory as cat -> Some cat.Slug
| :? DropProxy as proxy -> Option.ofObj proxy["slug"] |> Option.map string | :? DropProxy as proxy -> Option.ofObj proxy["slug"] |> Option.map string
| _ -> None | _ -> None
|> function |> function
| Some slug -> WebLog.relativeUrl (webLog ctx) (Permalink $"category/{slug}/") | Some slug -> WebLog.relativeUrl ctx.WebLog (Permalink $"category/{slug}/")
| None -> $"alert('unknown category object type {catObj.GetType().Name}')" | None -> $"alert('unknown category object type {catObj.GetType().Name}')"
@ -51,12 +53,12 @@ type CategoryLinkFilter () =
type EditPageLinkFilter () = type EditPageLinkFilter () =
static member EditPageLink (ctx : Context, pageObj : obj) = static member EditPageLink (ctx : Context, pageObj : obj) =
match pageObj with match pageObj with
| :? DisplayPage as page -> Some page.id | :? DisplayPage as page -> Some page.Id
| :? DropProxy as proxy -> Option.ofObj proxy["id"] |> Option.map string | :? DropProxy as proxy -> Option.ofObj proxy["id"] |> Option.map string
| :? String as theId -> Some theId | :? String as theId -> Some theId
| _ -> None | _ -> None
|> function |> function
| Some pageId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/page/{pageId}/edit") | Some pageId -> WebLog.relativeUrl ctx.WebLog (Permalink $"admin/page/{pageId}/edit")
| None -> $"alert('unknown page object type {pageObj.GetType().Name}')" | None -> $"alert('unknown page object type {pageObj.GetType().Name}')"
@ -64,26 +66,25 @@ type EditPageLinkFilter () =
type EditPostLinkFilter () = type EditPostLinkFilter () =
static member EditPostLink (ctx : Context, postObj : obj) = static member EditPostLink (ctx : Context, postObj : obj) =
match postObj with match postObj with
| :? PostListItem as post -> Some post.id | :? PostListItem as post -> Some post.Id
| :? DropProxy as proxy -> Option.ofObj proxy["id"] |> Option.map string | :? DropProxy as proxy -> Option.ofObj proxy["id"] |> Option.map string
| :? String as theId -> Some theId | :? String as theId -> Some theId
| _ -> None | _ -> None
|> function |> function
| Some postId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/post/{postId}/edit") | Some postId -> WebLog.relativeUrl ctx.WebLog (Permalink $"admin/post/{postId}/edit")
| None -> $"alert('unknown post object type {postObj.GetType().Name}')" | None -> $"alert('unknown post object type {postObj.GetType().Name}')"
/// A filter to generate nav links, highlighting the active link (exact match) /// A filter to generate nav links, highlighting the active link (exact match)
type NavLinkFilter () = type NavLinkFilter () =
static member NavLink (ctx : Context, url : string, text : string) = static member NavLink (ctx : Context, url : string, text : string) =
let webLog = webLog ctx let _, path = WebLog.hostAndPath ctx.WebLog
let _, path = WebLog.hostAndPath webLog
let path = if path = "" then path else $"{path.Substring 1}/" let path = if path = "" then path else $"{path.Substring 1}/"
seq { seq {
"<li class=\"nav-item\"><a class=\"nav-link" "<li class=\"nav-item\"><a class=\"nav-link"
if (string ctx.Environments[0].["current_page"]).StartsWith $"{path}{url}" then " active" if (string ctx.Environments[0].["current_page"]).StartsWith $"{path}{url}" then " active"
"\" href=\"" "\" href=\""
WebLog.relativeUrl webLog (Permalink url) WebLog.relativeUrl ctx.WebLog (Permalink url)
"\">" "\">"
text text
"</a></li>" "</a></li>"
@ -94,8 +95,7 @@ type NavLinkFilter () =
/// A filter to generate a link for theme asset (image, stylesheet, script, etc.) /// A filter to generate a link for theme asset (image, stylesheet, script, etc.)
type ThemeAssetFilter () = type ThemeAssetFilter () =
static member ThemeAsset (ctx : Context, asset : string) = static member ThemeAsset (ctx : Context, asset : string) =
let webLog = webLog ctx WebLog.relativeUrl ctx.WebLog (Permalink $"themes/{ctx.WebLog.themePath}/{asset}")
WebLog.relativeUrl webLog (Permalink $"themes/{webLog.themePath}/{asset}")
/// Create various items in the page header based on the state of the page being generated /// Create various items in the page header based on the state of the page being generated
@ -103,7 +103,7 @@ type PageHeadTag () =
inherit Tag () inherit Tag ()
override this.Render (context : Context, result : TextWriter) = override this.Render (context : Context, result : TextWriter) =
let webLog = webLog context let webLog = context.WebLog
// spacer // spacer
let s = " " let s = " "
let getBool name = let getBool name =
@ -137,12 +137,12 @@ type PageHeadTag () =
if getBool "is_post" then if getBool "is_post" then
let post = context.Environments[0].["model"] :?> PostDisplay let post = context.Environments[0].["model"] :?> PostDisplay
let url = WebLog.absoluteUrl webLog (Permalink post.posts[0].permalink) let url = WebLog.absoluteUrl webLog (Permalink post.Posts[0].Permalink)
result.WriteLine $"""{s}<link rel="canonical" href="{url}">""" result.WriteLine $"""{s}<link rel="canonical" href="{url}">"""
if getBool "is_page" then if getBool "is_page" then
let page = context.Environments[0].["page"] :?> DisplayPage let page = context.Environments[0].["page"] :?> DisplayPage
let url = WebLog.absoluteUrl webLog (Permalink page.permalink) let url = WebLog.absoluteUrl webLog (Permalink page.Permalink)
result.WriteLine $"""{s}<link rel="canonical" href="{url}">""" result.WriteLine $"""{s}<link rel="canonical" href="{url}">"""
@ -151,7 +151,7 @@ type PageFootTag () =
inherit Tag () inherit Tag ()
override this.Render (context : Context, result : TextWriter) = override this.Render (context : Context, result : TextWriter) =
let webLog = webLog context let webLog = context.WebLog
// spacer // spacer
let s = " " let s = " "
@ -176,7 +176,7 @@ type TagLinkFilter () =
|> function |> function
| Some tagMap -> tagMap.urlValue | Some tagMap -> tagMap.urlValue
| None -> tag.Replace (" ", "+") | None -> tag.Replace (" ", "+")
|> function tagUrl -> WebLog.relativeUrl (webLog ctx) (Permalink $"tag/{tagUrl}/") |> function tagUrl -> WebLog.relativeUrl ctx.WebLog (Permalink $"tag/{tagUrl}/")
/// Create links for a user to log on or off, and a dashboard link if they are logged off /// Create links for a user to log on or off, and a dashboard link if they are logged off
@ -184,8 +184,7 @@ type UserLinksTag () =
inherit Tag () inherit Tag ()
override this.Render (context : Context, result : TextWriter) = override this.Render (context : Context, result : TextWriter) =
let webLog = webLog context let link it = WebLog.relativeUrl context.WebLog (Permalink it)
let link it = WebLog.relativeUrl webLog (Permalink it)
seq { seq {
"""<ul class="navbar-nav flex-grow-1 justify-content-end">""" """<ul class="navbar-nav flex-grow-1 justify-content-end">"""
match Convert.ToBoolean context.Environments[0].["is_logged_on"] with match Convert.ToBoolean context.Environments[0].["is_logged_on"] with

View File

@ -9,25 +9,25 @@ open MyWebLog.ViewModels
// GET /admin // GET /admin
let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task { let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let webLogId = ctx.WebLog.id let getCount (f : WebLogId -> Task<int>) = f ctx.WebLog.id
let data = ctx.Data let data = ctx.Data
let getCount (f : WebLogId -> Task<int>) = f webLogId let posts = getCount (data.Post.CountByStatus Published)
let! posts = data.Post.countByStatus Published |> getCount let drafts = getCount (data.Post.CountByStatus Draft)
let! drafts = data.Post.countByStatus Draft |> getCount let pages = getCount data.Page.CountAll
let! pages = data.Page.countAll |> getCount let listed = getCount data.Page.CountListed
let! listed = data.Page.countListed |> getCount let cats = getCount data.Category.CountAll
let! cats = data.Category.countAll |> getCount let topCats = getCount data.Category.CountTopLevel
let! topCats = data.Category.countTopLevel |> getCount let! _ = Task.WhenAll (posts, drafts, pages, listed, cats, topCats)
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
page_title = "Dashboard" page_title = "Dashboard"
model = model =
{ posts = posts { Posts = posts.Result
drafts = drafts Drafts = drafts.Result
pages = pages Pages = pages.Result
listedPages = listed ListedPages = listed.Result
categories = cats Categories = cats.Result
topLevelCategories = topCats TopLevelCategories = topCats.Result
} }
|} |}
|> viewForTheme "admin" "dashboard" next ctx |> viewForTheme "admin" "dashboard" next ctx
@ -49,14 +49,12 @@ let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
} }
// GET /admin/categories/bare // GET /admin/categories/bare
let listCategoriesBare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let listCategoriesBare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
return! Hash.FromAnonymousObject {|
Hash.FromAnonymousObject {| categories = CategoryCache.get ctx
categories = CategoryCache.get ctx csrf = ctx.CsrfTokenSet
csrf = ctx.CsrfTokenSet |}
|} |> bareForTheme "admin" "category-list-body" next ctx
|> bareForTheme "admin" "category-list-body" next ctx
}
// GET /admin/category/{id}/edit // GET /admin/category/{id}/edit
@ -65,7 +63,7 @@ let editCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ct
match catId with match catId with
| "new" -> return Some ("Add a New Category", { Category.empty with id = CategoryId "new" }) | "new" -> return Some ("Add a New Category", { Category.empty with id = CategoryId "new" })
| _ -> | _ ->
match! ctx.Data.Category.findById (CategoryId catId) ctx.WebLog.id with match! ctx.Data.Category.FindById (CategoryId catId) ctx.WebLog.id with
| Some cat -> return Some ("Edit Category", cat) | Some cat -> return Some ("Edit Category", cat)
| None -> return None | None -> return None
} }
@ -86,34 +84,33 @@ let editCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ct
let saveCategory : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let saveCategory : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
let! model = ctx.BindFormAsync<EditCategoryModel> () let! model = ctx.BindFormAsync<EditCategoryModel> ()
let! category = task { let category =
match model.categoryId with match model.CategoryId with
| "new" -> return Some { Category.empty with id = CategoryId.create (); webLogId = ctx.WebLog.id } | "new" -> Task.FromResult (Some { Category.empty with id = CategoryId.create (); webLogId = ctx.WebLog.id })
| catId -> return! data.Category.findById (CategoryId catId) ctx.WebLog.id | catId -> data.Category.FindById (CategoryId catId) ctx.WebLog.id
} match! category with
match category with
| Some cat -> | Some cat ->
let cat = let cat =
{ cat with { cat with
name = model.name name = model.Name
slug = model.slug slug = model.Slug
description = if model.description = "" then None else Some model.description description = if model.Description = "" then None else Some model.Description
parentId = if model.parentId = "" then None else Some (CategoryId model.parentId) parentId = if model.ParentId = "" then None else Some (CategoryId model.ParentId)
} }
do! (match model.categoryId with "new" -> data.Category.add | _ -> data.Category.update) cat do! (match model.CategoryId with "new" -> data.Category.Add | _ -> data.Category.Update) cat
do! CategoryCache.update ctx do! CategoryCache.update ctx
do! addMessage ctx { UserMessage.success with message = "Category saved successfully" } do! addMessage ctx { UserMessage.success with Message = "Category saved successfully" }
return! listCategoriesBare next ctx return! listCategoriesBare next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// POST /admin/category/{id}/delete // POST /admin/category/{id}/delete
let deleteCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let deleteCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
match! ctx.Data.Category.delete (CategoryId catId) ctx.WebLog.id with match! ctx.Data.Category.Delete (CategoryId catId) ctx.WebLog.id with
| true -> | true ->
do! CategoryCache.update ctx do! CategoryCache.update ctx
do! addMessage ctx { UserMessage.success with message = "Category deleted successfully" } do! addMessage ctx { UserMessage.success with Message = "Category deleted successfully" }
| false -> do! addMessage ctx { UserMessage.error with message = "Category not found; cannot delete" } | false -> do! addMessage ctx { UserMessage.error with Message = "Category not found; cannot delete" }
return! listCategoriesBare next ctx return! listCategoriesBare next ctx
} }
@ -123,7 +120,7 @@ open Microsoft.AspNetCore.Http
/// Get the hash necessary to render the tag mapping list /// Get the hash necessary to render the tag mapping list
let private tagMappingHash (ctx : HttpContext) = task { let private tagMappingHash (ctx : HttpContext) = task {
let! mappings = ctx.Data.TagMap.findByWebLog ctx.WebLog.id let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.id
return Hash.FromAnonymousObject {| return Hash.FromAnonymousObject {|
csrf = ctx.CsrfTokenSet csrf = ctx.CsrfTokenSet
web_log = ctx.WebLog web_log = ctx.WebLog
@ -136,11 +133,10 @@ let private tagMappingHash (ctx : HttpContext) = task {
let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! hash = tagMappingHash ctx let! hash = tagMappingHash ctx
let! listTemplate = TemplateCache.get "admin" "tag-mapping-list-body" ctx.Data let! listTemplate = TemplateCache.get "admin" "tag-mapping-list-body" ctx.Data
return!
hash.Add ("tag_mapping_list", listTemplate.Render hash) addToHash "tag_mapping_list" (listTemplate.Render hash) hash
hash.Add ("page_title", "Tag Mappings") |> addToHash "page_title" "Tag Mappings"
|> viewForTheme "admin" "tag-mapping-list" next ctx
return! viewForTheme "admin" "tag-mapping-list" next ctx hash
} }
// GET /admin/settings/tag-mappings/bare // GET /admin/settings/tag-mappings/bare
@ -153,10 +149,8 @@ let tagMappingsBare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -
let editMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let editMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let isNew = tagMapId = "new" let isNew = tagMapId = "new"
let tagMap = let tagMap =
if isNew then if isNew then Task.FromResult (Some { TagMap.empty with id = TagMapId "new" })
Task.FromResult (Some { TagMap.empty with id = TagMapId "new" }) else ctx.Data.TagMap.FindById (TagMapId tagMapId) ctx.WebLog.id
else
ctx.Data.TagMap.findById (TagMapId tagMapId) ctx.WebLog.id
match! tagMap with match! tagMap with
| Some tm -> | Some tm ->
return! return!
@ -174,23 +168,22 @@ let saveMapping : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
let data = ctx.Data let data = ctx.Data
let! model = ctx.BindFormAsync<EditTagMapModel> () let! model = ctx.BindFormAsync<EditTagMapModel> ()
let tagMap = let tagMap =
if model.id = "new" then if model.IsNew then
Task.FromResult (Some { TagMap.empty with id = TagMapId.create (); webLogId = ctx.WebLog.id }) Task.FromResult (Some { TagMap.empty with id = TagMapId.create (); webLogId = ctx.WebLog.id })
else else data.TagMap.FindById (TagMapId model.Id) ctx.WebLog.id
data.TagMap.findById (TagMapId model.id) ctx.WebLog.id
match! tagMap with match! tagMap with
| Some tm -> | Some tm ->
do! data.TagMap.save { tm with tag = model.tag.ToLower (); urlValue = model.urlValue.ToLower () } do! data.TagMap.Save { tm with tag = model.Tag.ToLower (); urlValue = model.UrlValue.ToLower () }
do! addMessage ctx { UserMessage.success with message = "Tag mapping saved successfully" } do! addMessage ctx { UserMessage.success with Message = "Tag mapping saved successfully" }
return! tagMappingsBare next ctx return! tagMappingsBare next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// POST /admin/settings/tag-mapping/{id}/delete // POST /admin/settings/tag-mapping/{id}/delete
let deleteMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let deleteMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
match! ctx.Data.TagMap.delete (TagMapId tagMapId) ctx.WebLog.id with match! ctx.Data.TagMap.Delete (TagMapId tagMapId) ctx.WebLog.id with
| true -> do! addMessage ctx { UserMessage.success with message = "Tag mapping deleted successfully" } | true -> do! addMessage ctx { UserMessage.success with Message = "Tag mapping deleted successfully" }
| false -> do! addMessage ctx { UserMessage.error with message = "Tag mapping not found; nothing deleted" } | false -> do! addMessage ctx { UserMessage.error with Message = "Tag mapping not found; nothing deleted" }
return! tagMappingsBare next ctx return! tagMappingsBare next ctx
} }
@ -203,14 +196,12 @@ open System.Text.RegularExpressions
open MyWebLog.Data open MyWebLog.Data
// GET /admin/theme/update // GET /admin/theme/update
let themeUpdatePage : HttpHandler = requireAccess Administrator >=> fun next ctx -> task { let themeUpdatePage : HttpHandler = requireAccess Administrator >=> fun next ctx ->
return! Hash.FromAnonymousObject {|
Hash.FromAnonymousObject {| page_title = "Upload Theme"
page_title = "Upload Theme" csrf = ctx.CsrfTokenSet
csrf = ctx.CsrfTokenSet |}
|} |> viewForTheme "admin" "upload-theme" next ctx
|> viewForTheme "admin" "upload-theme" next ctx
}
/// Update the name and version for a theme based on the version.txt file, if present /// Update the name and version for a theme based on the version.txt file, if present
let private updateNameAndVersion (theme : Theme) (zip : ZipArchive) = backgroundTask { let private updateNameAndVersion (theme : Theme) (zip : ZipArchive) = backgroundTask {
@ -223,17 +214,15 @@ let private updateNameAndVersion (theme : Theme) (zip : ZipArchive) = background
let displayName = if parts[0] > "" then parts[0] else ThemeId.toString theme.id let displayName = if parts[0] > "" then parts[0] else ThemeId.toString theme.id
let version = if parts.Length > 1 && parts[1] > "" then parts[1] else now () let version = if parts.Length > 1 && parts[1] > "" then parts[1] else now ()
return { theme with name = displayName; version = version } return { theme with name = displayName; version = version }
| None -> | None -> return { theme with name = ThemeId.toString theme.id; version = now () }
return { theme with name = ThemeId.toString theme.id; version = now () }
} }
/// Delete all theme assets, and remove templates from theme /// Delete all theme assets, and remove templates from theme
let private checkForCleanLoad (theme : Theme) cleanLoad (data : IData) = backgroundTask { let private checkForCleanLoad (theme : Theme) cleanLoad (data : IData) = backgroundTask {
if cleanLoad then if cleanLoad then
do! data.ThemeAsset.deleteByTheme theme.id do! data.ThemeAsset.DeleteByTheme theme.id
return { theme with templates = [] } return { theme with templates = [] }
else else return theme
return theme
} }
/// Update the theme with all templates from the ZIP archive /// Update the theme with all templates from the ZIP archive
@ -261,7 +250,7 @@ let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundT
if assetName <> "" && not (assetName.EndsWith "/") then if assetName <> "" && not (assetName.EndsWith "/") then
use stream = new MemoryStream () use stream = new MemoryStream ()
do! asset.Open().CopyToAsync stream do! asset.Open().CopyToAsync stream
do! data.ThemeAsset.save do! data.ThemeAsset.Save
{ id = ThemeAssetId (themeId, assetName) { id = ThemeAssetId (themeId, assetName)
updatedOn = asset.LastWriteTime.DateTime updatedOn = asset.LastWriteTime.DateTime
data = stream.ToArray () data = stream.ToArray ()
@ -278,14 +267,14 @@ let loadThemeFromZip themeName file clean (data : IData) = backgroundTask {
use zip = new ZipArchive (file, ZipArchiveMode.Read) use zip = new ZipArchive (file, ZipArchiveMode.Read)
let themeId = ThemeId themeName let themeId = ThemeId themeName
let! theme = backgroundTask { let! theme = backgroundTask {
match! data.Theme.findById themeId with match! data.Theme.FindById themeId with
| Some t -> return t | Some t -> return t
| None -> return { Theme.empty with id = themeId } | None -> return { Theme.empty with id = themeId }
} }
let! theme = updateNameAndVersion theme zip let! theme = updateNameAndVersion theme zip
let! theme = checkForCleanLoad theme clean data let! theme = checkForCleanLoad theme clean data
let! theme = updateTemplates theme zip let! theme = updateTemplates theme zip
do! data.Theme.save theme do! data.Theme.Save theme
do! updateAssets themeId zip data do! updateAssets themeId zip data
} }
@ -301,16 +290,15 @@ let updateTheme : HttpHandler = requireAccess Administrator >=> fun next ctx ->
do! loadThemeFromZip themeName stream true data do! loadThemeFromZip themeName stream true data
do! ThemeAssetCache.refreshTheme (ThemeId themeName) data do! ThemeAssetCache.refreshTheme (ThemeId themeName) data
TemplateCache.invalidateTheme themeName TemplateCache.invalidateTheme themeName
do! addMessage ctx { UserMessage.success with message = "Theme updated successfully" } do! addMessage ctx { UserMessage.success with Message = "Theme updated successfully" }
return! redirectToGet "admin/dashboard" next ctx return! redirectToGet "admin/dashboard" next ctx
| Ok _ -> | Ok _ ->
do! addMessage ctx { UserMessage.error with message = "You may not replace the admin theme" } do! addMessage ctx { UserMessage.error with Message = "You may not replace the admin theme" }
return! redirectToGet "admin/theme/update" next ctx return! redirectToGet "admin/theme/update" next ctx
| Error message -> | Error message ->
do! addMessage ctx { UserMessage.error with message = message } do! addMessage ctx { UserMessage.error with Message = message }
return! redirectToGet "admin/theme/update" next ctx return! redirectToGet "admin/theme/update" next ctx
else else return! RequestErrors.BAD_REQUEST "Bad request" next ctx
return! RequestErrors.BAD_REQUEST "Bad request" next ctx
} }
// -- WEB LOG SETTINGS -- // -- WEB LOG SETTINGS --
@ -320,8 +308,8 @@ open System.Collections.Generic
// GET /admin/settings // GET /admin/settings
let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
let! allPages = data.Page.all ctx.WebLog.id let! allPages = data.Page.All ctx.WebLog.id
let! themes = data.Theme.all () let! themes = data.Theme.All ()
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
page_title = "Web Log Settings" page_title = "Web Log Settings"
@ -351,11 +339,11 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
let! model = ctx.BindFormAsync<SettingsModel> () let! model = ctx.BindFormAsync<SettingsModel> ()
match! data.WebLog.findById ctx.WebLog.id with match! data.WebLog.FindById ctx.WebLog.id with
| Some webLog -> | Some webLog ->
let oldSlug = webLog.slug let oldSlug = webLog.slug
let webLog = model.update webLog let webLog = model.update webLog
do! data.WebLog.updateSettings webLog do! data.WebLog.UpdateSettings webLog
// Update cache // Update cache
WebLogCache.set webLog WebLogCache.set webLog
@ -366,7 +354,7 @@ let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> t
let oldDir = Path.Combine (uploadRoot, oldSlug) let oldDir = Path.Combine (uploadRoot, oldSlug)
if Directory.Exists oldDir then Directory.Move (oldDir, Path.Combine (uploadRoot, webLog.slug)) if Directory.Exists oldDir then Directory.Move (oldDir, Path.Combine (uploadRoot, webLog.slug))
do! addMessage ctx { UserMessage.success with message = "Web log settings saved successfully" } do! addMessage ctx { UserMessage.success with Message = "Web log settings saved successfully" }
return! redirectToGet "admin/settings" next ctx return! redirectToGet "admin/settings" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }

View File

@ -49,17 +49,17 @@ let deriveFeedType (ctx : HttpContext) feedPath : (FeedType * int) option =
/// Determine the function to retrieve posts for the given feed /// Determine the function to retrieve posts for the given feed
let private getFeedPosts ctx feedType = let private getFeedPosts ctx feedType =
let childIds catId = let childIds catId =
let cat = CategoryCache.get ctx |> Array.find (fun c -> c.id = CategoryId.toString catId) let cat = CategoryCache.get ctx |> Array.find (fun c -> c.Id = CategoryId.toString catId)
getCategoryIds cat.slug ctx getCategoryIds cat.Slug ctx
let data = ctx.Data let data = ctx.Data
match feedType with match feedType with
| StandardFeed _ -> data.Post.findPageOfPublishedPosts ctx.WebLog.id 1 | StandardFeed _ -> data.Post.FindPageOfPublishedPosts ctx.WebLog.id 1
| CategoryFeed (catId, _) -> data.Post.findPageOfCategorizedPosts ctx.WebLog.id (childIds catId) 1 | CategoryFeed (catId, _) -> data.Post.FindPageOfCategorizedPosts ctx.WebLog.id (childIds catId) 1
| TagFeed (tag, _) -> data.Post.findPageOfTaggedPosts ctx.WebLog.id tag 1 | TagFeed (tag, _) -> data.Post.FindPageOfTaggedPosts ctx.WebLog.id tag 1
| Custom (feed, _) -> | Custom (feed, _) ->
match feed.source with match feed.source with
| Category catId -> data.Post.findPageOfCategorizedPosts ctx.WebLog.id (childIds catId) 1 | Category catId -> data.Post.FindPageOfCategorizedPosts ctx.WebLog.id (childIds catId) 1
| Tag tag -> data.Post.findPageOfTaggedPosts ctx.WebLog.id tag 1 | Tag tag -> data.Post.FindPageOfTaggedPosts ctx.WebLog.id tag 1
/// Strip HTML from a string /// Strip HTML from a string
let private stripHtml text = WebUtility.HtmlDecode <| Regex.Replace (text, "<(.|\n)*?>", "") let private stripHtml text = WebUtility.HtmlDecode <| Regex.Replace (text, "<(.|\n)*?>", "")
@ -116,8 +116,8 @@ let private toFeedItem webLog (authors : MetaItem list) (cats : DisplayCategory[
Name = (authors |> List.find (fun a -> a.name = WebLogUserId.toString post.authorId)).value)) Name = (authors |> List.find (fun a -> a.name = WebLogUserId.toString post.authorId)).value))
[ post.categoryIds [ post.categoryIds
|> List.map (fun catId -> |> List.map (fun catId ->
let cat = cats |> Array.find (fun c -> c.id = CategoryId.toString catId) let cat = cats |> Array.find (fun c -> c.Id = CategoryId.toString catId)
SyndicationCategory (cat.name, WebLog.absoluteUrl webLog (Permalink $"category/{cat.slug}/"), cat.name)) SyndicationCategory (cat.Name, WebLog.absoluteUrl webLog (Permalink $"category/{cat.Slug}/"), cat.Name))
post.tags post.tags
|> List.map (fun tag -> |> List.map (fun tag ->
let urlTag = let urlTag =
@ -326,7 +326,7 @@ let private selfAndLink webLog feedType ctx =
| Custom (feed, _) -> | Custom (feed, _) ->
match feed.source with match feed.source with
| Category (CategoryId catId) -> | Category (CategoryId catId) ->
feed.path, Permalink $"category/{(CategoryCache.get ctx |> Array.find (fun c -> c.id = catId)).slug}" feed.path, Permalink $"category/{(CategoryCache.get ctx |> Array.find (fun c -> c.Id = catId)).Slug}"
| Tag tag -> feed.path, Permalink $"""tag/{tag.Replace(" ", "+")}/""" | Tag tag -> feed.path, Permalink $"""tag/{tag.Replace(" ", "+")}/"""
/// Set the title and description of the feed based on its source /// Set the title and description of the feed based on its source
@ -337,9 +337,9 @@ let private setTitleAndDescription feedType (webLog : WebLog) (cats : DisplayCat
feed.Title <- cleanText None webLog.name feed.Title <- cleanText None webLog.name
feed.Description <- cleanText webLog.subtitle webLog.name feed.Description <- cleanText webLog.subtitle webLog.name
| CategoryFeed (CategoryId catId, _) -> | CategoryFeed (CategoryId catId, _) ->
let cat = cats |> Array.find (fun it -> it.id = catId) let cat = cats |> Array.find (fun it -> it.Id = catId)
feed.Title <- cleanText None $"""{webLog.name} - "{stripHtml cat.name}" Category""" feed.Title <- cleanText None $"""{webLog.name} - "{stripHtml cat.Name}" Category"""
feed.Description <- cleanText cat.description $"""Posts categorized under "{cat.name}" """ feed.Description <- cleanText cat.Description $"""Posts categorized under "{cat.Name}" """
| TagFeed (tag, _) -> | TagFeed (tag, _) ->
feed.Title <- cleanText None $"""{webLog.name} - "{tag}" Tag""" feed.Title <- cleanText None $"""{webLog.name} - "{tag}" Tag"""
feed.Description <- cleanText None $"""Posts with the "{tag}" tag""" feed.Description <- cleanText None $"""Posts with the "{tag}" tag"""
@ -351,9 +351,9 @@ let private setTitleAndDescription feedType (webLog : WebLog) (cats : DisplayCat
| None -> | None ->
match custom.source with match custom.source with
| Category (CategoryId catId) -> | Category (CategoryId catId) ->
let cat = cats |> Array.find (fun it -> it.id = catId) let cat = cats |> Array.find (fun it -> it.Id = catId)
feed.Title <- cleanText None $"""{webLog.name} - "{stripHtml cat.name}" Category""" feed.Title <- cleanText None $"""{webLog.name} - "{stripHtml cat.Name}" Category"""
feed.Description <- cleanText cat.description $"""Posts categorized under "{cat.name}" """ feed.Description <- cleanText cat.Description $"""Posts categorized under "{cat.Name}" """
| Tag tag -> | Tag tag ->
feed.Title <- cleanText None $"""{webLog.name} - "{tag}" Tag""" feed.Title <- cleanText None $"""{webLog.name} - "{tag}" Tag"""
feed.Description <- cleanText None $"""Posts with the "{tag}" tag""" feed.Description <- cleanText None $"""Posts with the "{tag}" tag"""
@ -417,81 +417,79 @@ let generate (feedType : FeedType) postCount : HttpHandler = fun next ctx -> bac
open DotLiquid open DotLiquid
// GET: /admin/settings/rss // GET: /admin/settings/rss
let editSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let editSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
let feeds = let feeds =
ctx.WebLog.rss.customFeeds ctx.WebLog.rss.customFeeds
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx)) |> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|> Array.ofList |> Array.ofList
return! Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
page_title = "RSS Settings" page_title = "RSS Settings"
csrf = ctx.CsrfTokenSet csrf = ctx.CsrfTokenSet
model = EditRssModel.fromRssOptions ctx.WebLog.rss model = EditRssModel.fromRssOptions ctx.WebLog.rss
custom_feeds = feeds custom_feeds = feeds
|} |}
|> viewForTheme "admin" "rss-settings" next ctx |> viewForTheme "admin" "rss-settings" next ctx
}
// POST: /admin/settings/rss // POST: /admin/settings/rss
let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
let! model = ctx.BindFormAsync<EditRssModel> () let! model = ctx.BindFormAsync<EditRssModel> ()
match! data.WebLog.findById ctx.WebLog.id with match! data.WebLog.FindById ctx.WebLog.id with
| Some webLog -> | Some webLog ->
let webLog = { webLog with rss = model.updateOptions webLog.rss } let webLog = { webLog with rss = model.updateOptions webLog.rss }
do! data.WebLog.updateRssOptions webLog do! data.WebLog.UpdateRssOptions webLog
WebLogCache.set webLog WebLogCache.set webLog
do! addMessage ctx { UserMessage.success with message = "RSS settings updated successfully" } do! addMessage ctx { UserMessage.success with Message = "RSS settings updated successfully" }
return! redirectToGet "admin/settings/rss" next ctx return! redirectToGet "admin/settings/rss" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// GET: /admin/settings/rss/{id}/edit // GET: /admin/settings/rss/{id}/edit
let editCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let editCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
let customFeed = let customFeed =
match feedId with match feedId with
| "new" -> Some { CustomFeed.empty with id = CustomFeedId "new" } | "new" -> Some { CustomFeed.empty with id = CustomFeedId "new" }
| _ -> ctx.WebLog.rss.customFeeds |> List.tryFind (fun f -> f.id = CustomFeedId feedId) | _ -> ctx.WebLog.rss.customFeeds |> List.tryFind (fun f -> f.id = CustomFeedId feedId)
match customFeed with match customFeed with
| Some f -> | Some f ->
return! Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
page_title = $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed""" page_title = $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed"""
csrf = ctx.CsrfTokenSet csrf = ctx.CsrfTokenSet
model = EditCustomFeedModel.fromFeed f model = EditCustomFeedModel.fromFeed f
categories = CategoryCache.get ctx categories = CategoryCache.get ctx
medium_values = [| medium_values = [|
KeyValuePair.Create ("", "&ndash; Unspecified &ndash;") KeyValuePair.Create ("", "&ndash; Unspecified &ndash;")
KeyValuePair.Create (PodcastMedium.toString Podcast, "Podcast") KeyValuePair.Create (PodcastMedium.toString Podcast, "Podcast")
KeyValuePair.Create (PodcastMedium.toString Music, "Music") KeyValuePair.Create (PodcastMedium.toString Music, "Music")
KeyValuePair.Create (PodcastMedium.toString Video, "Video") KeyValuePair.Create (PodcastMedium.toString Video, "Video")
KeyValuePair.Create (PodcastMedium.toString Film, "Film") KeyValuePair.Create (PodcastMedium.toString Film, "Film")
KeyValuePair.Create (PodcastMedium.toString Audiobook, "Audiobook") KeyValuePair.Create (PodcastMedium.toString Audiobook, "Audiobook")
KeyValuePair.Create (PodcastMedium.toString Newsletter, "Newsletter") KeyValuePair.Create (PodcastMedium.toString Newsletter, "Newsletter")
KeyValuePair.Create (PodcastMedium.toString Blog, "Blog") KeyValuePair.Create (PodcastMedium.toString Blog, "Blog")
|] |]
|} |}
|> viewForTheme "admin" "custom-feed-edit" next ctx |> viewForTheme "admin" "custom-feed-edit" next ctx
| None -> return! Error.notFound next ctx | None -> Error.notFound next ctx
}
// POST: /admin/settings/rss/save // POST: /admin/settings/rss/save
let saveCustomFeed : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let saveCustomFeed : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
match! data.WebLog.findById ctx.WebLog.id with match! data.WebLog.FindById ctx.WebLog.id with
| Some webLog -> | Some webLog ->
let! model = ctx.BindFormAsync<EditCustomFeedModel> () let! model = ctx.BindFormAsync<EditCustomFeedModel> ()
let theFeed = let theFeed =
match model.id with match model.Id with
| "new" -> Some { CustomFeed.empty with id = CustomFeedId.create () } | "new" -> Some { CustomFeed.empty with id = CustomFeedId.create () }
| _ -> webLog.rss.customFeeds |> List.tryFind (fun it -> CustomFeedId.toString it.id = model.id) | _ -> webLog.rss.customFeeds |> List.tryFind (fun it -> CustomFeedId.toString it.id = model.Id)
match theFeed with match theFeed with
| Some feed -> | Some feed ->
let feeds = model.updateFeed feed :: (webLog.rss.customFeeds |> List.filter (fun it -> it.id <> feed.id)) let feeds = model.updateFeed feed :: (webLog.rss.customFeeds |> List.filter (fun it -> it.id <> feed.id))
let webLog = { webLog with rss = { webLog.rss with customFeeds = feeds } } let webLog = { webLog with rss = { webLog.rss with customFeeds = feeds } }
do! data.WebLog.updateRssOptions webLog do! data.WebLog.UpdateRssOptions webLog
WebLogCache.set webLog WebLogCache.set webLog
do! addMessage ctx { do! addMessage ctx {
UserMessage.success with UserMessage.success with
message = $"""Successfully {if model.id = "new" then "add" else "sav"}ed custom feed""" Message = $"""Successfully {if model.Id = "new" then "add" else "sav"}ed custom feed"""
} }
return! redirectToGet $"admin/settings/rss/{CustomFeedId.toString feed.id}/edit" next ctx return! redirectToGet $"admin/settings/rss/{CustomFeedId.toString feed.id}/edit" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
@ -501,7 +499,7 @@ let saveCustomFeed : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
// POST /admin/settings/rss/{id}/delete // POST /admin/settings/rss/{id}/delete
let deleteCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let deleteCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
match! data.WebLog.findById ctx.WebLog.id with match! data.WebLog.FindById ctx.WebLog.id with
| Some webLog -> | Some webLog ->
let customId = CustomFeedId feedId let customId = CustomFeedId feedId
if webLog.rss.customFeeds |> List.exists (fun f -> f.id = customId) then if webLog.rss.customFeeds |> List.exists (fun f -> f.id = customId) then
@ -512,11 +510,11 @@ let deleteCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun ne
customFeeds = webLog.rss.customFeeds |> List.filter (fun f -> f.id <> customId) customFeeds = webLog.rss.customFeeds |> List.filter (fun f -> f.id <> customId)
} }
} }
do! data.WebLog.updateRssOptions webLog do! data.WebLog.UpdateRssOptions webLog
WebLogCache.set webLog WebLogCache.set webLog
do! addMessage ctx { UserMessage.success with message = "Custom feed deleted successfully" } do! addMessage ctx { UserMessage.success with Message = "Custom feed deleted successfully" }
else else
do! addMessage ctx { UserMessage.warning with message = "Custom feed not found; no action taken" } do! addMessage ctx { UserMessage.warning with Message = "Custom feed not found; no action taken" }
return! redirectToGet "admin/settings/rss" next ctx return! redirectToGet "admin/settings/rss" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }

View File

@ -125,9 +125,9 @@ let messagesToHeaders (messages : UserMessage array) : HttpHandler =
yield! yield!
messages messages
|> Array.map (fun m -> |> Array.map (fun m ->
match m.detail with match m.Detail with
| Some detail -> $"{m.level}|||{m.message}|||{detail}" | Some detail -> $"{m.Level}|||{m.Message}|||{detail}"
| None -> $"{m.level}|||{m.message}" | None -> $"{m.Level}|||{m.Message}"
|> setHttpHeader "X-Message") |> setHttpHeader "X-Message")
withHxNoPushUrl withHxNoPushUrl
} }
@ -184,7 +184,7 @@ module Error =
if isHtmx ctx then if isHtmx ctx then
let messages = [| let messages = [|
{ UserMessage.error with { UserMessage.error with
message = $"You are not authorized to access the URL {ctx.Request.Path.Value}" Message = $"You are not authorized to access the URL {ctx.Request.Path.Value}"
} }
|] |]
(messagesToHeaders messages >=> setStatusCode 401) earlyReturn ctx (messagesToHeaders messages >=> setStatusCode 401) earlyReturn ctx
@ -195,7 +195,7 @@ module Error =
handleContext (fun ctx -> handleContext (fun ctx ->
if isHtmx ctx then if isHtmx ctx then
let messages = [| let messages = [|
{ UserMessage.error with message = $"The URL {ctx.Request.Path.Value} was not found" } { UserMessage.error with Message = $"The URL {ctx.Request.Path.Value} was not found" }
|] |]
(messagesToHeaders messages >=> setStatusCode 404) earlyReturn ctx (messagesToHeaders messages >=> setStatusCode 404) earlyReturn ctx
else else
@ -216,7 +216,7 @@ let requireAccess level : HttpHandler = fun next ctx -> task {
| Some lvl -> | Some lvl ->
$"The page you tried to access requires {AccessLevel.toString level} privileges; your account only has {AccessLevel.toString lvl} privileges" $"The page you tried to access requires {AccessLevel.toString level} privileges; your account only has {AccessLevel.toString lvl} privileges"
| None -> "The page you tried to access required you to be logged on" | None -> "The page you tried to access required you to be logged on"
do! addMessage ctx { UserMessage.warning with message = message } do! addMessage ctx { UserMessage.warning with Message = message }
printfn "Added message to context" printfn "Added message to context"
do! commitSession ctx do! commitSession ctx
return! Error.notAuthorized next ctx return! Error.notAuthorized next ctx
@ -232,7 +232,7 @@ open MyWebLog.Data
/// Get the templates available for the current web log's theme (in a key/value pair list) /// Get the templates available for the current web log's theme (in a key/value pair list)
let templatesForTheme (ctx : HttpContext) (typ : string) = backgroundTask { let templatesForTheme (ctx : HttpContext) (typ : string) = backgroundTask {
match! ctx.Data.Theme.findByIdWithoutText (ThemeId ctx.WebLog.themePath) with match! ctx.Data.Theme.FindByIdWithoutText (ThemeId ctx.WebLog.themePath) with
| Some theme -> | Some theme ->
return seq { return seq {
KeyValuePair.Create ("", $"- Default (single-{typ}) -") KeyValuePair.Create ("", $"- Default (single-{typ}) -")
@ -251,7 +251,7 @@ let getAuthors (webLog : WebLog) (posts : Post list) (data : IData) =
posts posts
|> List.map (fun p -> p.authorId) |> List.map (fun p -> p.authorId)
|> List.distinct |> List.distinct
|> data.WebLogUser.findNames webLog.id |> data.WebLogUser.FindNames webLog.id
/// Get all tag mappings for a list of posts as metadata items /// Get all tag mappings for a list of posts as metadata items
let getTagMappings (webLog : WebLog) (posts : Post list) (data : IData) = let getTagMappings (webLog : WebLog) (posts : Post list) (data : IData) =
@ -259,17 +259,17 @@ let getTagMappings (webLog : WebLog) (posts : Post list) (data : IData) =
|> List.map (fun p -> p.tags) |> List.map (fun p -> p.tags)
|> List.concat |> List.concat
|> List.distinct |> List.distinct
|> fun tags -> data.TagMap.findMappingForTags tags webLog.id |> fun tags -> data.TagMap.FindMappingForTags tags webLog.id
/// Get all category IDs for the given slug (includes owned subcategories) /// Get all category IDs for the given slug (includes owned subcategories)
let getCategoryIds slug ctx = let getCategoryIds slug ctx =
let allCats = CategoryCache.get ctx let allCats = CategoryCache.get ctx
let cat = allCats |> Array.find (fun cat -> cat.slug = slug) let cat = allCats |> Array.find (fun cat -> cat.Slug = slug)
// Category pages include posts in subcategories // Category pages include posts in subcategories
allCats allCats
|> Seq.ofArray |> Seq.ofArray
|> Seq.filter (fun c -> c.id = cat.id || Array.contains cat.name c.parentNames) |> Seq.filter (fun c -> c.Id = cat.Id || Array.contains cat.Name c.ParentNames)
|> Seq.map (fun c -> CategoryId c.id) |> Seq.map (fun c -> CategoryId c.Id)
|> List.ofSeq |> List.ofSeq
open System open System

View File

@ -9,7 +9,7 @@ open MyWebLog.ViewModels
// GET /admin/pages // GET /admin/pages
// GET /admin/pages/page/{pageNbr} // GET /admin/pages/page/{pageNbr}
let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task { let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let! pages = ctx.Data.Page.findPageOfPages ctx.WebLog.id pageNbr let! pages = ctx.Data.Page.FindPageOfPages ctx.WebLog.id pageNbr
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
page_title = "Pages" page_title = "Pages"
@ -28,7 +28,7 @@ let edit pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match pgId with match pgId with
| "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new"; authorId = ctx.UserId }) | "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new"; authorId = ctx.UserId })
| _ -> | _ ->
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.id with
| Some page -> return Some ("Edit Page", page) | Some page -> return Some ("Edit Page", page)
| None -> return None | None -> return None
} }
@ -41,7 +41,7 @@ let edit pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
page_title = title page_title = title
csrf = ctx.CsrfTokenSet csrf = ctx.CsrfTokenSet
model = model model = model
metadata = Array.zip model.metaNames model.metaValues metadata = Array.zip model.MetaNames model.MetaValues
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |]) |> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
templates = templates templates = templates
|} |}
@ -52,17 +52,17 @@ let edit pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
// POST /admin/page/{id}/delete // POST /admin/page/{id}/delete
let delete pgId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let delete pgId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
match! ctx.Data.Page.delete (PageId pgId) ctx.WebLog.id with match! ctx.Data.Page.Delete (PageId pgId) ctx.WebLog.id with
| true -> | true ->
do! PageListCache.update ctx do! PageListCache.update ctx
do! addMessage ctx { UserMessage.success with message = "Page deleted successfully" } do! addMessage ctx { UserMessage.success with Message = "Page deleted successfully" }
| false -> do! addMessage ctx { UserMessage.error with message = "Page not found; nothing deleted" } | false -> do! addMessage ctx { UserMessage.error with Message = "Page not found; nothing deleted" }
return! redirectToGet "admin/pages" next ctx return! redirectToGet "admin/pages" next ctx
} }
// GET /admin/page/{id}/permalinks // GET /admin/page/{id}/permalinks
let editPermalinks pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task { let editPermalinks pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.id with
| Some pg when canEdit pg.authorId ctx -> | Some pg when canEdit pg.authorId ctx ->
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
@ -78,14 +78,14 @@ let editPermalinks pgId : HttpHandler = requireAccess Author >=> fun next ctx ->
// POST /admin/page/permalinks // POST /admin/page/permalinks
let savePermalinks : HttpHandler = requireAccess Author >=> fun next ctx -> task { let savePermalinks : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let! model = ctx.BindFormAsync<ManagePermalinksModel> () let! model = ctx.BindFormAsync<ManagePermalinksModel> ()
let pageId = PageId model.id let pageId = PageId model.Id
match! ctx.Data.Page.findById pageId ctx.WebLog.id with match! ctx.Data.Page.FindById pageId ctx.WebLog.id with
| Some pg when canEdit pg.authorId ctx -> | Some pg when canEdit pg.authorId ctx ->
let links = model.prior |> Array.map Permalink |> List.ofArray let links = model.Prior |> Array.map Permalink |> List.ofArray
match! ctx.Data.Page.updatePriorPermalinks pageId ctx.WebLog.id links with match! ctx.Data.Page.UpdatePriorPermalinks pageId ctx.WebLog.id links with
| true -> | true ->
do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" } do! addMessage ctx { UserMessage.success with Message = "Page permalinks saved successfully" }
return! redirectToGet $"admin/page/{model.id}/permalinks" next ctx return! redirectToGet $"admin/page/{model.Id}/permalinks" next ctx
| false -> return! Error.notFound next ctx | false -> return! Error.notFound next ctx
| Some _ -> return! Error.notAuthorized next ctx | Some _ -> return! Error.notAuthorized next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
@ -93,7 +93,7 @@ let savePermalinks : HttpHandler = requireAccess Author >=> fun next ctx -> task
// GET /admin/page/{id}/revisions // GET /admin/page/{id}/revisions
let editRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task { let editRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.id with
| Some pg when canEdit pg.authorId ctx -> | Some pg when canEdit pg.authorId ctx ->
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
@ -109,10 +109,10 @@ let editRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx ->
// GET /admin/page/{id}/revisions/purge // GET /admin/page/{id}/revisions/purge
let purgeRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task { let purgeRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
match! data.Page.findFullById (PageId pgId) ctx.WebLog.id with match! data.Page.FindFullById (PageId pgId) ctx.WebLog.id with
| Some pg -> | Some pg ->
do! data.Page.update { pg with revisions = [ List.head pg.revisions ] } do! data.Page.Update { pg with revisions = [ List.head pg.revisions ] }
do! addMessage ctx { UserMessage.success with message = "Prior revisions purged successfully" } do! addMessage ctx { UserMessage.success with Message = "Prior revisions purged successfully" }
return! redirectToGet $"admin/page/{pgId}/revisions" next ctx return! redirectToGet $"admin/page/{pgId}/revisions" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
@ -121,7 +121,7 @@ open Microsoft.AspNetCore.Http
/// Find the page and the requested revision /// Find the page and the requested revision
let private findPageRevision pgId revDate (ctx : HttpContext) = task { let private findPageRevision pgId revDate (ctx : HttpContext) = task {
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.id with
| Some pg -> | Some pg ->
let asOf = parseToUtc revDate let asOf = parseToUtc revDate
return Some pg, pg.revisions |> List.tryFind (fun r -> r.asOf = asOf) return Some pg, pg.revisions |> List.tryFind (fun r -> r.asOf = asOf)
@ -148,12 +148,12 @@ open System
let restoreRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task { let restoreRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! findPageRevision pgId revDate ctx with match! findPageRevision pgId revDate ctx with
| Some pg, Some rev when canEdit pg.authorId ctx -> | Some pg, Some rev when canEdit pg.authorId ctx ->
do! ctx.Data.Page.update do! ctx.Data.Page.Update
{ pg with { pg with
revisions = { rev with asOf = DateTime.UtcNow } revisions = { rev with asOf = DateTime.UtcNow }
:: (pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf)) :: (pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf))
} }
do! addMessage ctx { UserMessage.success with message = "Revision restored successfully" } do! addMessage ctx { UserMessage.success with Message = "Revision restored successfully" }
return! redirectToGet $"admin/page/{pgId}/revisions" next ctx return! redirectToGet $"admin/page/{pgId}/revisions" next ctx
| Some _, Some _ -> return! Error.notAuthorized next ctx | Some _, Some _ -> return! Error.notAuthorized next ctx
| None, _ | None, _
@ -164,52 +164,54 @@ let restoreRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun
let deleteRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task { let deleteRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! findPageRevision pgId revDate ctx with match! findPageRevision pgId revDate ctx with
| Some pg, Some rev when canEdit pg.authorId ctx -> | Some pg, Some rev when canEdit pg.authorId ctx ->
do! ctx.Data.Page.update { pg with revisions = pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) } do! ctx.Data.Page.Update { pg with revisions = pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) }
do! addMessage ctx { UserMessage.success with message = "Revision deleted successfully" } do! addMessage ctx { UserMessage.success with Message = "Revision deleted successfully" }
return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |}) return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |})
| Some _, Some _ -> return! Error.notAuthorized next ctx | Some _, Some _ -> return! Error.notAuthorized next ctx
| None, _ | None, _
| _, None -> return! Error.notFound next ctx | _, None -> return! Error.notFound next ctx
} }
#nowarn "3511" //#nowarn "3511"
open System.Threading.Tasks
// POST /admin/page/save // POST /admin/page/save
let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditPageModel> () let! model = ctx.BindFormAsync<EditPageModel> ()
let data = ctx.Data let data = ctx.Data
let now = DateTime.UtcNow let now = DateTime.UtcNow
let! pg = task { let pg =
match model.pageId with match model.PageId with
| "new" -> | "new" ->
return Some Task.FromResult (
{ Page.empty with Some
id = PageId.create () { Page.empty with
webLogId = ctx.WebLog.id id = PageId.create ()
authorId = ctx.UserId webLogId = ctx.WebLog.id
publishedOn = now authorId = ctx.UserId
} publishedOn = now
| pgId -> return! data.Page.findFullById (PageId pgId) ctx.WebLog.id })
} | pgId -> data.Page.FindFullById (PageId pgId) ctx.WebLog.id
match pg with match! pg with
| Some page when canEdit page.authorId ctx -> | Some page when canEdit page.authorId ctx ->
let updateList = page.showInPageList <> model.isShownInPageList let updateList = page.showInPageList <> model.IsShownInPageList
let revision = { asOf = now; text = MarkupText.parse $"{model.source}: {model.text}" } let revision = { asOf = now; text = MarkupText.parse $"{model.Source}: {model.Text}" }
// Detect a permalink change, and add the prior one to the prior list // Detect a permalink change, and add the prior one to the prior list
let page = let page =
match Permalink.toString page.permalink with match Permalink.toString page.permalink with
| "" -> page | "" -> page
| link when link = model.permalink -> page | link when link = model.Permalink -> page
| _ -> { page with priorPermalinks = page.permalink :: page.priorPermalinks } | _ -> { page with priorPermalinks = page.permalink :: page.priorPermalinks }
let page = let page =
{ page with { page with
title = model.title title = model.Title
permalink = Permalink model.permalink permalink = Permalink model.Permalink
updatedOn = now updatedOn = now
showInPageList = model.isShownInPageList showInPageList = model.IsShownInPageList
template = match model.template with "" -> None | tmpl -> Some tmpl template = match model.Template with "" -> None | tmpl -> Some tmpl
text = MarkupText.toHtml revision.text text = MarkupText.toHtml revision.text
metadata = Seq.zip model.metaNames model.metaValues metadata = Seq.zip model.MetaNames model.MetaValues
|> Seq.filter (fun it -> fst it > "") |> Seq.filter (fun it -> fst it > "")
|> Seq.map (fun it -> { name = fst it; value = snd it }) |> Seq.map (fun it -> { name = fst it; value = snd it })
|> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}") |> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}")
@ -218,9 +220,9 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
| Some r when r.text = revision.text -> page.revisions | Some r when r.text = revision.text -> page.revisions
| _ -> revision :: page.revisions | _ -> revision :: page.revisions
} }
do! (if model.pageId = "new" then data.Page.add else data.Page.update) page do! (if model.PageId = "new" then data.Page.Add else data.Page.Update) page
if updateList then do! PageListCache.update ctx if updateList then do! PageListCache.update ctx
do! addMessage ctx { UserMessage.success with message = "Page saved successfully" } do! addMessage ctx { UserMessage.success with Message = "Page saved successfully" }
return! redirectToGet $"admin/page/{PageId.toString page.id}/edit" next ctx return! redirectToGet $"admin/page/{PageId.toString page.id}/edit" next ctx
| Some _ -> return! Error.notAuthorized next ctx | Some _ -> return! Error.notAuthorized next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx

View File

@ -16,8 +16,7 @@ let private parseSlugAndPage webLog (slugAndPage : string seq) =
|| (webLog.rss.tagEnabled && fullPath.StartsWith "/tag/" )) || (webLog.rss.tagEnabled && fullPath.StartsWith "/tag/" ))
&& slugPath.EndsWith feedName then && slugPath.EndsWith feedName then
notBlank (slugPath.Replace(feedName, "").Split "/"), true notBlank (slugPath.Replace(feedName, "").Split "/"), true
else else notBlank (slugPath.Split "/"), false
notBlank (slugPath.Split "/"), false
let pageIdx = Array.IndexOf (slugs, "page") let pageIdx = Array.IndexOf (slugs, "page")
let pageNbr = let pageNbr =
match pageIdx with match pageIdx with
@ -56,7 +55,7 @@ let preparePostList webLog posts listType (url : string) pageNbr perPage ctx (da
| SinglePost -> | SinglePost ->
let post = List.head posts let post = List.head posts
let dateTime = defaultArg post.publishedOn post.updatedOn let dateTime = defaultArg post.publishedOn post.updatedOn
data.Post.findSurroundingPosts webLog.id dateTime data.Post.FindSurroundingPosts webLog.id dateTime
| _ -> Task.FromResult (None, None) | _ -> Task.FromResult (None, None)
let newerLink = let newerLink =
match listType, pageNbr with match listType, pageNbr with
@ -68,7 +67,7 @@ let preparePostList webLog posts listType (url : string) pageNbr perPage ctx (da
| CategoryList, _ -> relUrl $"category/{url}/page/{pageNbr - 1}" | CategoryList, _ -> relUrl $"category/{url}/page/{pageNbr - 1}"
| TagList, 2 -> relUrl $"tag/{url}/" | TagList, 2 -> relUrl $"tag/{url}/"
| TagList, _ -> relUrl $"tag/{url}/page/{pageNbr - 1}" | TagList, _ -> relUrl $"tag/{url}/page/{pageNbr - 1}"
| AdminList, 2 -> relUrl "admin/posts" | AdminList, 2 -> relUrl "admin/posts"
| AdminList, _ -> relUrl $"admin/posts/page/{pageNbr - 1}" | AdminList, _ -> relUrl $"admin/posts/page/{pageNbr - 1}"
let olderLink = let olderLink =
match listType, List.length posts > perPage with match listType, List.length posts > perPage with
@ -79,13 +78,13 @@ let preparePostList webLog posts listType (url : string) pageNbr perPage ctx (da
| TagList, true -> relUrl $"tag/{url}/page/{pageNbr + 1}" | TagList, true -> relUrl $"tag/{url}/page/{pageNbr + 1}"
| AdminList, true -> relUrl $"admin/posts/page/{pageNbr + 1}" | AdminList, true -> relUrl $"admin/posts/page/{pageNbr + 1}"
let model = let model =
{ posts = postItems { Posts = postItems
authors = authors Authors = authors
subtitle = None Subtitle = None
newerLink = newerLink NewerLink = newerLink
newerName = newerPost |> Option.map (fun p -> p.title) NewerName = newerPost |> Option.map (fun p -> p.title)
olderLink = olderLink OlderLink = olderLink
olderName = olderPost |> Option.map (fun p -> p.title) OlderName = olderPost |> Option.map (fun p -> p.title)
} }
return Hash.FromAnonymousObject {| return Hash.FromAnonymousObject {|
model = model model = model
@ -101,7 +100,7 @@ open Giraffe
let pageOfPosts pageNbr : HttpHandler = fun next ctx -> task { let pageOfPosts pageNbr : HttpHandler = fun next ctx -> task {
let count = ctx.WebLog.postsPerPage let count = ctx.WebLog.postsPerPage
let data = ctx.Data let data = ctx.Data
let! posts = data.Post.findPageOfPublishedPosts ctx.WebLog.id pageNbr count let! posts = data.Post.FindPageOfPublishedPosts ctx.WebLog.id pageNbr count
let! hash = preparePostList ctx.WebLog posts PostList "" pageNbr count ctx data let! hash = preparePostList ctx.WebLog posts PostList "" pageNbr count ctx data
let title = let title =
match pageNbr, ctx.WebLog.defaultPage with match pageNbr, ctx.WebLog.defaultPage with
@ -124,23 +123,24 @@ let pageOfCategorizedPosts slugAndPage : HttpHandler = fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
match parseSlugAndPage webLog slugAndPage with match parseSlugAndPage webLog slugAndPage with
| Some pageNbr, slug, isFeed -> | Some pageNbr, slug, isFeed ->
match CategoryCache.get ctx |> Array.tryFind (fun cat -> cat.slug = slug) with match CategoryCache.get ctx |> Array.tryFind (fun cat -> cat.Slug = slug) with
| Some cat when isFeed -> | Some cat when isFeed ->
return! Feed.generate (Feed.CategoryFeed ((CategoryId cat.id), $"category/{slug}/{webLog.rss.feedName}")) return! Feed.generate (Feed.CategoryFeed ((CategoryId cat.Id), $"category/{slug}/{webLog.rss.feedName}"))
(defaultArg webLog.rss.itemsInFeed webLog.postsPerPage) next ctx (defaultArg webLog.rss.itemsInFeed webLog.postsPerPage) next ctx
| Some cat -> | Some cat ->
// Category pages include posts in subcategories // Category pages include posts in subcategories
match! data.Post.findPageOfCategorizedPosts webLog.id (getCategoryIds slug ctx) pageNbr webLog.postsPerPage match! data.Post.FindPageOfCategorizedPosts webLog.id (getCategoryIds slug ctx) pageNbr webLog.postsPerPage
with with
| posts when List.length posts > 0 -> | posts when List.length posts > 0 ->
let! hash = preparePostList webLog posts CategoryList cat.slug pageNbr webLog.postsPerPage ctx data let! hash = preparePostList webLog posts CategoryList cat.Slug pageNbr webLog.postsPerPage ctx data
let pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" let pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>"""
hash.Add ("page_title", $"{cat.name}: Category Archive{pgTitle}") return!
hash.Add ("subtitle", defaultArg cat.description "") addToHash "page_title" $"{cat.Name}: Category Archive{pgTitle}" hash
hash.Add ("is_category", true) |> addToHash "subtitle" (defaultArg cat.Description "")
hash.Add ("is_category_home", (pageNbr = 1)) |> addToHash "is_category" true
hash.Add ("slug", slug) |> addToHash "is_category_home" (pageNbr = 1)
return! themedView "index" next ctx hash |> addToHash "slug" slug
|> themedView "index" next ctx
| _ -> return! Error.notFound next ctx | _ -> return! Error.notFound next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
| None, _, _ -> return! Error.notFound next ctx | None, _, _ -> return! Error.notFound next ctx
@ -157,7 +157,7 @@ let pageOfTaggedPosts slugAndPage : HttpHandler = fun next ctx -> task {
| Some pageNbr, rawTag, isFeed -> | Some pageNbr, rawTag, isFeed ->
let urlTag = HttpUtility.UrlDecode rawTag let urlTag = HttpUtility.UrlDecode rawTag
let! tag = backgroundTask { let! tag = backgroundTask {
match! data.TagMap.findByUrlValue urlTag webLog.id with match! data.TagMap.FindByUrlValue urlTag webLog.id with
| Some m -> return m.tag | Some m -> return m.tag
| None -> return urlTag | None -> return urlTag
} }
@ -165,19 +165,20 @@ let pageOfTaggedPosts slugAndPage : HttpHandler = fun next ctx -> task {
return! Feed.generate (Feed.TagFeed (tag, $"tag/{rawTag}/{webLog.rss.feedName}")) return! Feed.generate (Feed.TagFeed (tag, $"tag/{rawTag}/{webLog.rss.feedName}"))
(defaultArg webLog.rss.itemsInFeed webLog.postsPerPage) next ctx (defaultArg webLog.rss.itemsInFeed webLog.postsPerPage) next ctx
else else
match! data.Post.findPageOfTaggedPosts webLog.id tag pageNbr webLog.postsPerPage with match! data.Post.FindPageOfTaggedPosts webLog.id tag pageNbr webLog.postsPerPage with
| posts when List.length posts > 0 -> | posts when List.length posts > 0 ->
let! hash = preparePostList webLog posts TagList rawTag pageNbr webLog.postsPerPage ctx data let! hash = preparePostList webLog posts TagList rawTag pageNbr webLog.postsPerPage ctx data
let pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" let pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>"""
hash.Add ("page_title", $"Posts Tagged &ldquo;{tag}&rdquo;{pgTitle}") return!
hash.Add ("is_tag", true) addToHash "page_title" $"Posts Tagged &ldquo;{tag}&rdquo;{pgTitle}" hash
hash.Add ("is_tag_home", (pageNbr = 1)) |> addToHash "is_tag" true
hash.Add ("slug", rawTag) |> addToHash "is_tag_home" (pageNbr = 1)
return! themedView "index" next ctx hash |> addToHash "slug" rawTag
|> themedView "index" next ctx
// Other systems use hyphens for spaces; redirect if this is an old tag link // Other systems use hyphens for spaces; redirect if this is an old tag link
| _ -> | _ ->
let spacedTag = tag.Replace ("-", " ") let spacedTag = tag.Replace ("-", " ")
match! data.Post.findPageOfTaggedPosts webLog.id spacedTag pageNbr 1 with match! data.Post.FindPageOfTaggedPosts webLog.id spacedTag pageNbr 1 with
| posts when List.length posts > 0 -> | posts when List.length posts > 0 ->
let endUrl = if pageNbr = 1 then "" else $"page/{pageNbr}" let endUrl = if pageNbr = 1 then "" else $"page/{pageNbr}"
return! return!
@ -194,7 +195,7 @@ let home : HttpHandler = fun next ctx -> task {
match webLog.defaultPage with match webLog.defaultPage with
| "posts" -> return! pageOfPosts 1 next ctx | "posts" -> return! pageOfPosts 1 next ctx
| pageId -> | pageId ->
match! ctx.Data.Page.findById (PageId pageId) webLog.id with match! ctx.Data.Page.FindById (PageId pageId) webLog.id with
| Some page -> | Some page ->
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
@ -211,11 +212,12 @@ let home : HttpHandler = fun next ctx -> task {
// GET /admin/posts/page/{pageNbr} // GET /admin/posts/page/{pageNbr}
let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task { let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
let! posts = data.Post.findPageOfPosts ctx.WebLog.id pageNbr 25 let! posts = data.Post.FindPageOfPosts ctx.WebLog.id pageNbr 25
let! hash = preparePostList ctx.WebLog posts AdminList "" pageNbr 25 ctx data let! hash = preparePostList ctx.WebLog posts AdminList "" pageNbr 25 ctx data
hash.Add ("page_title", "Posts") return!
hash.Add ("csrf", ctx.CsrfTokenSet) addToHash "page_title" "Posts" hash
return! viewForTheme "admin" "post-list" next ctx hash |> addToHash "csrf" ctx.CsrfTokenSet
|> viewForTheme "admin" "post-list" next ctx
} }
// GET /admin/post/{id}/edit // GET /admin/post/{id}/edit
@ -225,13 +227,13 @@ let edit postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match postId with match postId with
| "new" -> return Some ("Write a New Post", { Post.empty with id = PostId "new" }) | "new" -> return Some ("Write a New Post", { Post.empty with id = PostId "new" })
| _ -> | _ ->
match! data.Post.findFullById (PostId postId) ctx.WebLog.id with match! data.Post.FindFullById (PostId postId) ctx.WebLog.id with
| Some post -> return Some ("Edit Post", post) | Some post -> return Some ("Edit Post", post)
| None -> return None | None -> return None
} }
match result with match result with
| Some (title, post) when canEdit post.authorId ctx -> | Some (title, post) when canEdit post.authorId ctx ->
let! cats = data.Category.findAllForView ctx.WebLog.id let! cats = data.Category.FindAllForView ctx.WebLog.id
let! templates = templatesForTheme ctx "post" let! templates = templatesForTheme ctx "post"
let model = EditPostModel.fromPost ctx.WebLog post let model = EditPostModel.fromPost ctx.WebLog post
return! return!
@ -239,7 +241,7 @@ let edit postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
page_title = title page_title = title
csrf = ctx.CsrfTokenSet csrf = ctx.CsrfTokenSet
model = model model = model
metadata = Array.zip model.metaNames model.metaValues metadata = Array.zip model.MetaNames model.MetaValues
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |]) |> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
templates = templates templates = templates
categories = cats categories = cats
@ -257,15 +259,15 @@ let edit postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
// POST /admin/post/{id}/delete // POST /admin/post/{id}/delete
let delete postId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let delete postId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
match! ctx.Data.Post.delete (PostId postId) ctx.WebLog.id with match! ctx.Data.Post.Delete (PostId postId) ctx.WebLog.id with
| true -> do! addMessage ctx { UserMessage.success with message = "Post deleted successfully" } | true -> do! addMessage ctx { UserMessage.success with Message = "Post deleted successfully" }
| false -> do! addMessage ctx { UserMessage.error with message = "Post not found; nothing deleted" } | false -> do! addMessage ctx { UserMessage.error with Message = "Post not found; nothing deleted" }
return! redirectToGet "admin/posts" next ctx return! redirectToGet "admin/posts" next ctx
} }
// GET /admin/post/{id}/permalinks // GET /admin/post/{id}/permalinks
let editPermalinks postId : HttpHandler = requireAccess Author >=> fun next ctx -> task { let editPermalinks postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.id with
| Some post when canEdit post.authorId ctx -> | Some post when canEdit post.authorId ctx ->
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
@ -281,14 +283,14 @@ let editPermalinks postId : HttpHandler = requireAccess Author >=> fun next ctx
// POST /admin/post/permalinks // POST /admin/post/permalinks
let savePermalinks : HttpHandler = requireAccess Author >=> fun next ctx -> task { let savePermalinks : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let! model = ctx.BindFormAsync<ManagePermalinksModel> () let! model = ctx.BindFormAsync<ManagePermalinksModel> ()
let postId = PostId model.id let postId = PostId model.Id
match! ctx.Data.Post.findById postId ctx.WebLog.id with match! ctx.Data.Post.FindById postId ctx.WebLog.id with
| Some post when canEdit post.authorId ctx -> | Some post when canEdit post.authorId ctx ->
let links = model.prior |> Array.map Permalink |> List.ofArray let links = model.Prior |> Array.map Permalink |> List.ofArray
match! ctx.Data.Post.updatePriorPermalinks (PostId model.id) ctx.WebLog.id links with match! ctx.Data.Post.UpdatePriorPermalinks postId ctx.WebLog.id links with
| true -> | true ->
do! addMessage ctx { UserMessage.success with message = "Post permalinks saved successfully" } do! addMessage ctx { UserMessage.success with Message = "Post permalinks saved successfully" }
return! redirectToGet $"admin/post/{model.id}/permalinks" next ctx return! redirectToGet $"admin/post/{model.Id}/permalinks" next ctx
| false -> return! Error.notFound next ctx | false -> return! Error.notFound next ctx
| Some _ -> return! Error.notAuthorized next ctx | Some _ -> return! Error.notAuthorized next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
@ -296,7 +298,7 @@ let savePermalinks : HttpHandler = requireAccess Author >=> fun next ctx -> task
// GET /admin/post/{id}/revisions // GET /admin/post/{id}/revisions
let editRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -> task { let editRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.id with
| Some post when canEdit post.authorId ctx -> | Some post when canEdit post.authorId ctx ->
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
@ -312,10 +314,10 @@ let editRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -
// GET /admin/post/{id}/revisions/purge // GET /admin/post/{id}/revisions/purge
let purgeRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -> task { let purgeRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
match! data.Post.findFullById (PostId postId) ctx.WebLog.id with match! data.Post.FindFullById (PostId postId) ctx.WebLog.id with
| Some post when canEdit post.authorId ctx -> | Some post when canEdit post.authorId ctx ->
do! data.Post.update { post with revisions = [ List.head post.revisions ] } do! data.Post.Update { post with revisions = [ List.head post.revisions ] }
do! addMessage ctx { UserMessage.success with message = "Prior revisions purged successfully" } do! addMessage ctx { UserMessage.success with Message = "Prior revisions purged successfully" }
return! redirectToGet $"admin/post/{postId}/revisions" next ctx return! redirectToGet $"admin/post/{postId}/revisions" next ctx
| Some _ -> return! Error.notAuthorized next ctx | Some _ -> return! Error.notAuthorized next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
@ -325,7 +327,7 @@ open Microsoft.AspNetCore.Http
/// Find the post and the requested revision /// Find the post and the requested revision
let private findPostRevision postId revDate (ctx : HttpContext) = task { let private findPostRevision postId revDate (ctx : HttpContext) = task {
match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.id with
| Some post -> | Some post ->
let asOf = parseToUtc revDate let asOf = parseToUtc revDate
return Some post, post.revisions |> List.tryFind (fun r -> r.asOf = asOf) return Some post, post.revisions |> List.tryFind (fun r -> r.asOf = asOf)
@ -350,12 +352,12 @@ let previewRevision (postId, revDate) : HttpHandler = requireAccess Author >=> f
let restoreRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task { let restoreRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! findPostRevision postId revDate ctx with match! findPostRevision postId revDate ctx with
| Some post, Some rev when canEdit post.authorId ctx -> | Some post, Some rev when canEdit post.authorId ctx ->
do! ctx.Data.Post.update do! ctx.Data.Post.Update
{ post with { post with
revisions = { rev with asOf = DateTime.UtcNow } revisions = { rev with asOf = DateTime.UtcNow }
:: (post.revisions |> List.filter (fun r -> r.asOf <> rev.asOf)) :: (post.revisions |> List.filter (fun r -> r.asOf <> rev.asOf))
} }
do! addMessage ctx { UserMessage.success with message = "Revision restored successfully" } do! addMessage ctx { UserMessage.success with Message = "Revision restored successfully" }
return! redirectToGet $"admin/post/{postId}/revisions" next ctx return! redirectToGet $"admin/post/{postId}/revisions" next ctx
| Some _, Some _ -> return! Error.notAuthorized next ctx | Some _, Some _ -> return! Error.notAuthorized next ctx
| None, _ | None, _
@ -366,64 +368,62 @@ let restoreRevision (postId, revDate) : HttpHandler = requireAccess Author >=> f
let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task { let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! findPostRevision postId revDate ctx with match! findPostRevision postId revDate ctx with
| Some post, Some rev when canEdit post.authorId ctx -> | Some post, Some rev when canEdit post.authorId ctx ->
do! ctx.Data.Post.update { post with revisions = post.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) } do! ctx.Data.Post.Update { post with revisions = post.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) }
do! addMessage ctx { UserMessage.success with message = "Revision deleted successfully" } do! addMessage ctx { UserMessage.success with Message = "Revision deleted successfully" }
return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |}) return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |})
| Some _, Some _ -> return! Error.notAuthorized next ctx | Some _, Some _ -> return! Error.notAuthorized next ctx
| None, _ | None, _
| _, None -> return! Error.notFound next ctx | _, None -> return! Error.notFound next ctx
} }
#nowarn "3511" //#nowarn "3511"
// POST /admin/post/save // POST /admin/post/save
let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditPostModel> () let! model = ctx.BindFormAsync<EditPostModel> ()
let data = ctx.Data let data = ctx.Data
let now = DateTime.UtcNow let now = DateTime.UtcNow
let! pst = task { let tryPost =
match model.postId with if model.PostId = "new" then
| "new" -> Task.FromResult (
return Some Some
{ Post.empty with { Post.empty with
id = PostId.create () id = PostId.create ()
webLogId = ctx.WebLog.id webLogId = ctx.WebLog.id
authorId = ctx.UserId authorId = ctx.UserId
} })
| postId -> return! data.Post.findFullById (PostId postId) ctx.WebLog.id else data.Post.FindFullById (PostId model.PostId) ctx.WebLog.id
} match! tryPost with
match pst with
| Some post when canEdit post.authorId ctx -> | Some post when canEdit post.authorId ctx ->
let revision = { asOf = now; text = MarkupText.parse $"{model.source}: {model.text}" } let priorCats = post.categoryIds
let revision = { asOf = now; text = MarkupText.parse $"{model.Source}: {model.Text}" }
// Detect a permalink change, and add the prior one to the prior list // Detect a permalink change, and add the prior one to the prior list
let post = let post =
match Permalink.toString post.permalink with match Permalink.toString post.permalink with
| "" -> post | "" -> post
| link when link = model.permalink -> post | link when link = model.Permalink -> post
| _ -> { post with priorPermalinks = post.permalink :: post.priorPermalinks } | _ -> { post with priorPermalinks = post.permalink :: post.priorPermalinks }
let post = model.updatePost post revision now let post = model.updatePost post revision now
let post = let post =
match model.setPublished with if model.SetPublished then
| true -> let dt = parseToUtc (model.PubOverride.Value.ToString "o")
let dt = parseToUtc (model.pubOverride.Value.ToString "o") if model.SetUpdated then
match model.setUpdated with
| true ->
{ post with { post with
publishedOn = Some dt publishedOn = Some dt
updatedOn = dt updatedOn = dt
revisions = [ { (List.head post.revisions) with asOf = dt } ] revisions = [ { (List.head post.revisions) with asOf = dt } ]
} }
| false -> { post with publishedOn = Some dt } else { post with publishedOn = Some dt }
| false -> post else post
do! (if model.postId = "new" then data.Post.add else data.Post.update) post do! (if model.PostId = "new" then data.Post.Add else data.Post.Update) post
// If the post was published or its categories changed, refresh the category cache // If the post was published or its categories changed, refresh the category cache
if model.doPublish if model.DoPublish
|| not (pst.Value.categoryIds || not (priorCats
|> List.append post.categoryIds |> List.append post.categoryIds
|> List.distinct |> List.distinct
|> List.length = List.length pst.Value.categoryIds) then |> List.length = List.length priorCats) then
do! CategoryCache.update ctx do! CategoryCache.update ctx
do! addMessage ctx { UserMessage.success with message = "Post saved successfully" } do! addMessage ctx { UserMessage.success with Message = "Post saved successfully" }
return! redirectToGet $"admin/post/{PostId.toString post.id}/edit" next ctx return! redirectToGet $"admin/post/{PostId.toString post.id}/edit" next ctx
| Some _ -> return! Error.notAuthorized next ctx | Some _ -> return! Error.notAuthorized next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx

View File

@ -27,7 +27,7 @@ module CatchAll =
if textLink = "" then yield redirectTo true (WebLog.relativeUrl webLog Permalink.empty) if textLink = "" then yield redirectTo true (WebLog.relativeUrl webLog Permalink.empty)
let permalink = Permalink (textLink.Substring 1) let permalink = Permalink (textLink.Substring 1)
// Current post // Current post
match data.Post.findByPermalink permalink webLog.id |> await with match data.Post.FindByPermalink permalink webLog.id |> await with
| Some post -> | Some post ->
debug (fun () -> "Found post by permalink") debug (fun () -> "Found post by permalink")
let model = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 ctx data |> await let model = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 ctx data |> await
@ -35,7 +35,7 @@ module CatchAll =
yield fun next ctx -> themedView (defaultArg post.template "single-post") next ctx model yield fun next ctx -> themedView (defaultArg post.template "single-post") next ctx model
| None -> () | None -> ()
// Current page // Current page
match data.Page.findByPermalink permalink webLog.id |> await with match data.Page.FindByPermalink permalink webLog.id |> await with
| Some page -> | Some page ->
debug (fun () -> "Found page by permalink") debug (fun () -> "Found page by permalink")
yield fun next ctx -> yield fun next ctx ->
@ -56,25 +56,25 @@ module CatchAll =
// Post differing only by trailing slash // Post differing only by trailing slash
let altLink = let altLink =
Permalink (if textLink.EndsWith "/" then textLink[1..textLink.Length - 2] else $"{textLink[1..]}/") Permalink (if textLink.EndsWith "/" then textLink[1..textLink.Length - 2] else $"{textLink[1..]}/")
match data.Post.findByPermalink altLink webLog.id |> await with match data.Post.FindByPermalink altLink webLog.id |> await with
| Some post -> | Some post ->
debug (fun () -> "Found post by trailing-slash-agnostic permalink") debug (fun () -> "Found post by trailing-slash-agnostic permalink")
yield redirectTo true (WebLog.relativeUrl webLog post.permalink) yield redirectTo true (WebLog.relativeUrl webLog post.permalink)
| None -> () | None -> ()
// Page differing only by trailing slash // Page differing only by trailing slash
match data.Page.findByPermalink altLink webLog.id |> await with match data.Page.FindByPermalink altLink webLog.id |> await with
| Some page -> | Some page ->
debug (fun () -> "Found page by trailing-slash-agnostic permalink") debug (fun () -> "Found page by trailing-slash-agnostic permalink")
yield redirectTo true (WebLog.relativeUrl webLog page.permalink) yield redirectTo true (WebLog.relativeUrl webLog page.permalink)
| None -> () | None -> ()
// Prior post // Prior post
match data.Post.findCurrentPermalink [ permalink; altLink ] webLog.id |> await with match data.Post.FindCurrentPermalink [ permalink; altLink ] webLog.id |> await with
| Some link -> | Some link ->
debug (fun () -> "Found post by prior permalink") debug (fun () -> "Found post by prior permalink")
yield redirectTo true (WebLog.relativeUrl webLog link) yield redirectTo true (WebLog.relativeUrl webLog link)
| None -> () | None -> ()
// Prior page // Prior page
match data.Page.findCurrentPermalink [ permalink; altLink ] webLog.id |> await with match data.Page.FindCurrentPermalink [ permalink; altLink ] webLog.id |> await with
| Some link -> | Some link ->
debug (fun () -> "Found page by prior permalink") debug (fun () -> "Found page by prior permalink")
yield redirectTo true (WebLog.relativeUrl webLog link) yield redirectTo true (WebLog.relativeUrl webLog link)
@ -83,11 +83,8 @@ module CatchAll =
} }
// GET {all-of-the-above} // GET {all-of-the-above}
let route : HttpHandler = fun next ctx -> task { let route : HttpHandler = fun next ctx ->
match deriveAction ctx |> Seq.tryHead with match deriveAction ctx |> Seq.tryHead with Some handler -> handler next ctx | None -> Error.notFound next ctx
| Some handler -> return! handler next ctx
| None -> return! Error.notFound next ctx
}
/// Serve theme assets /// Serve theme assets
@ -96,7 +93,7 @@ module Asset =
// GET /theme/{theme}/{**path} // GET /theme/{theme}/{**path}
let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task { let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task {
let path = urlParts |> Seq.skip 1 |> Seq.head let path = urlParts |> Seq.skip 1 |> Seq.head
match! ctx.Data.ThemeAsset.findById (ThemeAssetId.ofString path) with match! ctx.Data.ThemeAsset.FindById (ThemeAssetId.ofString path) with
| Some asset -> | Some asset ->
match Upload.checkModified asset.updatedOn ctx with match Upload.checkModified asset.updatedOn ctx with
| Some threeOhFour -> return! threeOhFour next ctx | Some threeOhFour -> return! threeOhFour next ctx
@ -219,10 +216,10 @@ let routerWithPath extraPath : HttpHandler =
subRoute extraPath router subRoute extraPath router
/// Handler to apply Giraffe routing with a possible sub-route /// Handler to apply Giraffe routing with a possible sub-route
let handleRoute : HttpHandler = fun next ctx -> task { let handleRoute : HttpHandler = fun next ctx ->
let _, extraPath = WebLog.hostAndPath ctx.WebLog let _, extraPath = WebLog.hostAndPath ctx.WebLog
return! (if extraPath = "" then router else routerWithPath extraPath) next ctx (if extraPath = "" then router else routerWithPath extraPath) next ctx
}
open Giraffe.EndpointRouting open Giraffe.EndpointRouting

View File

@ -45,13 +45,13 @@ let deriveMimeType path =
match mimeMap.TryGetContentType path with true, typ -> typ | false, _ -> "application/octet-stream" match mimeMap.TryGetContentType path with true, typ -> typ | false, _ -> "application/octet-stream"
/// Send a file, caching the response for 30 days /// Send a file, caching the response for 30 days
let sendFile updatedOn path (data : byte[]) : HttpHandler = fun next ctx -> task { let sendFile updatedOn path (data : byte[]) : HttpHandler = fun next ctx ->
let headers = ResponseHeaders ctx.Response.Headers let headers = ResponseHeaders ctx.Response.Headers
headers.ContentType <- (deriveMimeType >> MediaTypeHeaderValue) path headers.ContentType <- (deriveMimeType >> MediaTypeHeaderValue) path
headers.CacheControl <- cacheForThirtyDays headers.CacheControl <- cacheForThirtyDays
let stream = new MemoryStream (data) let stream = new MemoryStream (data)
return! streamData true stream None (Some (DateTimeOffset updatedOn)) next ctx streamData true stream None (Some (DateTimeOffset updatedOn)) next ctx
}
// GET /upload/{web-log-slug}/{**path} // GET /upload/{web-log-slug}/{**path}
let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task { let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task {
@ -65,7 +65,7 @@ let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task {
return! streamFile true fileName None None next ctx return! streamFile true fileName None None next ctx
else else
let path = String.Join ('/', Array.skip 1 parts) let path = String.Join ('/', Array.skip 1 parts)
match! ctx.Data.Upload.findByPath path webLog.id with match! ctx.Data.Upload.FindByPath path webLog.id with
| Some upload -> | Some upload ->
match checkModified upload.updatedOn ctx with match checkModified upload.updatedOn ctx with
| Some threeOhFour -> return! threeOhFour next ctx | Some threeOhFour -> return! threeOhFour next ctx
@ -87,7 +87,7 @@ let makeSlug it = ((Regex """\s+""").Replace ((Regex "[^A-z0-9 ]").Replace (it,
// GET /admin/uploads // GET /admin/uploads
let list : HttpHandler = requireAccess Author >=> fun next ctx -> task { let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let webLog = ctx.WebLog let webLog = ctx.WebLog
let! dbUploads = ctx.Data.Upload.findByWebLog webLog.id let! dbUploads = ctx.Data.Upload.FindByWebLog webLog.id
let diskUploads = let diskUploads =
let path = Path.Combine (uploadDir, webLog.slug) let path = Path.Combine (uploadDir, webLog.slug)
try try
@ -98,11 +98,11 @@ let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match File.GetCreationTime (Path.Combine (path, file)) with match File.GetCreationTime (Path.Combine (path, file)) with
| dt when dt > DateTime.UnixEpoch -> Some dt | dt when dt > DateTime.UnixEpoch -> Some dt
| _ -> None | _ -> None
{ DisplayUpload.id = "" { DisplayUpload.Id = ""
name = name Name = name
path = file.Replace($"{path}{slash}", "").Replace(name, "").Replace (slash, '/') Path = file.Replace($"{path}{slash}", "").Replace(name, "").Replace (slash, '/')
updatedOn = create UpdatedOn = create
source = UploadDestination.toString Disk Source = UploadDestination.toString Disk
}) })
|> List.ofSeq |> List.ofSeq
with with
@ -114,7 +114,7 @@ let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
dbUploads dbUploads
|> List.map (DisplayUpload.fromUpload webLog Database) |> List.map (DisplayUpload.fromUpload webLog Database)
|> List.append diskUploads |> List.append diskUploads
|> List.sortByDescending (fun file -> file.updatedOn, file.path) |> List.sortByDescending (fun file -> file.UpdatedOn, file.Path)
return! return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
@ -126,15 +126,14 @@ let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
} }
// GET /admin/upload/new // GET /admin/upload/new
let showNew : HttpHandler = requireAccess Author >=> fun next ctx -> task { let showNew : HttpHandler = requireAccess Author >=> fun next ctx ->
return! Hash.FromAnonymousObject {|
Hash.FromAnonymousObject {| page_title = "Upload a File"
page_title = "Upload a File" csrf = ctx.CsrfTokenSet
csrf = ctx.CsrfTokenSet destination = UploadDestination.toString ctx.WebLog.uploads
destination = UploadDestination.toString ctx.WebLog.uploads |}
|} |> viewForTheme "admin" "upload-new" next ctx
|> viewForTheme "admin" "upload-new" next ctx
}
/// Redirect to the upload list /// Redirect to the upload list
let showUploads : HttpHandler = let showUploads : HttpHandler =
@ -151,7 +150,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let month = localNow.ToString "MM" let month = localNow.ToString "MM"
let! form = ctx.BindFormAsync<UploadFileModel> () let! form = ctx.BindFormAsync<UploadFileModel> ()
match UploadDestination.parse form.destination with match UploadDestination.parse form.Destination with
| Database -> | Database ->
use stream = new MemoryStream () use stream = new MemoryStream ()
do! upload.CopyToAsync stream do! upload.CopyToAsync stream
@ -162,14 +161,14 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
updatedOn = DateTime.UtcNow updatedOn = DateTime.UtcNow
data = stream.ToArray () data = stream.ToArray ()
} }
do! ctx.Data.Upload.add file do! ctx.Data.Upload.Add file
| Disk -> | Disk ->
let fullPath = Path.Combine (uploadDir, ctx.WebLog.slug, year, month) let fullPath = Path.Combine (uploadDir, ctx.WebLog.slug, year, month)
let _ = Directory.CreateDirectory fullPath let _ = Directory.CreateDirectory fullPath
use stream = new FileStream (Path.Combine (fullPath, fileName), FileMode.Create) use stream = new FileStream (Path.Combine (fullPath, fileName), FileMode.Create)
do! upload.CopyToAsync stream do! upload.CopyToAsync stream
do! addMessage ctx { UserMessage.success with message = $"File uploaded to {form.destination} successfully" } do! addMessage ctx { UserMessage.success with Message = $"File uploaded to {form.Destination} successfully" }
return! showUploads next ctx return! showUploads next ctx
else else
return! RequestErrors.BAD_REQUEST "Bad request; no file present" next ctx return! RequestErrors.BAD_REQUEST "Bad request; no file present" next ctx
@ -177,9 +176,9 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
// POST /admin/upload/{id}/delete // POST /admin/upload/{id}/delete
let deleteFromDb upId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let deleteFromDb upId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
match! ctx.Data.Upload.delete (UploadId upId) ctx.WebLog.id with match! ctx.Data.Upload.Delete (UploadId upId) ctx.WebLog.id with
| Ok fileName -> | Ok fileName ->
do! addMessage ctx { UserMessage.success with message = $"{fileName} deleted successfully" } do! addMessage ctx { UserMessage.success with Message = $"{fileName} deleted successfully" }
return! showUploads next ctx return! showUploads next ctx
| Error _ -> return! Error.notFound next ctx | Error _ -> return! Error.notFound next ctx
} }
@ -193,8 +192,7 @@ let removeEmptyDirectories (webLog : WebLog) (filePath : string) =
if Directory.EnumerateFileSystemEntries fullPath |> Seq.isEmpty then if Directory.EnumerateFileSystemEntries fullPath |> Seq.isEmpty then
Directory.Delete fullPath Directory.Delete fullPath
path <- String.Join(slash, path.Split slash |> Array.rev |> Array.skip 1 |> Array.rev) path <- String.Join(slash, path.Split slash |> Array.rev |> Array.skip 1 |> Array.rev)
else else finished <- true
finished <- true
// POST /admin/upload/delete/{**path} // POST /admin/upload/delete/{**path}
let deleteFromDisk urlParts : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let deleteFromDisk urlParts : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
@ -203,8 +201,7 @@ let deleteFromDisk urlParts : HttpHandler = requireAccess WebLogAdmin >=> fun ne
if File.Exists path then if File.Exists path then
File.Delete path File.Delete path
removeEmptyDirectories ctx.WebLog filePath removeEmptyDirectories ctx.WebLog filePath
do! addMessage ctx { UserMessage.success with message = $"{filePath} deleted successfully" } do! addMessage ctx { UserMessage.success with Message = $"{filePath} deleted successfully" }
return! showUploads next ctx return! showUploads next ctx
else else return! Error.notFound next ctx
return! Error.notFound next ctx
} }

View File

@ -17,22 +17,18 @@ open MyWebLog
open MyWebLog.ViewModels open MyWebLog.ViewModels
// GET /user/log-on // GET /user/log-on
let logOn returnUrl : HttpHandler = fun next ctx -> task { let logOn returnUrl : HttpHandler = fun next ctx ->
let returnTo = let returnTo =
match returnUrl with match returnUrl with
| Some _ -> returnUrl | Some _ -> returnUrl
| None -> | None -> if ctx.Request.Query.ContainsKey "returnUrl" then Some ctx.Request.Query["returnUrl"].[0] else None
match ctx.Request.Query.ContainsKey "returnUrl" with Hash.FromAnonymousObject {|
| true -> Some ctx.Request.Query["returnUrl"].[0] page_title = "Log On"
| false -> None csrf = ctx.CsrfTokenSet
return! model = { LogOnModel.empty with ReturnTo = returnTo }
Hash.FromAnonymousObject {| |}
page_title = "Log On" |> viewForTheme "admin" "log-on" next ctx
csrf = ctx.CsrfTokenSet
model = { LogOnModel.empty with returnTo = returnTo }
|}
|> viewForTheme "admin" "log-on" next ctx
}
open System.Security.Claims open System.Security.Claims
open Microsoft.AspNetCore.Authentication open Microsoft.AspNetCore.Authentication
@ -41,8 +37,9 @@ open Microsoft.AspNetCore.Authentication.Cookies
// POST /user/log-on // POST /user/log-on
let doLogOn : HttpHandler = fun next ctx -> task { let doLogOn : HttpHandler = fun next ctx -> task {
let! model = ctx.BindFormAsync<LogOnModel> () let! model = ctx.BindFormAsync<LogOnModel> ()
match! ctx.Data.WebLogUser.findByEmail model.emailAddress ctx.WebLog.id with let data = ctx.Data
| Some user when user.passwordHash = hashedPassword model.password user.userName user.salt -> match! data.WebLogUser.FindByEmail model.EmailAddress ctx.WebLog.id with
| Some user when user.passwordHash = hashedPassword model.Password user.userName user.salt ->
let claims = seq { let claims = seq {
Claim (ClaimTypes.NameIdentifier, WebLogUserId.toString user.id) Claim (ClaimTypes.NameIdentifier, WebLogUserId.toString user.id)
Claim (ClaimTypes.Name, $"{user.firstName} {user.lastName}") Claim (ClaimTypes.Name, $"{user.firstName} {user.lastName}")
@ -53,34 +50,35 @@ let doLogOn : HttpHandler = fun next ctx -> task {
do! ctx.SignInAsync (identity.AuthenticationType, ClaimsPrincipal identity, do! ctx.SignInAsync (identity.AuthenticationType, ClaimsPrincipal identity,
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow)) AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
do! data.WebLogUser.SetLastSeen user.id user.webLogId
do! addMessage ctx do! addMessage ctx
{ UserMessage.success with message = $"Logged on successfully | Welcome to {ctx.WebLog.name}!" } { UserMessage.success with Message = $"Logged on successfully | Welcome to {ctx.WebLog.name}!" }
return! return!
match model.returnTo with match model.ReturnTo with
| Some url -> redirectTo false url next ctx | Some url -> redirectTo false url next ctx
| None -> redirectToGet "admin/dashboard" next ctx | None -> redirectToGet "admin/dashboard" next ctx
| _ -> | _ ->
do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" } do! addMessage ctx { UserMessage.error with Message = "Log on attempt unsuccessful" }
return! logOn model.returnTo next ctx return! logOn model.ReturnTo next ctx
} }
// GET /user/log-off // GET /user/log-off
let logOff : HttpHandler = fun next ctx -> task { let logOff : HttpHandler = fun next ctx -> task {
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
do! addMessage ctx { UserMessage.info with message = "Log off successful" } do! addMessage ctx { UserMessage.info with Message = "Log off successful" }
return! redirectToGet "" next ctx return! redirectToGet "" next ctx
} }
/// Display the user edit page, with information possibly filled in /// Display the user edit page, with information possibly filled in
let private showEdit (hash : Hash) : HttpHandler = fun next ctx -> task { let private showEdit (hash : Hash) : HttpHandler = fun next ctx ->
hash.Add ("page_title", "Edit Your Information") addToHash "page_title" "Edit Your Information" hash
hash.Add ("csrf", ctx.CsrfTokenSet) |> addToHash "csrf" ctx.CsrfTokenSet
return! viewForTheme "admin" "user-edit" next ctx hash |> viewForTheme "admin" "user-edit" next ctx
}
// GET /admin/user/edit // GET /admin/user/edit
let edit : HttpHandler = requireAccess Author >=> fun next ctx -> task { let edit : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! ctx.Data.WebLogUser.findById ctx.UserId ctx.WebLog.id with match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.id with
| Some user -> return! showEdit (Hash.FromAnonymousObject {| model = EditUserModel.fromUser user |}) next ctx | Some user -> return! showEdit (Hash.FromAnonymousObject {| model = EditUserModel.fromUser user |}) next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
@ -88,32 +86,32 @@ let edit : HttpHandler = requireAccess Author >=> fun next ctx -> task {
// POST /admin/user/save // POST /admin/user/save
let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditUserModel> () let! model = ctx.BindFormAsync<EditUserModel> ()
if model.newPassword = model.newPasswordConfirm then if model.NewPassword = model.NewPasswordConfirm then
let data = ctx.Data let data = ctx.Data
match! data.WebLogUser.findById ctx.UserId ctx.WebLog.id with match! data.WebLogUser.FindById ctx.UserId ctx.WebLog.id with
| Some user -> | Some user ->
let pw, salt = let pw, salt =
if model.newPassword = "" then if model.NewPassword = "" then
user.passwordHash, user.salt user.passwordHash, user.salt
else else
let newSalt = Guid.NewGuid () let newSalt = Guid.NewGuid ()
hashedPassword model.newPassword user.userName newSalt, newSalt hashedPassword model.NewPassword user.userName newSalt, newSalt
let user = let user =
{ user with { user with
firstName = model.firstName firstName = model.FirstName
lastName = model.lastName lastName = model.LastName
preferredName = model.preferredName preferredName = model.PreferredName
passwordHash = pw passwordHash = pw
salt = salt salt = salt
} }
do! data.WebLogUser.update user do! data.WebLogUser.Update user
let pwMsg = if model.newPassword = "" then "" else " and updated your password" let pwMsg = if model.NewPassword = "" then "" else " and updated your password"
do! addMessage ctx { UserMessage.success with message = $"Saved your information{pwMsg} successfully" } do! addMessage ctx { UserMessage.success with Message = $"Saved your information{pwMsg} successfully" }
return! redirectToGet "admin/user/edit" next ctx return! redirectToGet "admin/user/edit" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
else else
do! addMessage ctx { UserMessage.error with message = "Passwords did not match; no updates made" } do! addMessage ctx { UserMessage.error with Message = "Passwords did not match; no updates made" }
return! showEdit (Hash.FromAnonymousObject {| return! showEdit (Hash.FromAnonymousObject {|
model = { model with newPassword = ""; newPasswordConfirm = "" } model = { model with NewPassword = ""; NewPasswordConfirm = "" }
|}) next ctx |}) next ctx
} }

View File

@ -27,10 +27,10 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
// If this is the first web log being created, the user will be an installation admin; otherwise, they will be an // If this is the first web log being created, the user will be an installation admin; otherwise, they will be an
// admin just over their web log // admin just over their web log
let! webLogs = data.WebLog.all () let! webLogs = data.WebLog.All ()
let accessLevel = if List.isEmpty webLogs then Administrator else WebLogAdmin let accessLevel = if List.isEmpty webLogs then Administrator else WebLogAdmin
do! data.WebLog.add do! data.WebLog.Add
{ WebLog.empty with { WebLog.empty with
id = webLogId id = webLogId
name = args[2] name = args[2]
@ -42,8 +42,9 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
// Create the admin user // Create the admin user
let salt = Guid.NewGuid () let salt = Guid.NewGuid ()
let now = DateTime.UtcNow
do! data.WebLogUser.add do! data.WebLogUser.Add
{ WebLogUser.empty with { WebLogUser.empty with
id = userId id = userId
webLogId = webLogId webLogId = webLogId
@ -54,21 +55,22 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
salt = salt salt = salt
accessLevel = accessLevel accessLevel = accessLevel
createdOn = now
} }
// Create the default home page // Create the default home page
do! data.Page.add do! data.Page.Add
{ Page.empty with { Page.empty with
id = homePageId id = homePageId
webLogId = webLogId webLogId = webLogId
authorId = userId authorId = userId
title = "Welcome to myWebLog!" title = "Welcome to myWebLog!"
permalink = Permalink "welcome-to-myweblog.html" permalink = Permalink "welcome-to-myweblog.html"
publishedOn = DateTime.UtcNow publishedOn = now
updatedOn = DateTime.UtcNow updatedOn = now
text = "<p>This is your default home page.</p>" text = "<p>This is your default home page.</p>"
revisions = [ revisions = [
{ asOf = DateTime.UtcNow { asOf = now
text = Html "<p>This is your default home page.</p>" text = Html "<p>This is your default home page.</p>"
} }
] ]
@ -94,7 +96,7 @@ let createWebLog args sp = task {
let private importPriorPermalinks urlBase file (sp : IServiceProvider) = task { let private importPriorPermalinks urlBase file (sp : IServiceProvider) = task {
let data = sp.GetRequiredService<IData> () let data = sp.GetRequiredService<IData> ()
match! data.WebLog.findByHost urlBase with match! data.WebLog.FindByHost urlBase with
| Some webLog -> | Some webLog ->
let mapping = let mapping =
@ -105,10 +107,10 @@ let private importPriorPermalinks urlBase file (sp : IServiceProvider) = task {
Permalink parts[0], Permalink parts[1]) Permalink parts[0], Permalink parts[1])
for old, current in mapping do for old, current in mapping do
match! data.Post.findByPermalink current webLog.id with match! data.Post.FindByPermalink current webLog.id with
| Some post -> | Some post ->
let! withLinks = data.Post.findFullById post.id post.webLogId let! withLinks = data.Post.FindFullById post.id post.webLogId
let! _ = data.Post.updatePriorPermalinks post.id post.webLogId let! _ = data.Post.UpdatePriorPermalinks post.id post.webLogId
(old :: withLinks.Value.priorPermalinks) (old :: withLinks.Value.priorPermalinks)
printfn $"{Permalink.toString old} -> {Permalink.toString current}" printfn $"{Permalink.toString old} -> {Permalink.toString current}"
| None -> eprintfn $"Cannot find current post for {Permalink.toString current}" | None -> eprintfn $"Cannot find current post for {Permalink.toString current}"
@ -285,24 +287,24 @@ module Backup =
let themeId = ThemeId webLog.themePath let themeId = ThemeId webLog.themePath
printfn "- Exporting theme..." printfn "- Exporting theme..."
let! theme = data.Theme.findById themeId let! theme = data.Theme.FindById themeId
let! assets = data.ThemeAsset.findByThemeWithData themeId let! assets = data.ThemeAsset.FindByThemeWithData themeId
printfn "- Exporting users..." printfn "- Exporting users..."
let! users = data.WebLogUser.findByWebLog webLog.id let! users = data.WebLogUser.FindByWebLog webLog.id
printfn "- Exporting categories and tag mappings..." printfn "- Exporting categories and tag mappings..."
let! categories = data.Category.findByWebLog webLog.id let! categories = data.Category.FindByWebLog webLog.id
let! tagMaps = data.TagMap.findByWebLog webLog.id let! tagMaps = data.TagMap.FindByWebLog webLog.id
printfn "- Exporting pages..." printfn "- Exporting pages..."
let! pages = data.Page.findFullByWebLog webLog.id let! pages = data.Page.FindFullByWebLog webLog.id
printfn "- Exporting posts..." printfn "- Exporting posts..."
let! posts = data.Post.findFullByWebLog webLog.id let! posts = data.Post.FindFullByWebLog webLog.id
printfn "- Exporting uploads..." printfn "- Exporting uploads..."
let! uploads = data.Upload.findByWebLogWithData webLog.id let! uploads = data.Upload.FindByWebLogWithData webLog.id
printfn "- Writing archive..." printfn "- Writing archive..."
let archive = { let archive = {
@ -329,9 +331,9 @@ module Backup =
let private doRestore archive newUrlBase (data : IData) = task { let private doRestore archive newUrlBase (data : IData) = task {
let! restore = task { let! restore = task {
match! data.WebLog.findById archive.webLog.id with match! data.WebLog.FindById archive.webLog.id with
| Some webLog when defaultArg newUrlBase webLog.urlBase = webLog.urlBase -> | Some webLog when defaultArg newUrlBase webLog.urlBase = webLog.urlBase ->
do! data.WebLog.delete webLog.id do! data.WebLog.Delete webLog.id
return { archive with webLog = { archive.webLog with urlBase = defaultArg newUrlBase webLog.urlBase } } return { archive with webLog = { archive.webLog with urlBase = defaultArg newUrlBase webLog.urlBase } }
| Some _ -> | Some _ ->
// Err'body gets new IDs... // Err'body gets new IDs...
@ -379,31 +381,31 @@ module Backup =
// Restore theme and assets (one at a time, as assets can be large) // Restore theme and assets (one at a time, as assets can be large)
printfn "" printfn ""
printfn "- Importing theme..." printfn "- Importing theme..."
do! data.Theme.save restore.theme do! data.Theme.Save restore.theme
let! _ = restore.assets |> List.map (EncodedAsset.fromEncoded >> data.ThemeAsset.save) |> Task.WhenAll let! _ = restore.assets |> List.map (EncodedAsset.fromEncoded >> data.ThemeAsset.Save) |> Task.WhenAll
// Restore web log data // Restore web log data
printfn "- Restoring web log..." printfn "- Restoring web log..."
do! data.WebLog.add restore.webLog do! data.WebLog.Add restore.webLog
printfn "- Restoring users..." printfn "- Restoring users..."
do! data.WebLogUser.restore restore.users do! data.WebLogUser.Restore restore.users
printfn "- Restoring categories and tag mappings..." printfn "- Restoring categories and tag mappings..."
do! data.TagMap.restore restore.tagMappings do! data.TagMap.Restore restore.tagMappings
do! data.Category.restore restore.categories do! data.Category.Restore restore.categories
printfn "- Restoring pages..." printfn "- Restoring pages..."
do! data.Page.restore restore.pages do! data.Page.Restore restore.pages
printfn "- Restoring posts..." printfn "- Restoring posts..."
do! data.Post.restore restore.posts do! data.Post.Restore restore.posts
// TODO: comments not yet implemented // TODO: comments not yet implemented
printfn "- Restoring uploads..." printfn "- Restoring uploads..."
do! data.Upload.restore (restore.uploads |> List.map EncodedUpload.fromEncoded) do! data.Upload.Restore (restore.uploads |> List.map EncodedUpload.fromEncoded)
displayStats "Restored for <>NAME<>:" restore.webLog restore displayStats "Restored for <>NAME<>:" restore.webLog restore
} }
@ -436,7 +438,7 @@ module Backup =
let generateBackup (args : string[]) (sp : IServiceProvider) = task { let generateBackup (args : string[]) (sp : IServiceProvider) = task {
if args.Length > 1 && args.Length < 5 then if args.Length > 1 && args.Length < 5 then
let data = sp.GetRequiredService<IData> () let data = sp.GetRequiredService<IData> ()
match! data.WebLog.findByHost args[1] with match! data.WebLog.FindByHost args[1] with
| Some webLog -> | Some webLog ->
let fileName = let fileName =
if args.Length = 2 || (args.Length = 3 && args[2] = "pretty") then if args.Length = 2 || (args.Length = 3 && args[2] = "pretty") then
@ -469,13 +471,13 @@ module Backup =
/// Upgrade a WebLogAdmin user to an Administrator user /// Upgrade a WebLogAdmin user to an Administrator user
let private doUserUpgrade urlBase email (data : IData) = task { let private doUserUpgrade urlBase email (data : IData) = task {
match! data.WebLog.findByHost urlBase with match! data.WebLog.FindByHost urlBase with
| Some webLog -> | Some webLog ->
match! data.WebLogUser.findByEmail email webLog.id with match! data.WebLogUser.FindByEmail email webLog.id with
| Some user -> | Some user ->
match user.accessLevel with match user.accessLevel with
| WebLogAdmin -> | WebLogAdmin ->
do! data.WebLogUser.update { user with accessLevel = Administrator } do! data.WebLogUser.Update { user with accessLevel = Administrator }
printfn $"{email} is now an Administrator user" printfn $"{email} is now an Administrator user"
| other -> eprintfn $"ERROR: {email} is an {AccessLevel.toString other}, not a WebLogAdmin" | other -> eprintfn $"ERROR: {email} is an {AccessLevel.toString other}, not a WebLogAdmin"
| None -> eprintfn $"ERROR: no user {email} found at {urlBase}" | None -> eprintfn $"ERROR: no user {email} found at {urlBase}"

View File

@ -90,7 +90,7 @@ let rec main args =
let data = DataImplementation.get sp let data = DataImplementation.get sp
task { task {
do! data.startUp () do! data.StartUp ()
do! WebLogCache.fill data do! WebLogCache.fill data
do! ThemeAssetCache.fill data do! ThemeAssetCache.fill data
} |> Async.AwaitTask |> Async.RunSynchronously } |> Async.AwaitTask |> Async.RunSynchronously

View File

@ -3,25 +3,25 @@
<form hx-post="{{ "admin/category/save" | relative_link }}" method="post" class="container" <form hx-post="{{ "admin/category/save" | relative_link }}" method="post" class="container"
hx-target="#catList" hx-swap="outerHTML show:window:top"> hx-target="#catList" hx-swap="outerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type="hidden" name="categoryId" value="{{ model.category_id }}"> <input type="hidden" name="CategoryId" value="{{ model.category_id }}">
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-lg-4 col-xxl-3 offset-xxl-1 mb-3"> <div class="col-12 col-sm-6 col-lg-4 col-xxl-3 offset-xxl-1 mb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="name" id="name" class="form-control form-control-sm" placeholder="Name" autofocus <input type="text" name="Name" id="name" class="form-control form-control-sm" placeholder="Name" autofocus
required value="{{ model.name | escape }}"> required value="{{ model.name | escape }}">
<label for="name">Name</label> <label for="name">Name</label>
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-lg-4 col-xxl-3 mb-3"> <div class="col-12 col-sm-6 col-lg-4 col-xxl-3 mb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="slug" id="slug" class="form-control form-control-sm" placeholder="Slug" required <input type="text" name="Slug" id="slug" class="form-control form-control-sm" placeholder="Slug" required
value="{{ model.slug | escape }}"> value="{{ model.slug | escape }}">
<label for="slug">Slug</label> <label for="slug">Slug</label>
</div> </div>
</div> </div>
<div class="col-12 col-lg-4 col-xxl-3 offset-xxl-1 mb-3"> <div class="col-12 col-lg-4 col-xxl-3 offset-xxl-1 mb-3">
<div class="form-floating"> <div class="form-floating">
<select name="parentId" id="parentId" class="form-control form-control-sm"> <select name="ParentId" id="parentId" class="form-control form-control-sm">
<option value=""{% if model.parent_id == "" %} selected="selected"{% endif %}> <option value=""{% if model.parent_id == "" %} selected="selected"{% endif %}>
&ndash; None &ndash; &ndash; None &ndash;
</option> </option>
@ -38,7 +38,7 @@
</div> </div>
<div class="col-12 col-xl-10 offset-xl-1 mb-3"> <div class="col-12 col-xl-10 offset-xl-1 mb-3">
<div class="form-floating"> <div class="form-floating">
<input name="description" id="description" class="form-control form-control-sm" <input name="Description" id="description" class="form-control form-control-sm"
placeholder="A short description of this category" value="{{ model.description | escape }}"> placeholder="A short description of this category" value="{{ model.description | escape }}">
<label for="description">Description</label> <label for="description">Description</label>
</div> </div>

View File

@ -2,7 +2,7 @@
<article> <article>
<form action="{{ "admin/settings/rss/save" | relative_link }}" method="post"> <form action="{{ "admin/settings/rss/save" | relative_link }}" method="post">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type="hidden" name="id" value="{{ model.id }}"> <input type="hidden" name="Id" value="{{ model.id }}">
{%- assign typ = model.source_type -%} {%- assign typ = model.source_type -%}
<div class="container"> <div class="container">
<div class="row pb-3"> <div class="row pb-3">
@ -17,7 +17,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="path" id="path" class="form-control" placeholder="Relative Feed Path" <input type="text" name="Path" id="path" class="form-control" placeholder="Relative Feed Path"
value="{{ model.path }}"> value="{{ model.path }}">
<label for="path">Relative Feed Path</label> <label for="path">Relative Feed Path</label>
<span class="form-text fst-italic">Appended to {{ web_log.url_base }}/</span> <span class="form-text fst-italic">Appended to {{ web_log.url_base }}/</span>
@ -27,7 +27,7 @@
<div class="row"> <div class="row">
<div class="col py-3 d-flex align-self-center justify-content-center"> <div class="col py-3 d-flex align-self-center justify-content-center">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" name="isPodcast" id="isPodcast" class="form-check-input" value="true" <input type="checkbox" name="IsPodcast" id="isPodcast" class="form-check-input" value="true"
{%- if model.is_podcast %} checked="checked"{% endif %} onclick="Admin.checkPodcast()"> {%- if model.is_podcast %} checked="checked"{% endif %} onclick="Admin.checkPodcast()">
<label for="isPodcast" class="form-check-label">This Is a Podcast Feed</label> <label for="isPodcast" class="form-check-label">This Is a Podcast Feed</label>
</div> </div>
@ -41,7 +41,7 @@
<div class="row d-flex align-items-center"> <div class="row d-flex align-items-center">
<div class="col-1 d-flex justify-content-end pb-3"> <div class="col-1 d-flex justify-content-end pb-3">
<div class="form-check form-check-inline me-0"> <div class="form-check form-check-inline me-0">
<input type="radio" name="sourceType" id="sourceTypeCat" class="form-check-input" value="category" <input type="radio" name="SourceType" id="sourceTypeCat" class="form-check-input" value="category"
{%- unless typ == "tag" %} checked="checked" {% endunless -%} {%- unless typ == "tag" %} checked="checked" {% endunless -%}
onclick="Admin.customFeedBy('category')"> onclick="Admin.customFeedBy('category')">
<label for="sourceTypeCat" class="form-check-label d-none">Category</label> <label for="sourceTypeCat" class="form-check-label d-none">Category</label>
@ -49,7 +49,7 @@
</div> </div>
<div class="col-11 pb-3"> <div class="col-11 pb-3">
<div class="form-floating"> <div class="form-floating">
<select name="sourceValue" id="sourceValueCat" class="form-control" required <select name="SourceValue" id="sourceValueCat" class="form-control" required
{%- if typ == "tag" %} disabled="disabled"{% endif %}> {%- if typ == "tag" %} disabled="disabled"{% endif %}>
<option value="">&ndash; Select Category &ndash;</option> <option value="">&ndash; Select Category &ndash;</option>
{% for cat in categories -%} {% for cat in categories -%}
@ -64,14 +64,14 @@
</div> </div>
<div class="col-1 d-flex justify-content-end pb-3"> <div class="col-1 d-flex justify-content-end pb-3">
<div class="form-check form-check-inline me-0"> <div class="form-check form-check-inline me-0">
<input type="radio" name="sourceType" id="sourceTypeTag" class="form-check-input" value="tag" <input type="radio" name="SourceType" id="sourceTypeTag" class="form-check-input" value="tag"
{%- if typ == "tag" %} checked="checked"{% endif %} onclick="Admin.customFeedBy('tag')"> {%- if typ == "tag" %} checked="checked"{% endif %} onclick="Admin.customFeedBy('tag')">
<label for="sourceTypeTag" class="form-check-label d-none">Tag</label> <label for="sourceTypeTag" class="form-check-label d-none">Tag</label>
</div> </div>
</div> </div>
<div class="col-11 pb-3"> <div class="col-11 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="sourceValue" id="sourceValueTag" class="form-control" placeholder="Tag" <input type="text" name="SourceValue" id="sourceValueTag" class="form-control" placeholder="Tag"
{%- unless typ == "tag" %} disabled="disabled"{% endunless %} required {%- unless typ == "tag" %} disabled="disabled"{% endunless %} required
{%- if typ == "tag" %} value="{{ model.source_value }}"{% endif %}> {%- if typ == "tag" %} value="{{ model.source_value }}"{% endif %}>
<label for="sourceValueTag">Tag</label> <label for="sourceValueTag">Tag</label>
@ -88,21 +88,21 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-5 col-lg-4 offset-lg-1 pb-3"> <div class="col-12 col-md-5 col-lg-4 offset-lg-1 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="title" id="title" class="form-control" placeholder="Title" required <input type="text" name="Title" id="title" class="form-control" placeholder="Title" required
value="{{ model.title }}"> value="{{ model.title }}">
<label for="title">Title</label> <label for="title">Title</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-4 col-lg-4 pb-3"> <div class="col-12 col-md-4 col-lg-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="subtitle" id="subtitle" class="form-control" placeholder="Subtitle" <input type="text" name="Subtitle" id="subtitle" class="form-control" placeholder="Subtitle"
value="{{ model.subtitle }}"> value="{{ model.subtitle }}">
<label for="subtitle">Podcast Subtitle</label> <label for="subtitle">Podcast Subtitle</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-3 col-lg-2 pb-3"> <div class="col-12 col-md-3 col-lg-2 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="number" name="itemsInFeed" id="itemsInFeed" class="form-control" placeholder="Items" <input type="number" name="ItemsInFeed" id="itemsInFeed" class="form-control" placeholder="Items"
required value="{{ model.items_in_feed }}"> required value="{{ model.items_in_feed }}">
<label for="itemsInFeed"># Episodes</label> <label for="itemsInFeed"># Episodes</label>
</div> </div>
@ -111,7 +111,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-5 col-lg-4 offset-lg-1 pb-3"> <div class="col-12 col-md-5 col-lg-4 offset-lg-1 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="itunesCategory" id="itunesCategory" class="form-control" <input type="text" name="iTunesCategory" id="itunesCategory" class="form-control"
placeholder="iTunes Category" required value="{{ model.itunes_category }}"> placeholder="iTunes Category" required value="{{ model.itunes_category }}">
<label for="itunesCategory">iTunes Category</label> <label for="itunesCategory">iTunes Category</label>
<span class="form-text fst-italic"> <span class="form-text fst-italic">
@ -124,14 +124,14 @@
</div> </div>
<div class="col-12 col-md-4 pb-3"> <div class="col-12 col-md-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="itunesSubcategory" id="itunesSubcategory" class="form-control" <input type="text" name="iTunesSubcategory" id="itunesSubcategory" class="form-control"
placeholder="iTunes Subcategory" value="{{ model.itunes_subcategory }}"> placeholder="iTunes Subcategory" value="{{ model.itunes_subcategory }}">
<label for="itunesSubcategory">iTunes Subcategory</label> <label for="itunesSubcategory">iTunes Subcategory</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-3 col-lg-2 pb-3"> <div class="col-12 col-md-3 col-lg-2 pb-3">
<div class="form-floating"> <div class="form-floating">
<select name="explicit" id="explicit" class="form-control" required> <select name="Explicit" id="explicit" class="form-control" required>
<option value="yes"{% if model.explicit == "yes" %} selected="selected"{% endif %}>Yes</option> <option value="yes"{% if model.explicit == "yes" %} selected="selected"{% endif %}>Yes</option>
<option value="no"{% if model.explicit == "no" %} selected="selected"{% endif %}>No</option> <option value="no"{% if model.explicit == "no" %} selected="selected"{% endif %}>No</option>
<option value="clean"{% if model.explicit == "clean" %} selected="selected"{% endif %}> <option value="clean"{% if model.explicit == "clean" %} selected="selected"{% endif %}>
@ -145,14 +145,14 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-6 col-lg-4 offset-xxl-1 pb-3"> <div class="col-12 col-md-6 col-lg-4 offset-xxl-1 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="displayedAuthor" id="displayedAuthor" class="form-control" <input type="text" name="DisplayedAuthor" id="displayedAuthor" class="form-control"
placeholder="Author" required value="{{ model.displayed_author }}"> placeholder="Author" required value="{{ model.displayed_author }}">
<label for="displayedAuthor">Displayed Author</label> <label for="displayedAuthor">Displayed Author</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-lg-4 pb-3"> <div class="col-12 col-md-6 col-lg-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="email" name="email" id="email" class="form-control" placeholder="Email" required <input type="email" name="Email" id="email" class="form-control" placeholder="Email" required
value="{{ model.email }}"> value="{{ model.email }}">
<label for="email">Author E-mail</label> <label for="email">Author E-mail</label>
<span class="form-text fst-italic">For iTunes, must match registered e-mail</span> <span class="form-text fst-italic">For iTunes, must match registered e-mail</span>
@ -160,7 +160,7 @@
</div> </div>
<div class="col-12 col-sm-5 col-md-4 col-lg-4 col-xl-3 offset-xl-1 col-xxl-2 offset-xxl-0"> <div class="col-12 col-sm-5 col-md-4 col-lg-4 col-xl-3 offset-xl-1 col-xxl-2 offset-xxl-0">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="defaultMediaType" id="defaultMediaType" class="form-control" <input type="text" name="DefaultMediaType" id="defaultMediaType" class="form-control"
placeholder="Media Type" value="{{ model.default_media_type }}"> placeholder="Media Type" value="{{ model.default_media_type }}">
<label for="defaultMediaType">Default Media Type</label> <label for="defaultMediaType">Default Media Type</label>
<span class="form-text fst-italic">Optional; blank for no default</span> <span class="form-text fst-italic">Optional; blank for no default</span>
@ -168,7 +168,7 @@
</div> </div>
<div class="col-12 col-sm-7 col-md-8 col-lg-10 offset-lg-1"> <div class="col-12 col-sm-7 col-md-8 col-lg-10 offset-lg-1">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="imageUrl" id="imageUrl" class="form-control" placeholder="Image URL" required <input type="text" name="ImageUrl" id="imageUrl" class="form-control" placeholder="Image URL" required
value="{{ model.image_url }}"> value="{{ model.image_url }}">
<label for="imageUrl">Image URL</label> <label for="imageUrl">Image URL</label>
<span class="form-text fst-italic">Relative URL will be appended to {{ web_log.url_base }}/</span> <span class="form-text fst-italic">Relative URL will be appended to {{ web_log.url_base }}/</span>
@ -178,7 +178,7 @@
<div class="row pb-3"> <div class="row pb-3">
<div class="col-12 col-lg-10 offset-lg-1"> <div class="col-12 col-lg-10 offset-lg-1">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="summary" id="summary" class="form-control" placeholder="Summary" required <input type="text" name="Summary" id="summary" class="form-control" placeholder="Summary" required
value="{{ model.summary }}"> value="{{ model.summary }}">
<label for="summary">Summary</label> <label for="summary">Summary</label>
<span class="form-text fst-italic">Displayed in podcast directories</span> <span class="form-text fst-italic">Displayed in podcast directories</span>
@ -188,7 +188,7 @@
<div class="row pb-3"> <div class="row pb-3">
<div class="col-12 col-lg-10 offset-lg-1"> <div class="col-12 col-lg-10 offset-lg-1">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="mediaBaseUrl" id="mediaBaseUrl" class="form-control" <input type="text" name="MediaBaseUrl" id="mediaBaseUrl" class="form-control"
placeholder="Media Base URL" value="{{ model.media_base_url }}"> placeholder="Media Base URL" value="{{ model.media_base_url }}">
<label for="mediaBaseUrl">Media Base URL</label> <label for="mediaBaseUrl">Media Base URL</label>
<span class="form-text fst-italic">Optional; prepended to episode media file if present</span> <span class="form-text fst-italic">Optional; prepended to episode media file if present</span>
@ -198,7 +198,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-lg-5 offset-lg-1 pb-3"> <div class="col-12 col-lg-5 offset-lg-1 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="fundingUrl" id="fundingUrl" class="form-control" placeholder="Funding URL" <input type="text" name="FundingUrl" id="fundingUrl" class="form-control" placeholder="Funding URL"
value="{{ model.funding_url }}"> value="{{ model.funding_url }}">
<label for="fundingUrl">Funding URL</label> <label for="fundingUrl">Funding URL</label>
<span class="form-text fst-italic"> <span class="form-text fst-italic">
@ -208,7 +208,7 @@
</div> </div>
<div class="col-12 col-lg-5 pb-3"> <div class="col-12 col-lg-5 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="fundingText" id="fundingText" class="form-control" maxlength="128" <input type="text" name="FundingText" id="fundingText" class="form-control" maxlength="128"
placeholder="Funding Text" value="{{ model.funding_text }}"> placeholder="Funding Text" value="{{ model.funding_text }}">
<label for="fundingText">Funding Text</label> <label for="fundingText">Funding Text</label>
<span class="form-text fst-italic">Optional; text for the funding link</span> <span class="form-text fst-italic">Optional; text for the funding link</span>
@ -218,8 +218,8 @@
<div class="row pb-3"> <div class="row pb-3">
<div class="col-8 col-lg-5 offset-lg-1 pb-3"> <div class="col-8 col-lg-5 offset-lg-1 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="guid" id="guid" class="form-control" placeholder="GUID" <input type="text" name="PodcastGuid" id="guid" class="form-control" placeholder="GUID"
value="{{ model.guid }}"> value="{{ model.podcast_guid }}">
<label for="guid">Podcast GUID</label> <label for="guid">Podcast GUID</label>
<span class="form-text fst-italic"> <span class="form-text fst-italic">
Optional; v5 UUID uniquely identifying this podcast; once entered, do not change this value Optional; v5 UUID uniquely identifying this podcast; once entered, do not change this value
@ -230,7 +230,7 @@
</div> </div>
<div class="col-4 col-lg-3 offset-lg-2 pb-3"> <div class="col-4 col-lg-3 offset-lg-2 pb-3">
<div class="form-floating"> <div class="form-floating">
<select name="medium" id="medium" class="form-control"> <select name="Medium" id="medium" class="form-control">
{% for med in medium_values -%} {% for med in medium_values -%}
<option value="{{ med[0] }}"{% if model.medium == med[0] %} selected{% endif %}> <option value="{{ med[0] }}"{% if model.medium == med[0] %} selected{% endif %}>
{{ med[1] }} {{ med[1] }}

View File

@ -3,19 +3,19 @@
<form action="{{ "user/log-on" | relative_link }}" method="post" hx-push-url="true"> <form action="{{ "user/log-on" | relative_link }}" method="post" hx-push-url="true">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
{% if model.return_to %} {% if model.return_to %}
<input type="hidden" name="returnTo" value="{{ model.return_to.value }}"> <input type="hidden" name="ReturnTo" value="{{ model.return_to.value }}">
{% endif %} {% endif %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-md-6 col-lg-4 offset-lg-2 pb-3"> <div class="col-12 col-md-6 col-lg-4 offset-lg-2 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="email" id="email" name="emailAddress" class="form-control" autofocus required> <input type="email" id="email" name="EmailAddress" class="form-control" autofocus required>
<label for="email">E-mail Address</label> <label for="email">E-mail Address</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-lg-4 pb-3"> <div class="col-12 col-md-6 col-lg-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="password" id="password" name="password" class="form-control" required> <input type="password" id="password" name="Password" class="form-control" required>
<label for="password">Password</label> <label for="password">Password</label>
</div> </div>
</div> </div>

View File

@ -2,17 +2,17 @@
<article> <article>
<form action="{{ "admin/page/save" | relative_link }}" method="post" hx-push-url="true"> <form action="{{ "admin/page/save" | relative_link }}" method="post" hx-push-url="true">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type="hidden" name="pageId" value="{{ model.page_id }}"> <input type="hidden" name="PageId" value="{{ model.page_id }}">
<div class="container"> <div class="container">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-9"> <div class="col-9">
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<input type="text" name="title" id="title" class="form-control" autofocus required <input type="text" name="Title" id="title" class="form-control" autofocus required
value="{{ model.title }}"> value="{{ model.title }}">
<label for="title">Title</label> <label for="title">Title</label>
</div> </div>
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<input type="text" name="permalink" id="permalink" class="form-control" required <input type="text" name="Permalink" id="permalink" class="form-control" required
value="{{ model.permalink }}"> value="{{ model.permalink }}">
<label for="permalink">Permalink</label> <label for="permalink">Permalink</label>
{%- if model.page_id != "new" %} {%- if model.page_id != "new" %}
@ -29,20 +29,20 @@
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label for="text">Text</label> &nbsp; &nbsp; <label for="text">Text</label> &nbsp; &nbsp;
<input type="radio" name="source" id="source_html" class="btn-check" value="HTML" <input type="radio" name="Source" id="source_html" class="btn-check" value="HTML"
{%- if model.source == "HTML" %} checked="checked"{% endif %}> {%- if model.source == "HTML" %} checked="checked"{% endif %}>
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label> <label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
<input type="radio" name="source" id="source_md" class="btn-check" value="Markdown" <input type="radio" name="Source" id="source_md" class="btn-check" value="Markdown"
{%- if model.source == "Markdown" %} checked="checked"{% endif %}> {%- if model.source == "Markdown" %} checked="checked"{% endif %}>
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label> <label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<textarea name="text" id="text" class="form-control">{{ model.text }}</textarea> <textarea name="Text" id="text" class="form-control">{{ model.text }}</textarea>
</div> </div>
</div> </div>
<div class="col-3"> <div class="col-3">
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<select name="template" id="template" class="form-control"> <select name="Template" id="template" class="form-control">
{% for tmpl in templates -%} {% for tmpl in templates -%}
<option value="{{ tmpl[0] }}"{% if model.template == tmpl[0] %} selected="selected"{% endif %}> <option value="{{ tmpl[0] }}"{% if model.template == tmpl[0] %} selected="selected"{% endif %}>
{{ tmpl[1] }} {{ tmpl[1] }}
@ -52,7 +52,7 @@
<label for="template">Page Template</label> <label for="template">Page Template</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" name="isShownInPageList" id="showList" class="form-check-input" value="true" <input type="checkbox" name="IsShownInPageList" id="showList" class="form-check-input" value="true"
{%- if model.is_shown_in_page_list %} checked="checked"{% endif %}> {%- if model.is_shown_in_page_list %} checked="checked"{% endif %}>
<label for="showList" class="form-check-label">Show in Page List</label> <label for="showList" class="form-check-label">Show in Page List</label>
</div> </div>
@ -84,14 +84,14 @@
</div> </div>
<div class="col-3"> <div class="col-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="metaNames" id="metaNames_{{ meta[0] }}" class="form-control" <input type="text" name="MetaNames" id="metaNames_{{ meta[0] }}" class="form-control"
placeholder="Name" value="{{ meta[1] }}"> placeholder="Name" value="{{ meta[1] }}">
<label for="metaNames_{{ meta[0] }}">Name</label> <label for="metaNames_{{ meta[0] }}">Name</label>
</div> </div>
</div> </div>
<div class="col-8"> <div class="col-8">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="metaValues" id="metaValues_{{ meta[0] }}" class="form-control" <input type="text" name="MetaValues" id="metaValues_{{ meta[0] }}" class="form-control"
placeholder="Value" value="{{ meta[2] }}"> placeholder="Value" value="{{ meta[2] }}">
<label for="metaValues_{{ meta[0] }}">Value</label> <label for="metaValues_{{ meta[0] }}">Value</label>
</div> </div>

View File

@ -3,7 +3,7 @@
{%- assign base_url = "admin/" | append: model.entity | append: "/" -%} {%- assign base_url = "admin/" | append: model.entity | append: "/" -%}
<form action="{{ base_url | append: "permalinks" | relative_link }}" method="post"> <form action="{{ base_url | append: "permalinks" | relative_link }}" method="post">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type="hidden" name="id" value="{{ model.id }}"> <input type="hidden" name="Id" value="{{ model.id }}">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@ -36,7 +36,7 @@
</div> </div>
<div class="col-11"> <div class="col-11">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="prior" id="prior_{{ link_count }}" class="form-control" <input type="text" name="Prior" id="prior_{{ link_count }}" class="form-control"
placeholder="Link" value="{{ link }}"> placeholder="Link" value="{{ link }}">
<label for="prior_{{ link_count }}">Link</label> <label for="prior_{{ link_count }}">Link</label>
</div> </div>

View File

@ -2,17 +2,17 @@
<article> <article>
<form action="{{ "admin/post/save" | relative_link }}" method="post" hx-push-url="true"> <form action="{{ "admin/post/save" | relative_link }}" method="post" hx-push-url="true">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type="hidden" name="postId" value="{{ model.post_id }}"> <input type="hidden" name="PostId" value="{{ model.post_id }}">
<div class="container"> <div class="container">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-lg-9"> <div class="col-12 col-lg-9">
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<input type="text" name="title" id="title" class="form-control" placeholder="Title" autofocus required <input type="text" name="Title" id="title" class="form-control" placeholder="Title" autofocus required
value="{{ model.title }}"> value="{{ model.title }}">
<label for="title">Title</label> <label for="title">Title</label>
</div> </div>
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<input type="text" name="permalink" id="permalink" class="form-control" placeholder="Permalink" required <input type="text" name="Permalink" id="permalink" class="form-control" placeholder="Permalink" required
value="{{ model.permalink }}"> value="{{ model.permalink }}">
<label for="permalink">Permalink</label> <label for="permalink">Permalink</label>
{%- if model.post_id != "new" %} {%- if model.post_id != "new" %}
@ -30,26 +30,26 @@
<div class="mb-2"> <div class="mb-2">
<label for="text">Text</label> &nbsp; &nbsp; <label for="text">Text</label> &nbsp; &nbsp;
<div class="btn-group btn-group-sm" role="group" aria-label="Text format button group"> <div class="btn-group btn-group-sm" role="group" aria-label="Text format button group">
<input type="radio" name="source" id="source_html" class="btn-check" value="HTML" <input type="radio" name="Source" id="source_html" class="btn-check" value="HTML"
{%- if model.source == "HTML" %} checked="checked"{% endif %}> {%- if model.source == "HTML" %} checked="checked"{% endif %}>
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label> <label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
<input type="radio" name="source" id="source_md" class="btn-check" value="Markdown" <input type="radio" name="Source" id="source_md" class="btn-check" value="Markdown"
{%- if model.source == "Markdown" %} checked="checked"{% endif %}> {%- if model.source == "Markdown" %} checked="checked"{% endif %}>
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label> <label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
</div> </div>
</div> </div>
<div class="pb-3"> <div class="pb-3">
<textarea name="text" id="text" class="form-control" rows="20">{{ model.text }}</textarea> <textarea name="Text" id="text" class="form-control" rows="20">{{ model.text }}</textarea>
</div> </div>
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<input type="text" name="tags" id="tags" class="form-control" placeholder="Tags" <input type="text" name="Tags" id="tags" class="form-control" placeholder="Tags"
value="{{ model.tags }}"> value="{{ model.tags }}">
<label for="tags">Tags</label> <label for="tags">Tags</label>
<div class="form-text">comma-delimited</div> <div class="form-text">comma-delimited</div>
</div> </div>
{% if model.status == "Draft" %} {% if model.status == "Draft" %}
<div class="form-check form-switch pb-2"> <div class="form-check form-switch pb-2">
<input type="checkbox" name="doPublish" id="doPublish" class="form-check-input" value="true"> <input type="checkbox" name="DoPublish" id="doPublish" class="form-check-input" value="true">
<label for="doPublish" class="form-check-label">Publish This Post</label> <label for="doPublish" class="form-check-label">Publish This Post</label>
</div> </div>
{% endif %} {% endif %}
@ -59,7 +59,7 @@
<legend> <legend>
<span class="form-check form-switch"> <span class="form-check form-switch">
<small> <small>
<input type="checkbox" name="isEpisode" id="isEpisode" class="form-check-input" value="true" <input type="checkbox" name="IsEpisode" id="isEpisode" class="form-check-input" value="true"
data-bs-toggle="collapse" data-bs-target="#episodeItems" onclick="Admin.toggleEpisodeFields()" data-bs-toggle="collapse" data-bs-target="#episodeItems" onclick="Admin.toggleEpisodeFields()"
{%- if model.is_episode %}checked="checked"{% endif %}> {%- if model.is_episode %}checked="checked"{% endif %}>
</small> </small>
@ -70,7 +70,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-8 pb-3"> <div class="col-12 col-md-8 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="media" id="media" class="form-control" placeholder="Media" required <input type="text" name="Media" id="media" class="form-control" placeholder="Media" required
value="{{ model.media }}"> value="{{ model.media }}">
<label for="media">Media File</label> <label for="media">Media File</label>
<div class="form-text"> <div class="form-text">
@ -80,7 +80,7 @@
</div> </div>
<div class="col-12 col-md-4 pb-3"> <div class="col-12 col-md-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="mediaType" id="mediaType" class="form-control" placeholder="Media Type" <input type="text" name="MediaType" id="mediaType" class="form-control" placeholder="Media Type"
value="{{ model.media_type }}"> value="{{ model.media_type }}">
<label for="mediaType">Media MIME Type</label> <label for="mediaType">Media MIME Type</label>
<div class="form-text">Optional; overrides podcast default</div> <div class="form-text">Optional; overrides podcast default</div>
@ -90,7 +90,7 @@
<div class="row pb-3"> <div class="row pb-3">
<div class="col"> <div class="col">
<div class="form-floating"> <div class="form-floating">
<input type="number" name="length" id="length" class="form-control" placeholder="Length" required <input type="number" name="Length" id="length" class="form-control" placeholder="Length" required
value="{{ model.length }}"> value="{{ model.length }}">
<label for="length">Media Length (bytes)</label> <label for="length">Media Length (bytes)</label>
<div class="form-text">TODO: derive from above file name</div> <div class="form-text">TODO: derive from above file name</div>
@ -98,7 +98,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="duration" id="duration" class="form-control" placeholder="Duration" <input type="text" name="Duration" id="duration" class="form-control" placeholder="Duration"
value="{{ model.duration }}"> value="{{ model.duration }}">
<label for="duration">Duration</label> <label for="duration">Duration</label>
<div class="form-text">Recommended; enter in <code>HH:MM:SS</code> format</div> <div class="form-text">Recommended; enter in <code>HH:MM:SS</code> format</div>
@ -108,7 +108,7 @@
<div class="row pb-3"> <div class="row pb-3">
<div class="col"> <div class="col">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="subtitle" id="subtitle" class="form-control" placeholder="Subtitle" <input type="text" name="Subtitle" id="subtitle" class="form-control" placeholder="Subtitle"
value="{{ model.subtitle }}"> value="{{ model.subtitle }}">
<label for="subtitle">Subtitle</label> <label for="subtitle">Subtitle</label>
<div class="form-text">Optional; a subtitle for this episode</div> <div class="form-text">Optional; a subtitle for this episode</div>
@ -118,7 +118,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-8 pb-3"> <div class="col-12 col-md-8 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="imageUrl" id="imageUrl" class="form-control" placeholder="Image URL" <input type="text" name="ImageUrl" id="imageUrl" class="form-control" placeholder="Image URL"
value="{{ model.image_url }}"> value="{{ model.image_url }}">
<label for="imageUrl">Image URL</label> <label for="imageUrl">Image URL</label>
<div class="form-text"> <div class="form-text">
@ -128,7 +128,7 @@
</div> </div>
<div class="col-12 col-md-4 pb-3"> <div class="col-12 col-md-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<select name="explicit" id="explicit" class="form-control"> <select name="Explicit" id="explicit" class="form-control">
{% for exp_value in explicit_values %} {% for exp_value in explicit_values %}
<option value="{{ exp_value[0] }}" <option value="{{ exp_value[0] }}"
{%- if model.explicit == exp_value[0] %} selected="selected"{% endif -%}> {%- if model.explicit == exp_value[0] %} selected="selected"{% endif -%}>
@ -144,7 +144,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-8 pb-3"> <div class="col-12 col-md-8 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="chapterFile" id="chapterFile" class="form-control" <input type="text" name="ChapterFile" id="chapterFile" class="form-control"
placeholder="Chapter File" value="{{ model.chapter_file }}"> placeholder="Chapter File" value="{{ model.chapter_file }}">
<label for="chapterFile">Chapter File</label> <label for="chapterFile">Chapter File</label>
<div class="form-text">Optional; relative URL served from this web log</div> <div class="form-text">Optional; relative URL served from this web log</div>
@ -152,7 +152,7 @@
</div> </div>
<div class="col-12 col-md-4 pb-3"> <div class="col-12 col-md-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="chapterType" id="chapterType" class="form-control" <input type="text" name="ChapterType" id="chapterType" class="form-control"
placeholder="Chapter Type" value="{{ model.chapter_type }}"> placeholder="Chapter Type" value="{{ model.chapter_type }}">
<label for="chapterType">Chapter MIME Type</label> <label for="chapterType">Chapter MIME Type</label>
<div class="form-text"> <div class="form-text">
@ -165,7 +165,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-8 pb-3"> <div class="col-12 col-md-8 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="transcriptUrl" id="transcriptUrl" class="form-control" <input type="text" name="TranscriptUrl" id="transcriptUrl" class="form-control"
placeholder="Transcript URL" value="{{ model.transcript_url }}" placeholder="Transcript URL" value="{{ model.transcript_url }}"
onkeyup="Admin.requireTranscriptType()"> onkeyup="Admin.requireTranscriptType()">
<label for="transcriptUrl">Transcript URL</label> <label for="transcriptUrl">Transcript URL</label>
@ -174,7 +174,7 @@
</div> </div>
<div class="col-12 col-md-4 pb-3"> <div class="col-12 col-md-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="transcriptType" id="transcriptType" class="form-control" <input type="text" name="TranscriptType" id="transcriptType" class="form-control"
placeholder="Transcript Type" value="{{ model.transcript_type }}" placeholder="Transcript Type" value="{{ model.transcript_type }}"
{%- if model.transcript_url != "" %} required{% endif %}> {%- if model.transcript_url != "" %} required{% endif %}>
<label for="transcriptType">Transcript MIME Type</label> <label for="transcriptType">Transcript MIME Type</label>
@ -185,7 +185,7 @@
<div class="row pb-3"> <div class="row pb-3">
<div class="col"> <div class="col">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="transcriptLang" id="transcriptLang" class="form-control" <input type="text" name="TranscriptLang" id="transcriptLang" class="form-control"
placeholder="Transcript Language" value="{{ model.transcript_lang }}"> placeholder="Transcript Language" value="{{ model.transcript_lang }}">
<label for="transcriptLang">Transcript Language</label> <label for="transcriptLang">Transcript Language</label>
<div class="form-text">Optional; overrides podcast default</div> <div class="form-text">Optional; overrides podcast default</div>
@ -193,7 +193,7 @@
</div> </div>
<div class="col d-flex justify-content-center"> <div class="col d-flex justify-content-center">
<div class="form-check form-switch align-self-center pb-3"> <div class="form-check form-switch align-self-center pb-3">
<input type="checkbox" name="transcriptCaptions" id="transcriptCaptions" class="form-check-input" <input type="checkbox" name="TranscriptCaptions" id="transcriptCaptions" class="form-check-input"
value="true" {% if model.transcript_captions %} checked="checked"{% endif %}> value="true" {% if model.transcript_captions %} checked="checked"{% endif %}>
<label for="transcriptCaptions">This is a captions file</label> <label for="transcriptCaptions">This is a captions file</label>
</div> </div>
@ -202,7 +202,7 @@
<div class="row pb-3"> <div class="row pb-3">
<div class="col col-md-4"> <div class="col col-md-4">
<div class="form-floating"> <div class="form-floating">
<input type="number" name="seasonNumber" id="seasonNumber" class="form-control" <input type="number" name="SeasonNumber" id="seasonNumber" class="form-control"
placeholder="Season Number" value="{{ model.season_number }}"> placeholder="Season Number" value="{{ model.season_number }}">
<label for="seasonNumber">Season Number</label> <label for="seasonNumber">Season Number</label>
<div class="form-text">Optional</div> <div class="form-text">Optional</div>
@ -210,7 +210,7 @@
</div> </div>
<div class="col col-md-8"> <div class="col col-md-8">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="seasonDescription" id="seasonDescription" class="form-control" <input type="text" name="SeasonDescription" id="seasonDescription" class="form-control"
placeholder="Season Description" maxlength="128" value="{{ model.season_description }}"> placeholder="Season Description" maxlength="128" value="{{ model.season_description }}">
<label for="seasonDescription">Season Description</label> <label for="seasonDescription">Season Description</label>
<div class="form-text">Optional</div> <div class="form-text">Optional</div>
@ -220,7 +220,7 @@
<div class="row pb-3"> <div class="row pb-3">
<div class="col col-md-4"> <div class="col col-md-4">
<div class="form-floating"> <div class="form-floating">
<input type="number" name="episodeNumber" id="episodeNumber" class="form-control" step="0.01" <input type="number" name="EpisodeNumber" id="episodeNumber" class="form-control" step="0.01"
placeholder="Episode Number" value="{{ model.episode_number }}"> placeholder="Episode Number" value="{{ model.episode_number }}">
<label for="episodeNumber">Episode Number</label> <label for="episodeNumber">Episode Number</label>
<div class="form-text">Optional; up to 2 decimal points</div> <div class="form-text">Optional; up to 2 decimal points</div>
@ -228,7 +228,7 @@
</div> </div>
<div class="col col-md-8"> <div class="col col-md-8">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="episodeDescription" id="episodeDescription" class="form-control" <input type="text" name="EpisodeDescription" id="episodeDescription" class="form-control"
placeholder="Episode Description" maxlength="128" value="{{ model.episode_description }}"> placeholder="Episode Description" maxlength="128" value="{{ model.episode_description }}">
<label for="episodeDescription">Episode Description</label> <label for="episodeDescription">Episode Description</label>
<div class="form-text">Optional</div> <div class="form-text">Optional</div>
@ -259,14 +259,14 @@
</div> </div>
<div class="col-3"> <div class="col-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="metaNames" id="metaNames_{{ meta[0] }}" class="form-control" <input type="text" name="MetaNames" id="metaNames_{{ meta[0] }}" class="form-control"
placeholder="Name" value="{{ meta[1] }}"> placeholder="Name" value="{{ meta[1] }}">
<label for="metaNames_{{ meta[0] }}">Name</label> <label for="metaNames_{{ meta[0] }}">Name</label>
</div> </div>
</div> </div>
<div class="col-8"> <div class="col-8">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="metaValues" id="metaValues_{{ meta[0] }}" class="form-control" <input type="text" name="MetaValues" id="metaValues_{{ meta[0] }}" class="form-control"
placeholder="Value" value="{{ meta[2] }}"> placeholder="Value" value="{{ meta[2] }}">
<label for="metaValues_{{ meta[0] }}">Value</label> <label for="metaValues_{{ meta[0] }}">Value</label>
</div> </div>
@ -287,14 +287,14 @@
<div class="row"> <div class="row">
<div class="col align-self-center"> <div class="col align-self-center">
<div class="form-check form-switch pb-2"> <div class="form-check form-switch pb-2">
<input type="checkbox" name="setPublished" id="setPublished" class="form-check-input" <input type="checkbox" name="SetPublished" id="setPublished" class="form-check-input"
value="true"> value="true">
<label for="setPublished" class="form-check-label">Set Published Date</label> <label for="setPublished" class="form-check-label">Set Published Date</label>
</div> </div>
</div> </div>
<div class="col-4"> <div class="col-4">
<div class="form-floating"> <div class="form-floating">
<input type="datetime-local" name="pubOverride" id="pubOverride" class="form-control" <input type="datetime-local" name="PubOverride" id="pubOverride" class="form-control"
placeholder="Override Date" placeholder="Override Date"
{%- if model.pub_override -%} {%- if model.pub_override -%}
value="{{ model.pub_override | date: "yyyy-MM-dd\THH:mm" }}" value="{{ model.pub_override | date: "yyyy-MM-dd\THH:mm" }}"
@ -304,7 +304,7 @@
</div> </div>
<div class="col-5 align-self-center"> <div class="col-5 align-self-center">
<div class="form-check form-switch pb-2"> <div class="form-check form-switch pb-2">
<input type="checkbox" name="setUpdated" id="setUpdated" class="form-check-input" value="true"> <input type="checkbox" name="SetUpdated" id="setUpdated" class="form-check-input" value="true">
<label for="setUpdated" class="form-check-label"> <label for="setUpdated" class="form-check-label">
Purge revisions and<br>set as updated date as well Purge revisions and<br>set as updated date as well
</label> </label>
@ -317,7 +317,7 @@
</div> </div>
<div class="col-12 col-lg-3"> <div class="col-12 col-lg-3">
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<select name="template" id="template" class="form-control"> <select name="Template" id="template" class="form-control">
{% for tmpl in templates -%} {% for tmpl in templates -%}
<option value="{{ tmpl[0] }}"{% if model.template == tmpl[0] %} selected="selected"{% endif %}> <option value="{{ tmpl[0] }}"{% if model.template == tmpl[0] %} selected="selected"{% endif %}>
{{ tmpl[1] }} {{ tmpl[1] }}
@ -330,7 +330,7 @@
<legend>Categories</legend> <legend>Categories</legend>
{% for cat in categories %} {% for cat in categories %}
<div class="form-check"> <div class="form-check">
<input type="checkbox" name="categoryIds" id="categoryId_{{ cat.id }}" class="form-check-input" <input type="checkbox" name="CategoryIds" id="categoryId_{{ cat.id }}" class="form-check-input"
value="{{ cat.id }}" {% if model.category_ids contains cat.id %} checked="checked"{% endif %}> value="{{ cat.id }}" {% if model.category_ids contains cat.id %} checked="checked"{% endif %}>
<label for="categoryId_{{ cat.id }}" class="form-check-label" <label for="categoryId_{{ cat.id }}" class="form-check-label"
{%- if cat.description %} title="{{ cat.description.value | strip_html | escape }}"{% endif %}> {%- if cat.description %} title="{{ cat.description.value | strip_html | escape }}"{% endif %}>

View File

@ -8,18 +8,18 @@
<fieldset class="d-flex justify-content-evenly flex-row"> <fieldset class="d-flex justify-content-evenly flex-row">
<legend>Feeds Enabled</legend> <legend>Feeds Enabled</legend>
<div class="form-check form-switch pb-2"> <div class="form-check form-switch pb-2">
<input type="checkbox" name="feedEnabled" id="feedEnabled" class="form-check-input" value="true" <input type="checkbox" name="IsFeedEnabled" id="feedEnabled" class="form-check-input" value="true"
{% if model.feed_enabled %}checked="checked"{% endif %}> {%- if model.is_feed_enabled %} checked="checked"{% endif %}>
<label for="feedEnabled" class="form-check-label">All Posts</label> <label for="feedEnabled" class="form-check-label">All Posts</label>
</div> </div>
<div class="form-check form-switch pb-2"> <div class="form-check form-switch pb-2">
<input type="checkbox" name="categoryEnabled" id="categoryEnabled" class="form-check-input" value="true" <input type="checkbox" name="IsCategoryEnabled" id="categoryEnabled" class="form-check-input" value="true"
{% if model.category_enabled %}checked="checked"{% endif %}> {%- if model.is_category_enabled %} checked="checked"{% endif %}>
<label for="categoryEnabled" class="form-check-label">Posts by Category</label> <label for="categoryEnabled" class="form-check-label">Posts by Category</label>
</div> </div>
<div class="form-check form-switch pb-2"> <div class="form-check form-switch pb-2">
<input type="checkbox" name="tagEnabled" id="tagEnabled" class="form-check-input" value="true" <input type="checkbox" name="IsTagEnabled" id="tagEnabled" class="form-check-input" value="true"
{% if model.tag_enabled %}checked="checked"{% endif %}> {%- if model.tag_enabled %} checked="checked"{% endif %}>
<label for="tagEnabled" class="form-check-label">Posts by Tag</label> <label for="tagEnabled" class="form-check-label">Posts by Tag</label>
</div> </div>
</fieldset> </fieldset>
@ -28,7 +28,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-xl-2 offset-xl-2 pb-3"> <div class="col-12 col-sm-6 col-md-3 col-xl-2 offset-xl-2 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="feedName" id="feedName" class="form-control" placeholder="Feed File Name" <input type="text" name="FeedName" id="feedName" class="form-control" placeholder="Feed File Name"
value="{{ model.feed_name }}"> value="{{ model.feed_name }}">
<label for="feedName">Feed File Name</label> <label for="feedName">Feed File Name</label>
<span class="form-text">Default is <code>feed.xml</code></span> <span class="form-text">Default is <code>feed.xml</code></span>
@ -36,7 +36,7 @@
</div> </div>
<div class="col-12 col-sm-6 col-md-4 col-xl-2 pb-3"> <div class="col-12 col-sm-6 col-md-4 col-xl-2 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="number" name="itemsInFeed" id="itemsInFeed" class="form-control" min="0" <input type="number" name="ItemsInFeed" id="itemsInFeed" class="form-control" min="0"
placeholder="Items in Feed" required value="{{ model.items_in_feed }}"> placeholder="Items in Feed" required value="{{ model.items_in_feed }}">
<label for="itemsInFeed">Items in Feed</label> <label for="itemsInFeed">Items in Feed</label>
<span class="form-text">Set to &ldquo;0&rdquo; to use &ldquo;Posts per Page&rdquo; setting ({{ web_log.posts_per_page }})</span> <span class="form-text">Set to &ldquo;0&rdquo; to use &ldquo;Posts per Page&rdquo; setting ({{ web_log.posts_per_page }})</span>
@ -44,7 +44,7 @@
</div> </div>
<div class="col-12 col-md-5 col-xl-4 pb-3"> <div class="col-12 col-md-5 col-xl-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="copyright" id="copyright" class="form-control" placeholder="Copyright String" <input type="text" name="Copyright" id="copyright" class="form-control" placeholder="Copyright String"
value="{{ model.copyright }}"> value="{{ model.copyright }}">
<label for="copyright">Copyright String</label> <label for="copyright">Copyright String</label>
<span class="form-text"> <span class="form-text">

View File

@ -10,14 +10,14 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-6 col-xl-4 pb-3"> <div class="col-12 col-md-6 col-xl-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="name" id="name" class="form-control" placeholder="Name" required autofocus <input type="text" name="Name" id="name" class="form-control" placeholder="Name" required autofocus
value="{{ model.name }}"> value="{{ model.name }}">
<label for="name">Name</label> <label for="name">Name</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-4 pb-3"> <div class="col-12 col-md-6 col-xl-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="slug" id="slug" class="form-control" placeholder="Slug" required <input type="text" name="Slug" id="slug" class="form-control" placeholder="Slug" required
value="{{ model.slug }}"> value="{{ model.slug }}">
<label for="slug">Slug</label> <label for="slug">Slug</label>
<span class="form-text"> <span class="form-text">
@ -29,14 +29,14 @@
</div> </div>
<div class="col-12 col-md-6 col-xl-4 pb-3"> <div class="col-12 col-md-6 col-xl-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="subtitle" id="subtitle" class="form-control" placeholder="Subtitle" <input type="text" name="Subtitle" id="subtitle" class="form-control" placeholder="Subtitle"
value="{{ model.subtitle }}"> value="{{ model.subtitle }}">
<label for="subtitle">Subtitle</label> <label for="subtitle">Subtitle</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-4 offset-xl-1 pb-3"> <div class="col-12 col-md-6 col-xl-4 offset-xl-1 pb-3">
<div class="form-floating"> <div class="form-floating">
<select name="themePath" id="themePath" class="form-control" required> <select name="ThemePath" id="themePath" class="form-control" required>
{% for theme in themes -%} {% for theme in themes -%}
<option value="{{ theme[0] }}"{% if model.theme_path == theme[0] %} selected="selected"{% endif %}> <option value="{{ theme[0] }}"{% if model.theme_path == theme[0] %} selected="selected"{% endif %}>
{{ theme[1] }} {{ theme[1] }}
@ -48,7 +48,7 @@
</div> </div>
<div class="col-12 col-md-6 offset-md-1 col-xl-4 offset-xl-0 pb-3"> <div class="col-12 col-md-6 offset-md-1 col-xl-4 offset-xl-0 pb-3">
<div class="form-floating"> <div class="form-floating">
<select name="defaultPage" id="defaultPage" class="form-control" required> <select name="DefaultPage" id="defaultPage" class="form-control" required>
{% for pg in pages -%} {% for pg in pages -%}
<option value="{{ pg[0] }}" <option value="{{ pg[0] }}"
{%- if pg[0] == model.default_page %} selected="selected"{% endif %}> {%- if pg[0] == model.default_page %} selected="selected"{% endif %}>
@ -61,7 +61,7 @@
</div> </div>
<div class="col-12 col-md-4 col-xl-2 pb-3"> <div class="col-12 col-md-4 col-xl-2 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="number" name="postsPerPage" id="postsPerPage" class="form-control" min="0" max="50" required <input type="number" name="PostsPerPage" id="postsPerPage" class="form-control" min="0" max="50" required
value="{{ model.posts_per_page }}"> value="{{ model.posts_per_page }}">
<label for="postsPerPage">Posts per Page</label> <label for="postsPerPage">Posts per Page</label>
</div> </div>
@ -70,14 +70,14 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-4 col-xl-3 offset-xl-2 pb-3"> <div class="col-12 col-md-4 col-xl-3 offset-xl-2 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="timeZone" id="timeZone" class="form-control" placeholder="Time Zone" required <input type="text" name="TimeZone" id="timeZone" class="form-control" placeholder="Time Zone" required
value="{{ model.time_zone }}"> value="{{ model.time_zone }}">
<label for="timeZone">Time Zone</label> <label for="timeZone">Time Zone</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-4 col-xl-2"> <div class="col-12 col-md-4 col-xl-2">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" name="autoHtmx" id="autoHtmx" class="form-check-input" value="true" <input type="checkbox" name="AutoHtmx" id="autoHtmx" class="form-check-input" value="true"
{%- if model.auto_htmx %} checked="checked"{% endif %}> {%- if model.auto_htmx %} checked="checked"{% endif %}>
<label for="autoHtmx" class="form-check-label">Auto-Load htmx</label> <label for="autoHtmx" class="form-check-label">Auto-Load htmx</label>
</div> </div>
@ -87,7 +87,7 @@
</div> </div>
<div class="col-12 col-md-4 col-xl-3 pb-3"> <div class="col-12 col-md-4 col-xl-3 pb-3">
<div class="form-floating"> <div class="form-floating">
<select name="uploads" id="uploads" class="form-control"> <select name="Uploads" id="uploads" class="form-control">
{%- for it in upload_values %} {%- for it in upload_values %}
<option value="{{ it[0] }}"{% if model.uploads == it[0] %} selected{% endif %}>{{ it[1] }}</option> <option value="{{ it[0] }}"{% if model.uploads == it[0] %} selected{% endif %}>{{ it[1] }}</option>
{%- endfor %} {%- endfor %}

View File

@ -2,18 +2,18 @@
<form hx-post="{{ "admin/settings/tag-mapping/save" | relative_link }}" method="post" class="container" <form hx-post="{{ "admin/settings/tag-mapping/save" | relative_link }}" method="post" class="container"
hx-target="#tagList" hx-swap="outerHTML show:window:top"> hx-target="#tagList" hx-swap="outerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type="hidden" name="id" value="{{ model.id }}"> <input type="hidden" name="Id" value="{{ model.id }}">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-6 col-lg-4 offset-lg-2"> <div class="col-6 col-lg-4 offset-lg-2">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="tag" id="tag" class="form-control" placeholder="Tag" autofocus required <input type="text" name="Tag" id="tag" class="form-control" placeholder="Tag" autofocus required
value="{{ model.tag }}"> value="{{ model.tag }}">
<label for="tag">Tag</label> <label for="tag">Tag</label>
</div> </div>
</div> </div>
<div class="col-6 col-lg-4"> <div class="col-6 col-lg-4">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="urlValue" id="urlValue" class="form-control" placeholder="URL Value" required <input type="text" name="UrlValue" id="urlValue" class="form-control" placeholder="URL Value" required
value="{{ model.url_value }}"> value="{{ model.url_value }}">
<label for="urlValue">URL Value</label> <label for="urlValue">URL Value</label>
</div> </div>

View File

@ -6,17 +6,17 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-6 pb-3"> <div class="col-12 col-md-6 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="file" id="file" name="file" class="form-control" placeholder="File" required> <input type="file" id="file" name="File" class="form-control" placeholder="File" required>
<label for="file">File to Upload</label> <label for="file">File to Upload</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 pb-3 d-flex align-self-center justify-content-around"> <div class="col-12 col-md-6 pb-3 d-flex align-self-center justify-content-around">
Destination<br> Destination<br>
<div class="btn-group" role="group" aria-label="Upload destination button group"> <div class="btn-group" role="group" aria-label="Upload destination button group">
<input type="radio" name="destination" id="destination_db" class="btn-check" value="database" <input type="radio" name="Destination" id="destination_db" class="btn-check" value="database"
{%- if destination == "database" %} checked="checked"{% endif %}> {%- if destination == "database" %} checked="checked"{% endif %}>
<label class="btn btn-outline-primary" for="destination_db">Database</label> <label class="btn btn-outline-primary" for="destination_db">Database</label>
<input type="radio" name="destination" id="destination_disk" class="btn-check" value="disk" <input type="radio" name="Destination" id="destination_disk" class="btn-check" value="disk"
{%- if destination == "disk" %} checked="checked"{% endif %}> {%- if destination == "disk" %} checked="checked"{% endif %}>
<label class="btn btn-outline-secondary" for="destination_disk">Disk</label> <label class="btn btn-outline-secondary" for="destination_disk">Disk</label>
</div> </div>

View File

@ -6,21 +6,21 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-md-6 col-lg-4 pb-3"> <div class="col-12 col-md-6 col-lg-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="firstName" id="firstName" class="form-control" autofocus required <input type="text" name="FirstName" id="firstName" class="form-control" autofocus required
placeholder="First" value="{{ model.first_name }}"> placeholder="First" value="{{ model.first_name }}">
<label for="firstName">First Name</label> <label for="firstName">First Name</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-lg-4 pb-3"> <div class="col-12 col-md-6 col-lg-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="lastName" id="lastName" class="form-control" required <input type="text" name="LastName" id="lastName" class="form-control" required
placeholder="Last" value="{{ model.last_name }}"> placeholder="Last" value="{{ model.last_name }}">
<label for="lastName">Last Name</label> <label for="lastName">Last Name</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-lg-4 pb-3"> <div class="col-12 col-md-6 col-lg-4 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="text" name="preferredName" id="preferredName" class="form-control" required <input type="text" name="PreferredName" id="preferredName" class="form-control" required
placeholder="Preferred" value="{{ model.preferred_name }}"> placeholder="Preferred" value="{{ model.preferred_name }}">
<label for="preferredName">Preferred Name</label> <label for="preferredName">Preferred Name</label>
</div> </div>
@ -38,14 +38,14 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-6 pb-3"> <div class="col-12 col-md-6 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="password" name="newPassword" id="newPassword" class="form-control" <input type="password" name="NewPassword" id="newPassword" class="form-control"
placeholder="Password"> placeholder="Password">
<label for="newPassword">New Password</label> <label for="newPassword">New Password</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 pb-3"> <div class="col-12 col-md-6 pb-3">
<div class="form-floating"> <div class="form-floating">
<input type="password" name="newPasswordConfirm" id="newPasswordConfirm" class="form-control" <input type="password" name="NewPasswordConfirm" id="newPasswordConfirm" class="form-control"
placeholder="Confirm"> placeholder="Confirm">
<label for="newPasswordConfirm">Confirm New Password</label> <label for="newPasswordConfirm">Confirm New Password</label>
</div> </div>

View File

@ -56,7 +56,7 @@ this.Admin = {
const nameField = document.createElement("input") const nameField = document.createElement("input")
nameField.type = "text" nameField.type = "text"
nameField.name = "metaNames" nameField.name = "MetaNames"
nameField.id = `metaNames_${this.nextMetaIndex}` nameField.id = `metaNames_${this.nextMetaIndex}`
nameField.className = "form-control" nameField.className = "form-control"
nameField.placeholder = "Name" nameField.placeholder = "Name"
@ -94,7 +94,7 @@ this.Admin = {
const valueField = document.createElement("input") const valueField = document.createElement("input")
valueField.type = "text" valueField.type = "text"
valueField.name = "metaValues" valueField.name = "MetaValues"
valueField.id = `metaValues_${this.nextMetaIndex}` valueField.id = `metaValues_${this.nextMetaIndex}`
valueField.className = "form-control" valueField.className = "form-control"
valueField.placeholder = "Value" valueField.placeholder = "Value"
@ -182,7 +182,7 @@ this.Admin = {
// Link // Link
const linkField = document.createElement("input") const linkField = document.createElement("input")
linkField.type = "text" linkField.type = "text"
linkField.name = "prior" linkField.name = "Prior"
linkField.id = `prior_${this.nextPermalink}` linkField.id = `prior_${this.nextPermalink}`
linkField.className = "form-control" linkField.className = "form-control"
linkField.placeholder = "Link" linkField.placeholder = "Link"