Reassign child cats when deleting parent cat (#27)
- Create common page/post edit field template (#25) - Fix relative URL adjustment throughout - Fix upload name sanitization regex - Create modules within Admin handler module - Enable/disable podcast episode fields on page load - Fix upload destination casing in templates - Tweak default theme to show no posts found on index template - Update Bootstrap to 5.1.3 in default theme
This commit is contained in:
parent
6b49793fbb
commit
33698bd182
|
@ -5,6 +5,16 @@ open System.Threading.Tasks
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
open MyWebLog.ViewModels
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
|
/// The result of a category deletion attempt
|
||||||
|
type CategoryDeleteResult =
|
||||||
|
/// The category was deleted successfully
|
||||||
|
| CategoryDeleted
|
||||||
|
/// The category was deleted successfully, and its children were reassigned to its parent
|
||||||
|
| ReassignedChildCategories
|
||||||
|
/// The category was not found, so no effort was made to delete it
|
||||||
|
| CategoryNotFound
|
||||||
|
|
||||||
|
|
||||||
/// Data functions to support manipulating categories
|
/// Data functions to support manipulating categories
|
||||||
type ICategoryData =
|
type ICategoryData =
|
||||||
|
|
||||||
|
@ -18,7 +28,7 @@ type ICategoryData =
|
||||||
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<CategoryDeleteResult>
|
||||||
|
|
||||||
/// 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[]>
|
||||||
|
|
|
@ -274,7 +274,21 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
|
|
||||||
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 cat ->
|
||||||
|
// Reassign any children to the category's parent category
|
||||||
|
let! children = rethink<int> {
|
||||||
|
withTable Table.Category
|
||||||
|
filter (nameof Category.empty.ParentId) catId
|
||||||
|
count
|
||||||
|
result; withRetryDefault conn
|
||||||
|
}
|
||||||
|
if children > 0 then
|
||||||
|
do! rethink {
|
||||||
|
withTable Table.Category
|
||||||
|
filter (nameof Category.empty.ParentId) catId
|
||||||
|
update [ nameof Category.empty.ParentId, cat.ParentId :> obj ]
|
||||||
|
write; withRetryDefault; ignoreResult conn
|
||||||
|
}
|
||||||
// Delete the category off all posts where it is assigned
|
// Delete the category off all posts where it is assigned
|
||||||
do! rethink {
|
do! rethink {
|
||||||
withTable Table.Post
|
withTable Table.Post
|
||||||
|
@ -291,8 +305,8 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
delete
|
delete
|
||||||
write; withRetryDefault; ignoreResult conn
|
write; withRetryDefault; ignoreResult conn
|
||||||
}
|
}
|
||||||
return true
|
return if children = 0 then CategoryDeleted else ReassignedChildCategories
|
||||||
| None -> return false
|
| None -> return CategoryNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
member _.Restore cats = backgroundTask {
|
member _.Restore cats = backgroundTask {
|
||||||
|
|
|
@ -122,13 +122,23 @@ type SQLiteCategoryData (conn : SqliteConnection) =
|
||||||
/// Delete a category
|
/// Delete a category
|
||||||
let delete catId webLogId = backgroundTask {
|
let delete catId webLogId = backgroundTask {
|
||||||
match! findById catId webLogId with
|
match! findById catId webLogId with
|
||||||
| Some _ ->
|
| Some cat ->
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
|
// Reassign any children to the category's parent category
|
||||||
|
cmd.CommandText <- "SELECT COUNT(id) FROM category WHERE parent_id = @parentId"
|
||||||
|
cmd.Parameters.AddWithValue ("@parentId", CategoryId.toString catId) |> ignore
|
||||||
|
let! children = count cmd
|
||||||
|
if children > 0 then
|
||||||
|
cmd.CommandText <- "UPDATE category SET parent_id = @newParentId WHERE parent_id = @parentId"
|
||||||
|
cmd.Parameters.AddWithValue ("@newParentId", maybe (cat.ParentId |> Option.map CategoryId.toString))
|
||||||
|
|> ignore
|
||||||
|
do! write cmd
|
||||||
// Delete the category off all posts where it is assigned
|
// Delete the category off all posts where it is assigned
|
||||||
cmd.CommandText <- """
|
cmd.CommandText <- """
|
||||||
DELETE FROM post_category
|
DELETE FROM post_category
|
||||||
WHERE category_id = @id
|
WHERE category_id = @id
|
||||||
AND post_id IN (SELECT id FROM post WHERE web_log_id = @webLogId)"""
|
AND post_id IN (SELECT id FROM post WHERE web_log_id = @webLogId)"""
|
||||||
|
cmd.Parameters.Clear ()
|
||||||
let catIdParameter = cmd.Parameters.AddWithValue ("@id", CategoryId.toString catId)
|
let catIdParameter = cmd.Parameters.AddWithValue ("@id", CategoryId.toString catId)
|
||||||
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) |> ignore
|
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) |> ignore
|
||||||
do! write cmd
|
do! write cmd
|
||||||
|
@ -137,8 +147,8 @@ type SQLiteCategoryData (conn : SqliteConnection) =
|
||||||
cmd.Parameters.Clear ()
|
cmd.Parameters.Clear ()
|
||||||
cmd.Parameters.Add catIdParameter |> ignore
|
cmd.Parameters.Add catIdParameter |> ignore
|
||||||
do! write cmd
|
do! write cmd
|
||||||
return true
|
return if children = 0 then CategoryDeleted else ReassignedChildCategories
|
||||||
| None -> return false
|
| None -> return CategoryNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore categories from a backup
|
/// Restore categories from a backup
|
||||||
|
|
|
@ -12,6 +12,17 @@ module private Helpers =
|
||||||
match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed
|
match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed
|
||||||
|
|
||||||
|
|
||||||
|
/// Helper functions that are needed outside this file
|
||||||
|
[<AutoOpen>]
|
||||||
|
module PublicHelpers =
|
||||||
|
|
||||||
|
/// If the web log is not being served from the domain root, add the path information to relative URLs in page and
|
||||||
|
/// post text
|
||||||
|
let addBaseToRelativeUrls extra (text : string) =
|
||||||
|
if extra = "" then text
|
||||||
|
else text.Replace("href=\"/", $"href=\"{extra}/").Replace ("src=\"/", $"src=\"{extra}/")
|
||||||
|
|
||||||
|
|
||||||
/// The model used to display the admin dashboard
|
/// The model used to display the admin dashboard
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DashboardModel =
|
type DashboardModel =
|
||||||
|
@ -147,7 +158,7 @@ type DisplayPage =
|
||||||
UpdatedOn = page.UpdatedOn
|
UpdatedOn = page.UpdatedOn
|
||||||
IsInPageList = page.IsInPageList
|
IsInPageList = page.IsInPageList
|
||||||
IsDefault = pageId = webLog.DefaultPage
|
IsDefault = pageId = webLog.DefaultPage
|
||||||
Text = if extra = "" then page.Text else page.Text.Replace ("href=\"/", $"href=\"{extra}/")
|
Text = addBaseToRelativeUrls extra page.Text
|
||||||
Metadata = page.Metadata
|
Metadata = page.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1061,7 +1072,7 @@ type PostListItem =
|
||||||
Permalink = Permalink.toString post.Permalink
|
Permalink = Permalink.toString post.Permalink
|
||||||
PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable
|
PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable
|
||||||
UpdatedOn = inTZ post.UpdatedOn
|
UpdatedOn = inTZ post.UpdatedOn
|
||||||
Text = if extra = "" then post.Text else post.Text.Replace ("href=\"/", $"href=\"{extra}/")
|
Text = addBaseToRelativeUrls extra post.Text
|
||||||
CategoryIds = post.CategoryIds |> List.map CategoryId.toString
|
CategoryIds = post.CategoryIds |> List.map CategoryId.toString
|
||||||
Tags = post.Tags
|
Tags = post.Tags
|
||||||
Episode = post.Episode
|
Episode = post.Episode
|
||||||
|
|
|
@ -6,10 +6,11 @@ open Giraffe
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
open MyWebLog.ViewModels
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
// ~~ DASHBOARDS ~~
|
/// ~~ DASHBOARDS ~~
|
||||||
|
module Dashboard =
|
||||||
|
|
||||||
// GET /admin/dashboard
|
// GET /admin/dashboard
|
||||||
let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let user : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
let getCount (f : WebLogId -> Task<int>) = f ctx.WebLog.Id
|
let getCount (f : WebLogId -> Task<int>) = f ctx.WebLog.Id
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
let posts = getCount (data.Post.CountByStatus Published)
|
let posts = getCount (data.Post.CountByStatus Published)
|
||||||
|
@ -30,10 +31,10 @@ let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
TopLevelCategories = topCats.Result
|
TopLevelCategories = topCats.Result
|
||||||
}
|
}
|
||||||
|> adminView "dashboard" next ctx
|
|> adminView "dashboard" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/administration
|
// GET /admin/administration
|
||||||
let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
let admin : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
match! TemplateCache.get adminTheme "theme-list-body" ctx.Data with
|
match! TemplateCache.get adminTheme "theme-list-body" ctx.Data with
|
||||||
| Ok bodyTemplate ->
|
| Ok bodyTemplate ->
|
||||||
let! themes = ctx.Data.Theme.All ()
|
let! themes = ctx.Data.Theme.All ()
|
||||||
|
@ -41,7 +42,10 @@ let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx
|
||||||
let! hash =
|
let! hash =
|
||||||
hashForPage "myWebLog Administration"
|
hashForPage "myWebLog Administration"
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
|> addToHash "themes" (
|
||||||
|
themes
|
||||||
|
|> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse)
|
||||||
|
|> Array.ofList)
|
||||||
|> addToHash "cached_themes" (
|
|> addToHash "cached_themes" (
|
||||||
themes
|
themes
|
||||||
|> Seq.ofList
|
|> Seq.ofList
|
||||||
|
@ -65,22 +69,25 @@ let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx
|
||||||
addToHash "theme_list" (bodyTemplate.Render hash) hash
|
addToHash "theme_list" (bodyTemplate.Render hash) hash
|
||||||
|> adminView "admin-dashboard" next ctx
|
|> adminView "admin-dashboard" next ctx
|
||||||
| Error message -> return! Error.server message next ctx
|
| Error message -> return! Error.server message next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Redirect the user to the admin dashboard
|
/// Redirect the user to the admin dashboard
|
||||||
let toAdminDashboard : HttpHandler = redirectToGet "admin/administration"
|
let toAdminDashboard : HttpHandler = redirectToGet "admin/administration"
|
||||||
|
|
||||||
// ~~ CACHES ~~
|
|
||||||
|
|
||||||
// POST /admin/cache/web-log/{id}/refresh
|
/// ~~ CACHES ~~
|
||||||
let refreshWebLogCache webLogId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
module Cache =
|
||||||
|
|
||||||
|
// POST /admin/cache/web-log/{id}/refresh
|
||||||
|
let refreshWebLog webLogId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
if webLogId = "all" then
|
if webLogId = "all" then
|
||||||
do! WebLogCache.fill data
|
do! WebLogCache.fill data
|
||||||
for webLog in WebLogCache.all () do
|
for webLog in WebLogCache.all () do
|
||||||
do! PageListCache.refresh webLog data
|
do! PageListCache.refresh webLog data
|
||||||
do! CategoryCache.refresh webLog.Id data
|
do! CategoryCache.refresh webLog.Id data
|
||||||
do! addMessage ctx { UserMessage.success with Message = "Successfully refresh web log cache for all web logs" }
|
do! addMessage ctx
|
||||||
|
{ UserMessage.success with Message = "Successfully refresh web log cache for all web logs" }
|
||||||
else
|
else
|
||||||
match! data.WebLog.FindById (WebLogId webLogId) with
|
match! data.WebLog.FindById (WebLogId webLogId) with
|
||||||
| Some webLog ->
|
| Some webLog ->
|
||||||
|
@ -92,10 +99,10 @@ let refreshWebLogCache webLogId : HttpHandler = requireAccess Administrator >=>
|
||||||
| None ->
|
| None ->
|
||||||
do! addMessage ctx { UserMessage.error with Message = $"No web log exists with ID {webLogId}" }
|
do! addMessage ctx { UserMessage.error with Message = $"No web log exists with ID {webLogId}" }
|
||||||
return! toAdminDashboard next ctx
|
return! toAdminDashboard next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/cache/theme/{id}/refresh
|
// POST /admin/cache/theme/{id}/refresh
|
||||||
let refreshThemeCache themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
let refreshTheme themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
if themeId = "all" then
|
if themeId = "all" then
|
||||||
TemplateCache.empty ()
|
TemplateCache.empty ()
|
||||||
|
@ -116,12 +123,16 @@ let refreshThemeCache themeId : HttpHandler = requireAccess Administrator >=> fu
|
||||||
| None ->
|
| None ->
|
||||||
do! addMessage ctx { UserMessage.error with Message = $"No theme exists with ID {themeId}" }
|
do! addMessage ctx { UserMessage.error with Message = $"No theme exists with ID {themeId}" }
|
||||||
return! toAdminDashboard next ctx
|
return! toAdminDashboard next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// ~~ CATEGORIES ~~
|
|
||||||
|
|
||||||
// GET /admin/categories
|
/// ~~ CATEGORIES ~~
|
||||||
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
module Category =
|
||||||
|
|
||||||
|
open MyWebLog.Data
|
||||||
|
|
||||||
|
// GET /admin/categories
|
||||||
|
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
match! TemplateCache.get adminTheme "category-list-body" ctx.Data with
|
match! TemplateCache.get adminTheme "category-list-body" ctx.Data with
|
||||||
| Ok catListTemplate ->
|
| Ok catListTemplate ->
|
||||||
let! hash =
|
let! hash =
|
||||||
|
@ -132,17 +143,17 @@ let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
||||||
addToHash "category_list" (catListTemplate.Render hash) hash
|
addToHash "category_list" (catListTemplate.Render hash) hash
|
||||||
|> adminView "category-list" next ctx
|
|> adminView "category-list" next ctx
|
||||||
| Error message -> return! Error.server message next ctx
|
| Error message -> return! Error.server message next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/categories/bare
|
// GET /admin/categories/bare
|
||||||
let listCategoriesBare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
let bare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
||||||
hashForPage "Categories"
|
hashForPage "Categories"
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
|> adminBareView "category-list-body" next ctx
|
|> adminBareView "category-list-body" next ctx
|
||||||
|
|
||||||
|
|
||||||
// GET /admin/category/{id}/edit
|
// GET /admin/category/{id}/edit
|
||||||
let editCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let edit catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let! result = task {
|
let! result = task {
|
||||||
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" })
|
||||||
|
@ -159,10 +170,10 @@ let editCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ct
|
||||||
|> addToHash ViewContext.Model (EditCategoryModel.fromCategory cat)
|
|> addToHash ViewContext.Model (EditCategoryModel.fromCategory cat)
|
||||||
|> adminBareView "category-edit" next ctx
|
|> adminBareView "category-edit" next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/category/save
|
// POST /admin/category/save
|
||||||
let saveCategory : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let save : 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 =
|
let category =
|
||||||
|
@ -180,43 +191,55 @@ let saveCategory : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> t
|
||||||
do! (if model.IsNew then data.Category.Add else data.Category.Update) updatedCat
|
do! (if model.IsNew then data.Category.Add else data.Category.Update) updatedCat
|
||||||
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! bare 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 delete catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
match! ctx.Data.Category.Delete (CategoryId catId) ctx.WebLog.Id with
|
let! result = ctx.Data.Category.Delete (CategoryId catId) ctx.WebLog.Id
|
||||||
| true ->
|
match result with
|
||||||
|
| CategoryDeleted
|
||||||
|
| ReassignedChildCategories ->
|
||||||
do! CategoryCache.update ctx
|
do! CategoryCache.update ctx
|
||||||
do! addMessage ctx { UserMessage.success with Message = "Category deleted successfully" }
|
let detail =
|
||||||
| false -> do! addMessage ctx { UserMessage.error with Message = "Category not found; cannot delete" }
|
match result with
|
||||||
return! listCategoriesBare next ctx
|
| ReassignedChildCategories ->
|
||||||
}
|
Some "<em>(Its child categories were reassigned to its parent category)</em>"
|
||||||
|
| _ -> None
|
||||||
|
do! addMessage ctx { UserMessage.success with Message = "Category deleted successfully"; Detail = detail }
|
||||||
|
| CategoryNotFound ->
|
||||||
|
do! addMessage ctx { UserMessage.error with Message = "Category not found; cannot delete" }
|
||||||
|
return! bare next ctx
|
||||||
|
}
|
||||||
|
|
||||||
open Microsoft.AspNetCore.Http
|
|
||||||
|
|
||||||
// ~~ TAG MAPPINGS ~~
|
/// ~~ TAG MAPPINGS ~~
|
||||||
|
module TagMapping =
|
||||||
|
|
||||||
/// Add tag mappings to the given hash
|
open Microsoft.AspNetCore.Http
|
||||||
let private withTagMappings (ctx : HttpContext) hash = task {
|
|
||||||
|
/// Add tag mappings to the given hash
|
||||||
|
let withTagMappings (ctx : HttpContext) hash = task {
|
||||||
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
|
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
|
||||||
return
|
return
|
||||||
addToHash "mappings" mappings hash
|
addToHash "mappings" mappings hash
|
||||||
|> addToHash "mapping_ids" (mappings |> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id }))
|
|> addToHash "mapping_ids" (
|
||||||
}
|
mappings
|
||||||
|
|> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id }))
|
||||||
|
}
|
||||||
|
|
||||||
// GET /admin/settings/tag-mappings
|
// GET /admin/settings/tag-mappings
|
||||||
let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let! hash =
|
let! hash =
|
||||||
hashForPage ""
|
hashForPage ""
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
|> withTagMappings ctx
|
|> withTagMappings ctx
|
||||||
return! adminBareView "tag-mapping-list-body" next ctx hash
|
return! adminBareView "tag-mapping-list-body" next ctx hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/settings/tag-mapping/{id}/edit
|
// GET /admin/settings/tag-mapping/{id}/edit
|
||||||
let editMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let edit tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let isNew = tagMapId = "new"
|
let isNew = tagMapId = "new"
|
||||||
let tagMap =
|
let tagMap =
|
||||||
if isNew then someTask { TagMap.empty with Id = TagMapId "new" }
|
if isNew then someTask { TagMap.empty with Id = TagMapId "new" }
|
||||||
|
@ -229,10 +252,10 @@ let editMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next
|
||||||
|> addToHash ViewContext.Model (EditTagMapModel.fromMapping tm)
|
|> addToHash ViewContext.Model (EditTagMapModel.fromMapping tm)
|
||||||
|> adminBareView "tag-mapping-edit" next ctx
|
|> adminBareView "tag-mapping-edit" next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/settings/tag-mapping/save
|
// POST /admin/settings/tag-mapping/save
|
||||||
let saveMapping : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
let! model = ctx.BindFormAsync<EditTagMapModel> ()
|
let! model = ctx.BindFormAsync<EditTagMapModel> ()
|
||||||
let tagMap =
|
let tagMap =
|
||||||
|
@ -242,44 +265,46 @@ let saveMapping : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
|
||||||
| 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! tagMappings next ctx
|
return! all 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 delete 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! tagMappings next ctx
|
return! all next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// ~~ THEMES ~~
|
|
||||||
|
|
||||||
open System
|
/// ~~ THEMES ~~
|
||||||
open System.IO
|
module Theme =
|
||||||
open System.IO.Compression
|
|
||||||
open System.Text.RegularExpressions
|
|
||||||
open MyWebLog.Data
|
|
||||||
|
|
||||||
// GET /admin/theme/list
|
open System
|
||||||
let listThemes : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
open System.IO
|
||||||
|
open System.IO.Compression
|
||||||
|
open System.Text.RegularExpressions
|
||||||
|
open MyWebLog.Data
|
||||||
|
|
||||||
|
// GET /admin/theme/list
|
||||||
|
let all : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
let! themes = ctx.Data.Theme.All ()
|
let! themes = ctx.Data.Theme.All ()
|
||||||
return!
|
return!
|
||||||
hashForPage "Themes"
|
hashForPage "Themes"
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
||||||
|> adminBareView "theme-list-body" next ctx
|
|> adminBareView "theme-list-body" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/theme/new
|
// GET /admin/theme/new
|
||||||
let addTheme : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
let add : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
||||||
hashForPage "Upload a Theme File"
|
hashForPage "Upload a Theme File"
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
|> adminBareView "theme-upload" next ctx
|
|> adminBareView "theme-upload" 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 {
|
||||||
let now () = DateTime.UtcNow.ToString "yyyyMMdd.HHmm"
|
let now () = DateTime.UtcNow.ToString "yyyyMMdd.HHmm"
|
||||||
match zip.Entries |> Seq.filter (fun it -> it.FullName = "version.txt") |> Seq.tryHead with
|
match zip.Entries |> Seq.filter (fun it -> it.FullName = "version.txt") |> Seq.tryHead with
|
||||||
| Some versionItem ->
|
| Some versionItem ->
|
||||||
|
@ -290,10 +315,10 @@ let private updateNameAndVersion (theme : Theme) (zip : ZipArchive) = background
|
||||||
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 -> return { theme with Name = ThemeId.toString theme.Id; Version = now () }
|
| None -> return { theme with Name = ThemeId.toString theme.Id; Version = now () }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the theme with all templates from the ZIP archive
|
/// Update the theme with all templates from the ZIP archive
|
||||||
let private updateTemplates (theme : Theme) (zip : ZipArchive) = backgroundTask {
|
let private updateTemplates (theme : Theme) (zip : ZipArchive) = backgroundTask {
|
||||||
let tasks =
|
let tasks =
|
||||||
zip.Entries
|
zip.Entries
|
||||||
|> Seq.filter (fun it -> it.Name.EndsWith ".liquid")
|
|> Seq.filter (fun it -> it.Name.EndsWith ".liquid")
|
||||||
|
@ -308,10 +333,10 @@ let private updateTemplates (theme : Theme) (zip : ZipArchive) = backgroundTask
|
||||||
|> Array.fold (fun t template ->
|
|> Array.fold (fun t template ->
|
||||||
{ t with Templates = template :: (t.Templates |> List.filter (fun it -> it.Name <> template.Name)) })
|
{ t with Templates = template :: (t.Templates |> List.filter (fun it -> it.Name <> template.Name)) })
|
||||||
theme
|
theme
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update theme assets from the ZIP archive
|
/// Update theme assets from the ZIP archive
|
||||||
let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundTask {
|
let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundTask {
|
||||||
for asset in zip.Entries |> Seq.filter (fun it -> it.FullName.StartsWith "wwwroot") do
|
for asset in zip.Entries |> Seq.filter (fun it -> it.FullName.StartsWith "wwwroot") do
|
||||||
let assetName = asset.FullName.Replace ("wwwroot/", "")
|
let assetName = asset.FullName.Replace ("wwwroot/", "")
|
||||||
if assetName <> "" && not (assetName.EndsWith "/") then
|
if assetName <> "" && not (assetName.EndsWith "/") then
|
||||||
|
@ -322,10 +347,10 @@ let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundT
|
||||||
UpdatedOn = asset.LastWriteTime.DateTime
|
UpdatedOn = asset.LastWriteTime.DateTime
|
||||||
Data = stream.ToArray ()
|
Data = stream.ToArray ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the theme name from the file name given
|
/// Derive the theme ID from the file name given
|
||||||
let getThemeIdFromFileName (fileName : string) =
|
let deriveIdFromFileName (fileName : string) =
|
||||||
let themeName = fileName.Split(".").[0].ToLowerInvariant().Replace (" ", "-")
|
let themeName = fileName.Split(".").[0].ToLowerInvariant().Replace (" ", "-")
|
||||||
if themeName.EndsWith "-theme" then
|
if themeName.EndsWith "-theme" then
|
||||||
if Regex.IsMatch (themeName, """^[a-z0-9\-]+$""") then
|
if Regex.IsMatch (themeName, """^[a-z0-9\-]+$""") then
|
||||||
|
@ -333,8 +358,8 @@ let getThemeIdFromFileName (fileName : string) =
|
||||||
else Error $"Theme ID {fileName} is invalid"
|
else Error $"Theme ID {fileName} is invalid"
|
||||||
else Error "Theme .zip file name must end in \"-theme.zip\""
|
else Error "Theme .zip file name must end in \"-theme.zip\""
|
||||||
|
|
||||||
/// Load a theme from the given stream, which should contain a ZIP archive
|
/// Load a theme from the given stream, which should contain a ZIP archive
|
||||||
let loadThemeFromZip themeId file (data : IData) = backgroundTask {
|
let loadFromZip themeId file (data : IData) = backgroundTask {
|
||||||
let! isNew, theme = backgroundTask {
|
let! isNew, theme = backgroundTask {
|
||||||
match! data.Theme.FindById themeId with
|
match! data.Theme.FindById themeId with
|
||||||
| Some t -> return false, t
|
| Some t -> return false, t
|
||||||
|
@ -348,13 +373,13 @@ let loadThemeFromZip themeId file (data : IData) = backgroundTask {
|
||||||
do! updateAssets themeId zip data
|
do! updateAssets themeId zip data
|
||||||
|
|
||||||
return theme
|
return theme
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/theme/new
|
// POST /admin/theme/new
|
||||||
let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
let save : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
if ctx.Request.HasFormContentType && ctx.Request.Form.Files.Count > 0 then
|
if ctx.Request.HasFormContentType && ctx.Request.Form.Files.Count > 0 then
|
||||||
let themeFile = Seq.head ctx.Request.Form.Files
|
let themeFile = Seq.head ctx.Request.Form.Files
|
||||||
match getThemeIdFromFileName themeFile.FileName with
|
match deriveIdFromFileName themeFile.FileName with
|
||||||
| Ok themeId when themeId <> adminTheme ->
|
| Ok themeId when themeId <> adminTheme ->
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
let! exists = data.Theme.Exists themeId
|
let! exists = data.Theme.Exists themeId
|
||||||
|
@ -364,7 +389,7 @@ let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> ta
|
||||||
// Load the theme to the database
|
// Load the theme to the database
|
||||||
use stream = new MemoryStream ()
|
use stream = new MemoryStream ()
|
||||||
do! themeFile.CopyToAsync stream
|
do! themeFile.CopyToAsync stream
|
||||||
let! _ = loadThemeFromZip themeId stream data
|
let! _ = loadFromZip themeId stream data
|
||||||
do! ThemeAssetCache.refreshTheme themeId data
|
do! ThemeAssetCache.refreshTheme themeId data
|
||||||
TemplateCache.invalidateTheme themeId
|
TemplateCache.invalidateTheme themeId
|
||||||
// Save the .zip file
|
// Save the .zip file
|
||||||
|
@ -388,37 +413,40 @@ let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> ta
|
||||||
do! addMessage ctx { UserMessage.error with Message = message }
|
do! addMessage ctx { UserMessage.error with Message = message }
|
||||||
return! toAdminDashboard next ctx
|
return! toAdminDashboard next ctx
|
||||||
else return! RequestErrors.BAD_REQUEST "Bad request" next ctx
|
else return! RequestErrors.BAD_REQUEST "Bad request" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/theme/{id}/delete
|
// POST /admin/theme/{id}/delete
|
||||||
let deleteTheme themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
let delete themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
match themeId with
|
match themeId with
|
||||||
| "admin" | "default" ->
|
| "admin" | "default" ->
|
||||||
do! addMessage ctx { UserMessage.error with Message = $"You may not delete the {themeId} theme" }
|
do! addMessage ctx { UserMessage.error with Message = $"You may not delete the {themeId} theme" }
|
||||||
return! listThemes next ctx
|
return! all next ctx
|
||||||
| it when WebLogCache.isThemeInUse (ThemeId it) ->
|
| it when WebLogCache.isThemeInUse (ThemeId it) ->
|
||||||
do! addMessage ctx
|
do! addMessage ctx
|
||||||
{ UserMessage.error with
|
{ UserMessage.error with
|
||||||
Message = $"You may not delete the {themeId} theme, as it is currently in use"
|
Message = $"You may not delete the {themeId} theme, as it is currently in use"
|
||||||
}
|
}
|
||||||
return! listThemes next ctx
|
return! all next ctx
|
||||||
| _ ->
|
| _ ->
|
||||||
match! data.Theme.Delete (ThemeId themeId) with
|
match! data.Theme.Delete (ThemeId themeId) with
|
||||||
| true ->
|
| true ->
|
||||||
let zippedTheme = $"{themeId}-theme.zip"
|
let zippedTheme = $"{themeId}-theme.zip"
|
||||||
if File.Exists zippedTheme then File.Delete zippedTheme
|
if File.Exists zippedTheme then File.Delete zippedTheme
|
||||||
do! addMessage ctx { UserMessage.success with Message = $"Theme ID {themeId} deleted successfully" }
|
do! addMessage ctx { UserMessage.success with Message = $"Theme ID {themeId} deleted successfully" }
|
||||||
return! listThemes next ctx
|
return! all next ctx
|
||||||
| false -> return! Error.notFound next ctx
|
| false -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// ~~ WEB LOG SETTINGS ~~
|
|
||||||
|
|
||||||
open System.Collections.Generic
|
/// ~~ WEB LOG SETTINGS ~~
|
||||||
|
module WebLog =
|
||||||
|
|
||||||
// GET /admin/settings
|
open System.Collections.Generic
|
||||||
let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
open System.IO
|
||||||
|
|
||||||
|
// GET /admin/settings
|
||||||
|
let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
match! TemplateCache.get adminTheme "user-list-body" data with
|
match! TemplateCache.get adminTheme "user-list-body" data with
|
||||||
| Ok userTemplate ->
|
| Ok userTemplate ->
|
||||||
|
@ -442,7 +470,8 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
|
||||||
|> addToHash "themes" (
|
|> addToHash "themes" (
|
||||||
themes
|
themes
|
||||||
|> Seq.ofList
|
|> Seq.ofList
|
||||||
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|
|> Seq.map (fun it ->
|
||||||
|
KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|
||||||
|> Array.ofSeq)
|
|> Array.ofSeq)
|
||||||
|> addToHash "upload_values" [|
|
|> addToHash "upload_values" [|
|
||||||
KeyValuePair.Create (UploadDestination.toString Database, "Database")
|
KeyValuePair.Create (UploadDestination.toString Database, "Database")
|
||||||
|
@ -455,17 +484,17 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
|
||||||
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|
||||||
|> Array.ofList)
|
|> Array.ofList)
|
||||||
|> addViewContext ctx
|
|> addViewContext ctx
|
||||||
let! hash' = withTagMappings ctx hash
|
let! hash' = TagMapping.withTagMappings ctx hash
|
||||||
return!
|
return!
|
||||||
addToHash "user_list" (userTemplate.Render hash') hash'
|
addToHash "user_list" (userTemplate.Render hash') hash'
|
||||||
|> addToHash "tag_mapping_list" (tagMapTemplate.Render hash')
|
|> addToHash "tag_mapping_list" (tagMapTemplate.Render hash')
|
||||||
|> adminView "settings" next ctx
|
|> adminView "settings" next ctx
|
||||||
| Error message -> return! Error.server message next ctx
|
| Error message -> return! Error.server message next ctx
|
||||||
| Error message -> return! Error.server message next ctx
|
| Error message -> return! Error.server message next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/settings
|
// POST /admin/settings
|
||||||
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
|
||||||
|
@ -486,4 +515,4 @@ let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> t
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,8 +124,14 @@ let private findPageRevision pgId revDate (ctx : HttpContext) = task {
|
||||||
let previewRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let previewRevision (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 ->
|
||||||
|
let _, extra = WebLog.hostAndPath ctx.WebLog
|
||||||
return! {|
|
return! {|
|
||||||
content = $"""<div class="mwl-revision-preview mb-3">{MarkupText.toHtml rev.Text}</div>"""
|
content =
|
||||||
|
[ """<div class="mwl-revision-preview mb-3">"""
|
||||||
|
(MarkupText.toHtml >> addBaseToRelativeUrls extra) rev.Text
|
||||||
|
"</div>"
|
||||||
|
]
|
||||||
|
|> String.concat ""
|
||||||
|}
|
|}
|
||||||
|> makeHash |> adminBareView "" next ctx
|
|> makeHash |> adminBareView "" next ctx
|
||||||
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
||||||
|
|
|
@ -329,8 +329,14 @@ let private findPostRevision postId revDate (ctx : HttpContext) = task {
|
||||||
let previewRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let previewRevision (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 ->
|
||||||
|
let _, extra = WebLog.hostAndPath ctx.WebLog
|
||||||
return! {|
|
return! {|
|
||||||
content = $"""<div class="mwl-revision-preview mb-3">{MarkupText.toHtml rev.Text}</div>"""
|
content =
|
||||||
|
[ """<div class="mwl-revision-preview mb-3">"""
|
||||||
|
(MarkupText.toHtml >> addBaseToRelativeUrls extra) rev.Text
|
||||||
|
"</div>"
|
||||||
|
]
|
||||||
|
|> String.concat ""
|
||||||
|}
|
|}
|
||||||
|> makeHash |> adminBareView "" next ctx
|
|> makeHash |> adminBareView "" next ctx
|
||||||
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
||||||
|
|
|
@ -106,13 +106,13 @@ let router : HttpHandler = choose [
|
||||||
]
|
]
|
||||||
subRoute "/admin" (requireUser >=> choose [
|
subRoute "/admin" (requireUser >=> choose [
|
||||||
GET_HEAD >=> choose [
|
GET_HEAD >=> choose [
|
||||||
route "/administration" >=> Admin.adminDashboard
|
route "/administration" >=> Admin.Dashboard.admin
|
||||||
subRoute "/categor" (choose [
|
subRoute "/categor" (choose [
|
||||||
route "ies" >=> Admin.listCategories
|
route "ies" >=> Admin.Category.all
|
||||||
route "ies/bare" >=> Admin.listCategoriesBare
|
route "ies/bare" >=> Admin.Category.bare
|
||||||
routef "y/%s/edit" Admin.editCategory
|
routef "y/%s/edit" Admin.Category.edit
|
||||||
])
|
])
|
||||||
route "/dashboard" >=> Admin.dashboard
|
route "/dashboard" >=> Admin.Dashboard.user
|
||||||
route "/my-info" >=> User.myInfo
|
route "/my-info" >=> User.myInfo
|
||||||
subRoute "/page" (choose [
|
subRoute "/page" (choose [
|
||||||
route "s" >=> Page.all 1
|
route "s" >=> Page.all 1
|
||||||
|
@ -131,20 +131,20 @@ let router : HttpHandler = choose [
|
||||||
routef "/%s/revisions" Post.editRevisions
|
routef "/%s/revisions" Post.editRevisions
|
||||||
])
|
])
|
||||||
subRoute "/settings" (choose [
|
subRoute "/settings" (choose [
|
||||||
route "" >=> Admin.settings
|
route "" >=> Admin.WebLog.settings
|
||||||
routef "/rss/%s/edit" Feed.editCustomFeed
|
routef "/rss/%s/edit" Feed.editCustomFeed
|
||||||
subRoute "/user" (choose [
|
subRoute "/user" (choose [
|
||||||
route "s" >=> User.all
|
route "s" >=> User.all
|
||||||
routef "/%s/edit" User.edit
|
routef "/%s/edit" User.edit
|
||||||
])
|
])
|
||||||
subRoute "/tag-mapping" (choose [
|
subRoute "/tag-mapping" (choose [
|
||||||
route "s" >=> Admin.tagMappings
|
route "s" >=> Admin.TagMapping.all
|
||||||
routef "/%s/edit" Admin.editMapping
|
routef "/%s/edit" Admin.TagMapping.edit
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
subRoute "/theme" (choose [
|
subRoute "/theme" (choose [
|
||||||
route "/list" >=> Admin.listThemes
|
route "/list" >=> Admin.Theme.all
|
||||||
route "/new" >=> Admin.addTheme
|
route "/new" >=> Admin.Theme.add
|
||||||
])
|
])
|
||||||
subRoute "/upload" (choose [
|
subRoute "/upload" (choose [
|
||||||
route "s" >=> Upload.list
|
route "s" >=> Upload.list
|
||||||
|
@ -153,12 +153,12 @@ let router : HttpHandler = choose [
|
||||||
]
|
]
|
||||||
POST >=> validateCsrf >=> choose [
|
POST >=> validateCsrf >=> choose [
|
||||||
subRoute "/cache" (choose [
|
subRoute "/cache" (choose [
|
||||||
routef "/theme/%s/refresh" Admin.refreshThemeCache
|
routef "/theme/%s/refresh" Admin.Cache.refreshTheme
|
||||||
routef "/web-log/%s/refresh" Admin.refreshWebLogCache
|
routef "/web-log/%s/refresh" Admin.Cache.refreshWebLog
|
||||||
])
|
])
|
||||||
subRoute "/category" (choose [
|
subRoute "/category" (choose [
|
||||||
route "/save" >=> Admin.saveCategory
|
route "/save" >=> Admin.Category.save
|
||||||
routef "/%s/delete" Admin.deleteCategory
|
routef "/%s/delete" Admin.Category.delete
|
||||||
])
|
])
|
||||||
route "/my-info" >=> User.saveMyInfo
|
route "/my-info" >=> User.saveMyInfo
|
||||||
subRoute "/page" (choose [
|
subRoute "/page" (choose [
|
||||||
|
@ -178,15 +178,15 @@ let router : HttpHandler = choose [
|
||||||
routef "/%s/revisions/purge" Post.purgeRevisions
|
routef "/%s/revisions/purge" Post.purgeRevisions
|
||||||
])
|
])
|
||||||
subRoute "/settings" (choose [
|
subRoute "/settings" (choose [
|
||||||
route "" >=> Admin.saveSettings
|
route "" >=> Admin.WebLog.saveSettings
|
||||||
subRoute "/rss" (choose [
|
subRoute "/rss" (choose [
|
||||||
route "" >=> Feed.saveSettings
|
route "" >=> Feed.saveSettings
|
||||||
route "/save" >=> Feed.saveCustomFeed
|
route "/save" >=> Feed.saveCustomFeed
|
||||||
routef "/%s/delete" Feed.deleteCustomFeed
|
routef "/%s/delete" Feed.deleteCustomFeed
|
||||||
])
|
])
|
||||||
subRoute "/tag-mapping" (choose [
|
subRoute "/tag-mapping" (choose [
|
||||||
route "/save" >=> Admin.saveMapping
|
route "/save" >=> Admin.TagMapping.save
|
||||||
routef "/%s/delete" Admin.deleteMapping
|
routef "/%s/delete" Admin.TagMapping.delete
|
||||||
])
|
])
|
||||||
subRoute "/user" (choose [
|
subRoute "/user" (choose [
|
||||||
route "/save" >=> User.save
|
route "/save" >=> User.save
|
||||||
|
@ -194,8 +194,8 @@ let router : HttpHandler = choose [
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
subRoute "/theme" (choose [
|
subRoute "/theme" (choose [
|
||||||
route "/new" >=> Admin.saveTheme
|
route "/new" >=> Admin.Theme.save
|
||||||
routef "/%s/delete" Admin.deleteTheme
|
routef "/%s/delete" Admin.Theme.delete
|
||||||
])
|
])
|
||||||
subRoute "/upload" (choose [
|
subRoute "/upload" (choose [
|
||||||
route "/save" >=> Upload.save
|
route "/save" >=> Upload.save
|
||||||
|
|
|
@ -85,7 +85,7 @@ open System.Text.RegularExpressions
|
||||||
open MyWebLog.ViewModels
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
/// Turn a string into a lowercase URL-safe slug
|
/// Turn a string into a lowercase URL-safe slug
|
||||||
let makeSlug it = ((Regex """\s+""").Replace ((Regex "[^A-z0-9 ]").Replace (it, ""), "-")).ToLowerInvariant ()
|
let makeSlug it = ((Regex """\s+""").Replace ((Regex "[^A-z0-9 -]").Replace (it, ""), "-")).ToLowerInvariant ()
|
||||||
|
|
||||||
// GET /admin/uploads
|
// GET /admin/uploads
|
||||||
let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
|
|
|
@ -51,7 +51,10 @@ let doLogOn : HttpHandler = fun next ctx -> task {
|
||||||
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
|
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
|
||||||
do! data.WebLogUser.SetLastSeen user.Id user.WebLogId
|
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 = "Log on successful"
|
||||||
|
Detail = Some $"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
|
||||||
|
|
|
@ -137,13 +137,13 @@ let loadTheme (args : string[]) (sp : IServiceProvider) = task {
|
||||||
match args[1].LastIndexOf Path.DirectorySeparatorChar with
|
match args[1].LastIndexOf Path.DirectorySeparatorChar with
|
||||||
| -1 -> args[1]
|
| -1 -> args[1]
|
||||||
| it -> args[1][(it + 1)..]
|
| it -> args[1][(it + 1)..]
|
||||||
match Handlers.Admin.getThemeIdFromFileName fileName with
|
match Handlers.Admin.Theme.deriveIdFromFileName fileName with
|
||||||
| Ok themeId ->
|
| Ok themeId ->
|
||||||
let data = sp.GetRequiredService<IData> ()
|
let data = sp.GetRequiredService<IData> ()
|
||||||
use stream = File.Open (args[1], FileMode.Open)
|
use stream = File.Open (args[1], FileMode.Open)
|
||||||
use copy = new MemoryStream ()
|
use copy = new MemoryStream ()
|
||||||
do! stream.CopyToAsync copy
|
do! stream.CopyToAsync copy
|
||||||
let! theme = Handlers.Admin.loadThemeFromZip themeId copy data
|
let! theme = Handlers.Admin.Theme.loadFromZip themeId copy data
|
||||||
let fac = sp.GetRequiredService<ILoggerFactory> ()
|
let fac = sp.GetRequiredService<ILoggerFactory> ()
|
||||||
let log = fac.CreateLogger "MyWebLog.Themes"
|
let log = fac.CreateLogger "MyWebLog.Themes"
|
||||||
log.LogInformation $"{theme.Name} v{theme.Version} ({ThemeId.toString theme.Id}) loaded"
|
log.LogInformation $"{theme.Name} v{theme.Version} ({ThemeId.toString theme.Id}) loaded"
|
||||||
|
|
32
src/admin-theme/_edit-common.liquid
Normal file
32
src/admin-theme/_edit-common.liquid
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<div class="form-floating pb-3">
|
||||||
|
<input type="text" name="Title" id="title" class="form-control" placeholder="Title" autofocus required
|
||||||
|
value="{{ model.title }}">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating pb-3">
|
||||||
|
<input type="text" name="Permalink" id="permalink" class="form-control" placeholder="Permalink" required
|
||||||
|
value="{{ model.permalink }}">
|
||||||
|
<label for="permalink">Permalink</label>
|
||||||
|
{%- unless model.is_new %}
|
||||||
|
{%- assign entity_url_base = "admin/" | append: entity | append: "/" | append: entity_id -%}
|
||||||
|
<span class="form-text">
|
||||||
|
<a href="{{ entity_url_base | append: "/permalinks" | relative_link }}">Manage Permalinks</a>
|
||||||
|
<span class="text-muted"> • </span>
|
||||||
|
<a href="{{ entity_url_base | append: "/revisions" | relative_link }}">Manage Revisions</a>
|
||||||
|
</span>
|
||||||
|
{%- endunless -%}
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="text">Text</label>
|
||||||
|
<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"
|
||||||
|
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
|
||||||
|
<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"
|
||||||
|
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
|
||||||
|
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pb-3">
|
||||||
|
<textarea name="Text" id="text" class="form-control" rows="20">{{ model.text }}</textarea>
|
||||||
|
</div>
|
|
@ -2,8 +2,6 @@
|
||||||
<article>
|
<article>
|
||||||
<fieldset class="container mb-3 pb-0">
|
<fieldset class="container mb-3 pb-0">
|
||||||
<legend>Themes</legend>
|
<legend>Themes</legend>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<a href="{{ "admin/theme/new" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
<a href="{{ "admin/theme/new" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||||
hx-target="#theme_new">
|
hx-target="#theme_new">
|
||||||
Upload a New Theme
|
Upload a New Theme
|
||||||
|
@ -18,8 +16,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row mwl-table-detail" id="theme_new"></div>
|
<div class="row mwl-table-detail" id="theme_new"></div>
|
||||||
{{ theme_list }}
|
{{ theme_list }}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="container mb-3 pb-0">
|
<fieldset class="container mb-3 pb-0">
|
||||||
{%- assign cache_base_url = "admin/cache/" -%}
|
{%- assign cache_base_url = "admin/cache/" -%}
|
||||||
|
@ -27,10 +23,9 @@
|
||||||
<div class="row pb-2">
|
<div class="row pb-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<p>
|
<p>
|
||||||
myWebLog uses a few caches to ensure that it serves pages as fast as possible. Normal actions taken within the
|
myWebLog uses a few caches to ensure that it serves pages as fast as possible.
|
||||||
admin area will keep these up to date; however, if changes occur outside of the system (creating a new web log
|
(<a href="https://bitbadger.solutions/open-source/myweblog/advanced.html#cache-management"
|
||||||
via CLI, loading an updated theme via CLI, direct data updates, etc.), these options allow for the caches to
|
target="_blank">more information</a>)
|
||||||
be refreshed without requiring you to restart the application.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,21 +7,21 @@
|
||||||
<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" placeholder="Name" autofocus required
|
||||||
required value="{{ model.name | escape }}">
|
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" 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">
|
||||||
<option value=""{% if model.parent_id == "" %} selected="selected"{% endif %}>
|
<option value=""{% if model.parent_id == "" %} selected="selected"{% endif %}>
|
||||||
– None –
|
– None –
|
||||||
</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"
|
||||||
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>
|
||||||
|
|
|
@ -14,18 +14,6 @@
|
||||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
{{ htmx_script }}
|
{{ htmx_script }}
|
||||||
<script>
|
|
||||||
const cssLoaded = [...document.styleSheets].filter(it => it.href.indexOf("bootstrap.min.css") > -1).length > 0
|
|
||||||
if (!cssLoaded) {
|
|
||||||
const local = document.createElement("link")
|
|
||||||
local.rel = "stylesheet"
|
|
||||||
local.href = "{{ "themes/admin/bootstrap.min.css" | relative_link }}"
|
|
||||||
document.getElementsByTagName("link")[0].prepend(local)
|
|
||||||
}
|
|
||||||
setTimeout(function () {
|
|
||||||
if (!bootstrap) document.write('<script src=\"{{ "script/bootstrap.bundle.min.js" | relative_link }}\"><\/script>')
|
|
||||||
}, 2000)
|
|
||||||
</script>
|
|
||||||
<script src="{{ "themes/admin/admin.js" | relative_link }}"></script>
|
<script src="{{ "themes/admin/admin.js" | relative_link }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -6,39 +6,9 @@
|
||||||
<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">
|
{%- assign entity = "page" -%}
|
||||||
<input type="text" name="Title" id="title" class="form-control" autofocus required
|
{%- assign entity_id = model.page_id -%}
|
||||||
value="{{ model.title }}">
|
{% include_template "_edit-common" %}
|
||||||
<label for="title">Title</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating pb-3">
|
|
||||||
<input type="text" name="Permalink" id="permalink" class="form-control" required
|
|
||||||
value="{{ model.permalink }}">
|
|
||||||
<label for="permalink">Permalink</label>
|
|
||||||
{%- unless model.is_new %}
|
|
||||||
<span class="form-text">
|
|
||||||
<a href="{{ "admin/page/" | append: model.page_id | append: "/permalinks" | relative_link }}">
|
|
||||||
Manage Permalinks
|
|
||||||
</a>
|
|
||||||
<span class="text-muted"> • </span>
|
|
||||||
<a href="{{ "admin/page/" | append: model.page_id | append: "/revisions" | relative_link }}">
|
|
||||||
Manage Revisions
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
{% endunless -%}
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<label for="text">Text</label>
|
|
||||||
<input type="radio" name="Source" id="source_html" class="btn-check" value="HTML"
|
|
||||||
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
|
|
||||||
<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"
|
|
||||||
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
|
|
||||||
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<textarea name="Text" id="text" class="form-control">{{ model.text }}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<div class="form-floating pb-3">
|
<div class="form-floating pb-3">
|
||||||
|
|
|
@ -6,41 +6,9 @@
|
||||||
<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">
|
{%- assign entity = "post" -%}
|
||||||
<input type="text" name="Title" id="title" class="form-control" placeholder="Title" autofocus required
|
{%- assign entity_id = model.post_id -%}
|
||||||
value="{{ model.title }}">
|
{% include_template "_edit-common" %}
|
||||||
<label for="title">Title</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating pb-3">
|
|
||||||
<input type="text" name="Permalink" id="permalink" class="form-control" placeholder="Permalink" required
|
|
||||||
value="{{ model.permalink }}">
|
|
||||||
<label for="permalink">Permalink</label>
|
|
||||||
{%- unless model.is_new %}
|
|
||||||
<span class="form-text">
|
|
||||||
<a href="{{ "admin/post/" | append: model.post_id | append: "/permalinks" | relative_link }}">
|
|
||||||
Manage Permalinks
|
|
||||||
</a>
|
|
||||||
<span class="text-muted"> • </span>
|
|
||||||
<a href="{{ "admin/post/" | append: model.post_id | append: "/revisions" | relative_link }}">
|
|
||||||
Manage Revisions
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
{% endunless -%}
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<label for="text">Text</label>
|
|
||||||
<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"
|
|
||||||
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
|
|
||||||
<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"
|
|
||||||
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
|
|
||||||
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pb-3">
|
|
||||||
<textarea name="Text" id="text" class="form-control" rows="20">{{ model.text }}</textarea>
|
|
||||||
</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 }}">
|
||||||
|
@ -61,7 +29,7 @@
|
||||||
<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>
|
||||||
<label for="isEpisode">Podcast Episode</label>
|
<label for="isEpisode">Podcast Episode</label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -344,3 +312,4 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
<script>window.setTimeout(() => Admin.toggleEpisodeFields(), 500)</script>
|
||||||
|
|
|
@ -6,8 +6,6 @@
|
||||||
</p>
|
</p>
|
||||||
<fieldset class="container mb-3">
|
<fieldset class="container mb-3">
|
||||||
<legend>Web Log Settings</legend>
|
<legend>Web Log Settings</legend>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<form action="{{ "admin/settings" | relative_link }}" method="post">
|
<form action="{{ "admin/settings" | 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 }}">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -54,9 +52,8 @@
|
||||||
<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 %}>
|
|
||||||
{{ pg[1] }}
|
{{ pg[1] }}
|
||||||
</option>
|
</option>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
@ -66,8 +63,8 @@
|
||||||
</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"
|
<input type="number" name="PostsPerPage" id="postsPerPage" class="form-control" min="0" max="50" required
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,8 +91,7 @@
|
||||||
<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] }}"
|
<option value="{{ it[0] }}"{% if model.uploads == it[0] %} selected{% endif %}>{{ it[1] }}</option>
|
||||||
{%- if model.uploads == it[0] %} selected{% endif %}>{{ it[1] }}</option>
|
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</select>
|
</select>
|
||||||
<label for="uploads">Default Upload Destination</label>
|
<label for="uploads">Default Upload Destination</label>
|
||||||
|
@ -109,13 +105,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset id="users" class="container mb-3 pb-0">
|
<fieldset id="users" class="container mb-3 pb-0">
|
||||||
<legend>Users</legend>
|
<legend>Users</legend>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
{% include_template "_user-list-columns" %}
|
{% include_template "_user-list-columns" %}
|
||||||
<a href="{{ "admin/settings/user/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
<a href="{{ "admin/settings/user/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||||
hx-target="#user_new">
|
hx-target="#user_new">
|
||||||
|
@ -130,13 +122,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ user_list }}
|
{{ user_list }}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset id="rss-settings" class="container mb-3 pb-0">
|
<fieldset id="rss-settings" class="container mb-3 pb-0">
|
||||||
<legend>RSS Settings</legend>
|
<legend>RSS Settings</legend>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<form action="{{ "admin/settings/rss" | relative_link }}" method="post">
|
<form action="{{ "admin/settings/rss" | 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 }}">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -202,13 +190,11 @@
|
||||||
</form>
|
</form>
|
||||||
<fieldset class="container mb-3 pb-0">
|
<fieldset class="container mb-3 pb-0">
|
||||||
<legend>Custom Feeds</legend>
|
<legend>Custom Feeds</legend>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<a class="btn btn-sm btn-secondary" href="{{ 'admin/settings/rss/new/edit' | relative_link }}">
|
<a class="btn btn-sm btn-secondary" href="{{ 'admin/settings/rss/new/edit' | relative_link }}">
|
||||||
Add a New Custom Feed
|
Add a New Custom Feed
|
||||||
</a>
|
</a>
|
||||||
{%- assign feed_count = custom_feeds | size -%}
|
{%- assign feed_count = custom_feeds | size -%}
|
||||||
{% if feed_count > 0 %}
|
{%- if feed_count > 0 %}
|
||||||
<form method="post" class="container g-0" hx-target="body">
|
<form method="post" class="container g-0" hx-target="body">
|
||||||
{%- assign source_col = "col-12 col-md-6" -%}
|
{%- assign source_col = "col-12 col-md-6" -%}
|
||||||
{%- assign path_col = "col-12 col-md-6" -%}
|
{%- assign path_col = "col-12 col-md-6" -%}
|
||||||
|
@ -242,27 +228,19 @@
|
||||||
<span class="d-none d-md-inline">{{ feed.path }}</span>
|
<span class="d-none d-md-inline">{{ feed.path }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{%- endfor %}
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{%- else %}
|
||||||
<p class="text-muted fst-italic text-center">No custom feeds defined</p>
|
<p class="text-muted fst-italic text-center">No custom feeds defined</p>
|
||||||
{% endif %}
|
{%- endif %}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset id="tag-mappings" class="container mb-3 pb-0">
|
<fieldset id="tag-mappings" class="container mb-3 pb-0">
|
||||||
<legend>Tag Mappings</legend>
|
<legend>Tag Mappings</legend>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<a href="{{ "admin/settings/tag-mapping/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
<a href="{{ "admin/settings/tag-mapping/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||||
hx-target="#tag_new">
|
hx-target="#tag_new">
|
||||||
Add a New Tag Mapping
|
Add a New Tag Mapping
|
||||||
</a>
|
</a>
|
||||||
{{ tag_mapping_list }}
|
{{ tag_mapping_list }}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<div class="row mwl-table-detail">
|
<div class="row mwl-table-detail">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
{%- capture badge_class -%}
|
{%- capture badge_class -%}
|
||||||
{%- if file.source == "disk" %}secondary{% else %}primary{% endif -%}
|
{%- if file.source == "Disk" %}secondary{% else %}primary{% endif -%}
|
||||||
{%- endcapture -%}
|
{%- endcapture -%}
|
||||||
{%- assign path_and_name = file.path | append: file.name -%}
|
{%- assign path_and_name = file.path | append: file.name -%}
|
||||||
{%- assign blog_rel = upload_path | append: path_and_name -%}
|
{%- assign blog_rel = upload_path | append: path_and_name -%}
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
{% if is_web_log_admin %}
|
{% if is_web_log_admin %}
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- capture delete_url -%}
|
{%- capture delete_url -%}
|
||||||
{%- if file.source == "disk" -%}
|
{%- if file.source == "Disk" -%}
|
||||||
admin/upload/delete/{{ path_and_name }}
|
admin/upload/delete/{{ path_and_name }}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
admin/upload/{{ file.id }}/delete
|
admin/upload/{{ file.id }}/delete
|
||||||
|
|
|
@ -13,11 +13,11 @@
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
<h1 class="index-title">{{ page_title }}</h1>
|
<h1 class="index-title">{{ page_title }}</h1>
|
||||||
{%- if subtitle %}<h4 class="text-muted">{{ subtitle }}</h4>{% endif -%}
|
{%- if subtitle %}<h4 class="text-muted">{{ subtitle }}</h4>{% endif -%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<section class="container mt-3" aria-label="The posts for the page">
|
{%- assign post_count = model.posts | size -%}
|
||||||
{% for post in model.posts %}
|
{%- if post_count > 0 %}
|
||||||
|
<section class="container mt-3" aria-label="The posts for the page">
|
||||||
|
{%- for post in model.posts %}
|
||||||
<article>
|
<article>
|
||||||
<h1>
|
<h1>
|
||||||
<a href="{{ post | relative_link }}" title="Permanent link to "{{ post.title | escape }}"">
|
<a href="{{ post | relative_link }}" title="Permanent link to "{{ post.title | escape }}"">
|
||||||
|
@ -40,8 +42,8 @@
|
||||||
<hr>
|
<hr>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
<nav aria-label="pagination">
|
<nav aria-label="pagination">
|
||||||
<ul class="pagination justify-content-evenly mt-2">
|
<ul class="pagination justify-content-evenly mt-2">
|
||||||
{% if model.newer_link -%}
|
{% if model.newer_link -%}
|
||||||
<li class="page-item"><a class="page-link" href="{{ model.newer_link.value }}">« Newer Posts</a></li>
|
<li class="page-item"><a class="page-link" href="{{ model.newer_link.value }}">« Newer Posts</a></li>
|
||||||
|
@ -50,4 +52,9 @@
|
||||||
<li class="page-item"><a class="page-link" href="{{ model.older_link.value }}">Older Posts »</a></li>
|
<li class="page-item"><a class="page-link" href="{{ model.older_link.value }}">Older Posts »</a></li>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
{%- else %}
|
||||||
|
<article>
|
||||||
|
<p class="text-center mt-3">No posts found</p>
|
||||||
|
</article>
|
||||||
|
{%- endif %}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
||||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||||
<title>{{ page_title | strip_html }}{% if page_title %} « {% endif %}{{ web_log.name | strip_html }}</title>
|
<title>{{ page_title | strip_html }}{% if page_title %} « {% endif %}{{ web_log.name | strip_html }}</title>
|
||||||
{% page_head -%}
|
{% page_head -%}
|
||||||
</head>
|
</head>
|
||||||
|
@ -55,8 +55,8 @@
|
||||||
<img src="{{ "themes/admin/logo-dark.png" | relative_link }}" alt="myWebLog" width="120" height="34">
|
<img src="{{ "themes/admin/logo-dark.png" | relative_link }}" alt="myWebLog" width="120" height="34">
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user