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
/// Extensions on the DotLiquid Context object
type Context with
/// Get the current web log from the DotLiquid context /// Get the current web log from the DotLiquid context
let webLog (ctx : Context) = member this.WebLog = this.Environments[0].["web_log"] :?> WebLog
ctx.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,43 +417,42 @@ 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
@ -470,28 +469,27 @@ let editCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next
|] |]
|} |}
|> 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 (
Some
{ Page.empty with { Page.empty with
id = PageId.create () id = PageId.create ()
webLogId = ctx.WebLog.id webLogId = ctx.WebLog.id
authorId = ctx.UserId authorId = ctx.UserId
publishedOn = now 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
@ -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
| true -> Some ctx.Request.Query["returnUrl"].[0]
| false -> None
return!
Hash.FromAnonymousObject {| Hash.FromAnonymousObject {|
page_title = "Log On" page_title = "Log On"
csrf = ctx.CsrfTokenSet csrf = ctx.CsrfTokenSet
model = { LogOnModel.empty with returnTo = returnTo } model = { LogOnModel.empty with ReturnTo = returnTo }
|} |}
|> viewForTheme "admin" "log-on" next ctx |> 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"