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:
Daniel J. Summers 2022-07-28 20:36:02 -04:00
parent 6b49793fbb
commit 33698bd182
22 changed files with 926 additions and 898 deletions

View File

@ -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[]>

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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)
@ -33,7 +34,7 @@ let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
} }
// 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
@ -70,17 +74,20 @@ let adminDashboard : HttpHandler = requireAccess Administrator >=> fun 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 ~~
/// ~~ CACHES ~~
module Cache =
// POST /admin/cache/web-log/{id}/refresh // POST /admin/cache/web-log/{id}/refresh
let refreshWebLogCache webLogId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task { 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 ->
@ -95,7 +102,7 @@ let refreshWebLogCache webLogId : HttpHandler = requireAccess Administrator >=>
} }
// 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 ()
@ -118,10 +125,14 @@ let refreshThemeCache themeId : HttpHandler = requireAccess Administrator >=> fu
return! toAdminDashboard next ctx return! toAdminDashboard next ctx
} }
// ~~ CATEGORIES ~~
/// ~~ CATEGORIES ~~
module Category =
open MyWebLog.Data
// GET /admin/categories // GET /admin/categories
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { 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 =
@ -135,14 +146,14 @@ let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun 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" })
@ -162,7 +173,7 @@ let editCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ct
} }
// 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,34 +191,46 @@ 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
} }
/// ~~ TAG MAPPINGS ~~
module TagMapping =
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
// ~~ TAG MAPPINGS ~~
/// Add tag mappings to the given hash /// Add tag mappings to the given hash
let private withTagMappings (ctx : HttpContext) hash = task { 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
@ -216,7 +239,7 @@ let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
} }
// 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" }
@ -232,7 +255,7 @@ let editMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next
} }
// 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,19 +265,21 @@ 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 ~~
/// ~~ THEMES ~~
module Theme =
open System open System
open System.IO open System.IO
@ -263,7 +288,7 @@ open System.Text.RegularExpressions
open MyWebLog.Data open MyWebLog.Data
// GET /admin/theme/list // GET /admin/theme/list
let listThemes : HttpHandler = requireAccess Administrator >=> fun next ctx -> task { 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"
@ -273,7 +298,7 @@ let listThemes : HttpHandler = requireAccess Administrator >=> fun next ctx -> t
} }
// 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
@ -324,8 +349,8 @@ let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundT
} }
} }
/// 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
@ -334,7 +359,7 @@ let getThemeIdFromFileName (fileName : string) =
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
@ -351,10 +376,10 @@ let loadThemeFromZip themeId file (data : IData) = backgroundTask {
} }
// 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
@ -391,31 +416,34 @@ let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> ta
} }
// 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 ~~
/// ~~ WEB LOG SETTINGS ~~
module WebLog =
open System.Collections.Generic open System.Collections.Generic
open System.IO
// GET /admin/settings // GET /admin/settings
let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
@ -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,7 +484,7 @@ 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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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"

View 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"> &bull; </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> &nbsp; &nbsp;
<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>

View File

@ -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>

View File

@ -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 %}>
&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"
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

@ -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>

View File

@ -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"> &bull; </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> &nbsp; &nbsp;
<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">

View File

@ -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"> &bull; </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> &nbsp; &nbsp;
<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 }}">
@ -344,3 +312,4 @@
</div> </div>
</form> </form>
</article> </article>
<script>window.setTimeout(() => Admin.toggleEpisodeFields(), 500)</script>

View File

@ -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>

View File

@ -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"> &bull; </span> <span class="text-muted"> &bull; </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

View File

@ -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>

View File

@ -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 %}
{%- assign post_count = model.posts | size -%}
{%- if post_count > 0 %}
<section class="container mt-3" aria-label="The posts for the page"> <section class="container mt-3" aria-label="The posts for the page">
{% for post in model.posts %} {%- for post in model.posts %}
<article> <article>
<h1> <h1>
<a href="{{ post | relative_link }}" title="Permanent link to &quot;{{ post.title | escape }}&quot;"> <a href="{{ post | relative_link }}" title="Permanent link to &quot;{{ post.title | escape }}&quot;">
@ -51,3 +53,8 @@
{%- endif -%} {%- endif -%}
</ul> </ul>
</nav> </nav>
{%- else %}
<article>
<p class="text-center mt-3">No posts found</p>
</article>
{%- endif %}

View File

@ -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 %} &laquo; {% endif %}{{ web_log.name | strip_html }}</title> <title>{{ page_title | strip_html }}{% if page_title %} &laquo; {% 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>