Support directory installations
- Add web log to HttpContext on first retrieval per req - Add several DotLiquid filters - Use int for page numbers - Update all themes to use rel/abs link and other filters
This commit is contained in:
		
							parent
							
								
									63425ad806
								
							
						
					
					
						commit
						cbf87f5b49
					
				| @ -540,35 +540,32 @@ module Post = | |||||||
|         } |         } | ||||||
|      |      | ||||||
|     /// Find posts to be displayed on an admin page |     /// Find posts to be displayed on an admin page | ||||||
|     let findPageOfPosts (webLogId : WebLogId) (pageNbr : int64) postsPerPage = |     let findPageOfPosts (webLogId : WebLogId) (pageNbr : int) postsPerPage = | ||||||
|         let pg = int pageNbr |  | ||||||
|         rethink<Post list> { |         rethink<Post list> { | ||||||
|             withTable Table.Post |             withTable Table.Post | ||||||
|             getAll [ webLogId ] (nameof webLogId) |             getAll [ webLogId ] (nameof webLogId) | ||||||
|             without [ "priorPermalinks"; "revisions" ] |             without [ "priorPermalinks"; "revisions" ] | ||||||
|             orderByFuncDescending (fun row -> row["publishedOn"].Default_ "updatedOn" :> obj) |             orderByFuncDescending (fun row -> row["publishedOn"].Default_ "updatedOn" :> obj) | ||||||
|             skip ((pg - 1) * postsPerPage) |             skip ((pageNbr - 1) * postsPerPage) | ||||||
|             limit (postsPerPage + 1) |             limit (postsPerPage + 1) | ||||||
|             result; withRetryDefault |             result; withRetryDefault | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     /// Find posts to be displayed on a page |     /// Find posts to be displayed on a page | ||||||
|     let findPageOfPublishedPosts (webLogId : WebLogId) (pageNbr : int64) postsPerPage = |     let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage = | ||||||
|         let pg = int pageNbr |  | ||||||
|         rethink<Post list> { |         rethink<Post list> { | ||||||
|             withTable Table.Post |             withTable Table.Post | ||||||
|             getAll [ webLogId ] (nameof webLogId) |             getAll [ webLogId ] (nameof webLogId) | ||||||
|             filter "status" Published |             filter "status" Published | ||||||
|             without [ "priorPermalinks"; "revisions" ] |             without [ "priorPermalinks"; "revisions" ] | ||||||
|             orderByDescending "publishedOn" |             orderByDescending "publishedOn" | ||||||
|             skip ((pg - 1) * postsPerPage) |             skip ((pageNbr - 1) * postsPerPage) | ||||||
|             limit (postsPerPage + 1) |             limit (postsPerPage + 1) | ||||||
|             result; withRetryDefault |             result; withRetryDefault | ||||||
|         } |         } | ||||||
|      |      | ||||||
|     /// Find posts to be displayed on a tag list page |     /// Find posts to be displayed on a tag list page | ||||||
|     let findPageOfTaggedPosts (webLogId : WebLogId) (tag : string) (pageNbr : int64) postsPerPage = |     let findPageOfTaggedPosts (webLogId : WebLogId) (tag : string) pageNbr postsPerPage = | ||||||
|         let pg = int pageNbr |  | ||||||
|         rethink<Post list> { |         rethink<Post list> { | ||||||
|             withTable Table.Post |             withTable Table.Post | ||||||
|             getAll [ tag ] "tags" |             getAll [ tag ] "tags" | ||||||
| @ -576,7 +573,7 @@ module Post = | |||||||
|             filter "status" Published |             filter "status" Published | ||||||
|             without [ "priorPermalinks"; "revisions" ] |             without [ "priorPermalinks"; "revisions" ] | ||||||
|             orderByDescending "publishedOn" |             orderByDescending "publishedOn" | ||||||
|             skip ((pg - 1) * postsPerPage) |             skip ((pageNbr - 1) * postsPerPage) | ||||||
|             limit (postsPerPage + 1) |             limit (postsPerPage + 1) | ||||||
|             result; withRetryDefault |             result; withRetryDefault | ||||||
|         } |         } | ||||||
| @ -711,6 +708,12 @@ module WebLog = | |||||||
|         write; withRetryOnce; ignoreResult |         write; withRetryOnce; ignoreResult | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     /// Get all web logs | ||||||
|  |     let all = rethink<WebLog list> { | ||||||
|  |         withTable Table.WebLog | ||||||
|  |         result; withRetryDefault | ||||||
|  |     } | ||||||
|  |      | ||||||
|     /// Retrieve a web log by the URL base |     /// Retrieve a web log by the URL base | ||||||
|     let findByHost (url : string) = |     let findByHost (url : string) = | ||||||
|         rethink<WebLog list> { |         rethink<WebLog list> { | ||||||
|  | |||||||
| @ -289,8 +289,20 @@ module WebLog = | |||||||
|           timeZone     = "" |           timeZone     = "" | ||||||
|         } |         } | ||||||
|      |      | ||||||
|     /// Convert a permalink to an absolute URL |     /// Get the host (including scheme) and extra path from the URL base | ||||||
|     let absoluteUrl webLog = function Permalink link -> $"{webLog.urlBase}{link}" |     let hostAndPath webLog = | ||||||
|  |         let scheme = webLog.urlBase.Split "://" | ||||||
|  |         let host   = scheme[1].Split "/" | ||||||
|  |         $"{scheme[0]}://{host[0]}", if host.Length > 1 then $"""/{String.Join ("/", host |> Array.skip 1)}""" else "" | ||||||
|  |      | ||||||
|  |     /// Generate an absolute URL for the given link | ||||||
|  |     let absoluteUrl webLog permalink = | ||||||
|  |         $"{webLog.urlBase}/{Permalink.toString permalink}" | ||||||
|  | 
 | ||||||
|  |     /// Generate a relative URL for the given link | ||||||
|  |     let relativeUrl webLog permalink = | ||||||
|  |         let _, leadPath = hostAndPath webLog | ||||||
|  |         $"{leadPath}/{Permalink.toString permalink}" | ||||||
|      |      | ||||||
|     /// Convert a date/time to the web log's local date/time |     /// Convert a date/time to the web log's local date/time | ||||||
|     let localTime webLog (date : DateTime) = |     let localTime webLog (date : DateTime) = | ||||||
|  | |||||||
| @ -400,7 +400,8 @@ type PostListItem = | |||||||
| 
 | 
 | ||||||
|     /// Create a post list item from a post |     /// Create a post list item from a post | ||||||
|     static member fromPost (webLog : WebLog) (post : Post) = |     static member fromPost (webLog : WebLog) (post : Post) = | ||||||
|         let inTZ = WebLog.localTime webLog |         let _, extra = WebLog.hostAndPath webLog | ||||||
|  |         let inTZ     = WebLog.localTime   webLog | ||||||
|         { id          = PostId.toString post.id |         { id          = PostId.toString post.id | ||||||
|           authorId    = WebLogUserId.toString post.authorId |           authorId    = WebLogUserId.toString post.authorId | ||||||
|           status      = PostStatus.toString   post.status |           status      = PostStatus.toString   post.status | ||||||
| @ -408,7 +409,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        = post.text |           text        = if extra = "" then post.text else post.text.Replace ("href=\"/", $"href=\"{extra}/") | ||||||
|           categoryIds = post.categoryIds |> List.map CategoryId.toString |           categoryIds = post.categoryIds |> List.map CategoryId.toString | ||||||
|           tags        = post.tags |           tags        = post.tags | ||||||
|           meta        = post.metadata |           meta        = post.metadata | ||||||
|  | |||||||
| @ -6,10 +6,12 @@ open Microsoft.AspNetCore.Http | |||||||
| module Cache = | module Cache = | ||||||
|      |      | ||||||
|     /// Create the cache key for the web log for the current request |     /// Create the cache key for the web log for the current request | ||||||
|     let makeKey (ctx : HttpContext) = ctx.Request.Host.ToUriComponent () |     let makeKey (ctx : HttpContext) = (ctx.Items["webLog"] :?> WebLog).urlBase | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| open System.Collections.Concurrent | open System.Collections.Concurrent | ||||||
|  | open Microsoft.Extensions.DependencyInjection | ||||||
|  | open RethinkDb.Driver.Net | ||||||
| 
 | 
 | ||||||
| /// <summary> | /// <summary> | ||||||
| /// In-memory cache of web log details | /// In-memory cache of web log details | ||||||
| @ -17,23 +19,35 @@ open System.Collections.Concurrent | |||||||
| /// <remarks>This is filled by the middleware via the first request for each host, and can be updated via the web log | /// <remarks>This is filled by the middleware via the first request for each host, and can be updated via the web log | ||||||
| /// settings update page</remarks> | /// settings update page</remarks> | ||||||
| module WebLogCache = | module WebLogCache = | ||||||
|          |      | ||||||
|  |     /// Create the full path of the request | ||||||
|  |     let private fullPath (ctx : HttpContext) = | ||||||
|  |         $"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{ctx.Request.Path.Value}" | ||||||
|  |      | ||||||
|     /// The cache of web log details |     /// The cache of web log details | ||||||
|     let private _cache = ConcurrentDictionary<string, WebLog> () |     let mutable private _cache : WebLog list = [] | ||||||
| 
 | 
 | ||||||
|     /// Does a host exist in the cache? |     /// Does a host exist in the cache? | ||||||
|     let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) |     let exists ctx = | ||||||
|  |         let path = fullPath ctx | ||||||
|  |         _cache |> List.exists (fun wl -> path.StartsWith wl.urlBase) | ||||||
| 
 | 
 | ||||||
|     /// Get the web log for the current request |     /// Get the web log for the current request | ||||||
|     let get ctx = _cache[Cache.makeKey ctx] |     let get ctx = | ||||||
|  |         let path = fullPath ctx | ||||||
|  |         _cache |> List.find (fun wl -> path.StartsWith wl.urlBase) | ||||||
| 
 | 
 | ||||||
|     /// Cache the web log for a particular host |     /// Cache the web log for a particular host | ||||||
|     let set ctx webLog = _cache[Cache.makeKey ctx] <- webLog |     let set webLog = | ||||||
|  |         _cache <- webLog :: (_cache |> List.filter (fun wl -> wl.id <> webLog.id)) | ||||||
|  |      | ||||||
|  |     /// Fill the web log cache from the database | ||||||
|  |     let fill conn = backgroundTask { | ||||||
|  |         let! webLogs = Data.WebLog.all conn | ||||||
|  |         _cache <- webLogs | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| open Microsoft.Extensions.DependencyInjection |  | ||||||
| open RethinkDb.Driver.Net |  | ||||||
| 
 |  | ||||||
| /// A cache of page information needed to display the page list in templates | /// A cache of page information needed to display the page list in templates | ||||||
| module PageListCache = | module PageListCache = | ||||||
|      |      | ||||||
| @ -42,12 +56,15 @@ module PageListCache = | |||||||
|     /// Cache of displayed pages |     /// Cache of displayed pages | ||||||
|     let private _cache = ConcurrentDictionary<string, DisplayPage[]> () |     let private _cache = ConcurrentDictionary<string, DisplayPage[]> () | ||||||
|      |      | ||||||
|  |     /// Are there pages cached for this web log? | ||||||
|  |     let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) | ||||||
|  |      | ||||||
|     /// Get the pages for the web log for this request |     /// Get the pages for the web log for this request | ||||||
|     let get ctx = _cache[Cache.makeKey ctx] |     let get ctx = _cache[Cache.makeKey ctx] | ||||||
|      |      | ||||||
|     /// Update the pages for the current web log |     /// Update the pages for the current web log | ||||||
|     let update ctx = task { |     let update (ctx : HttpContext) = backgroundTask { | ||||||
|         let  webLog = WebLogCache.get ctx |         let  webLog = ctx.Items["webLog"] :?> WebLog | ||||||
|         let  conn   = ctx.RequestServices.GetRequiredService<IConnection> () |         let  conn   = ctx.RequestServices.GetRequiredService<IConnection> () | ||||||
|         let! pages  = Data.Page.findListed webLog.id conn |         let! pages  = Data.Page.findListed webLog.id conn | ||||||
|         _cache[Cache.makeKey ctx] <- pages |> List.map (DisplayPage.fromPage webLog) |> Array.ofList |         _cache[Cache.makeKey ctx] <- pages |> List.map (DisplayPage.fromPage webLog) |> Array.ofList | ||||||
| @ -62,12 +79,15 @@ module CategoryCache = | |||||||
|     /// The cache itself |     /// The cache itself | ||||||
|     let private _cache = ConcurrentDictionary<string, DisplayCategory[]> () |     let private _cache = ConcurrentDictionary<string, DisplayCategory[]> () | ||||||
|      |      | ||||||
|  |     /// Are there categories cached for this web log? | ||||||
|  |     let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) | ||||||
|  |      | ||||||
|     /// Get the categories for the web log for this request |     /// Get the categories for the web log for this request | ||||||
|     let get ctx = _cache[Cache.makeKey ctx] |     let get ctx = _cache[Cache.makeKey ctx] | ||||||
|      |      | ||||||
|     /// Update the cache with fresh data |     /// Update the cache with fresh data | ||||||
|     let update ctx = backgroundTask { |     let update (ctx : HttpContext) = backgroundTask { | ||||||
|         let  webLog = WebLogCache.get ctx |         let  webLog = ctx.Items["webLog"] :?> WebLog | ||||||
|         let  conn   = ctx.RequestServices.GetRequiredService<IConnection> () |         let  conn   = ctx.RequestServices.GetRequiredService<IConnection> () | ||||||
|         let! cats   = Data.Category.findAllForView webLog.id conn |         let! cats   = Data.Category.findAllForView webLog.id conn | ||||||
|         _cache[Cache.makeKey ctx] <- cats |         _cache[Cache.makeKey ctx] <- cats | ||||||
| @ -84,7 +104,7 @@ module TemplateCache = | |||||||
|     let private _cache = ConcurrentDictionary<string, Template> () |     let private _cache = ConcurrentDictionary<string, Template> () | ||||||
|      |      | ||||||
|     /// Get a template for the given theme and template nate |     /// Get a template for the given theme and template nate | ||||||
|     let get (theme : string) (templateName : string) = task { |     let get (theme : string) (templateName : string) = backgroundTask { | ||||||
|         let templatePath = $"themes/{theme}/{templateName}" |         let templatePath = $"themes/{theme}/{templateName}" | ||||||
|         match _cache.ContainsKey templatePath with |         match _cache.ContainsKey templatePath with | ||||||
|         | true -> () |         | true -> () | ||||||
|  | |||||||
							
								
								
									
										123
									
								
								src/MyWebLog/DotLiquidBespoke.fs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/MyWebLog/DotLiquidBespoke.fs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | |||||||
|  | /// Custom DotLiquid filters and tags | ||||||
|  | module MyWebLog.DotLiquidBespoke | ||||||
|  | 
 | ||||||
|  | open System | ||||||
|  | open System.IO | ||||||
|  | open DotLiquid | ||||||
|  | open MyWebLog.ViewModels | ||||||
|  | 
 | ||||||
|  | /// Get the current web log from the DotLiquid context | ||||||
|  | let webLog (ctx : Context) = | ||||||
|  |     ctx.Environments[0].["web_log"] :?> WebLog | ||||||
|  | 
 | ||||||
|  | /// Obtain the link from known types | ||||||
|  | let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) = | ||||||
|  |     match item with | ||||||
|  |     | :? String       as link  -> Some link | ||||||
|  |     | :? DisplayPage  as page  -> Some page.permalink | ||||||
|  |     | :? PostListItem as post  -> Some post.permalink | ||||||
|  |     | :? DropProxy    as proxy -> Option.ofObj proxy["permalink"] |> Option.map string | ||||||
|  |     | _ -> None | ||||||
|  |     |> function | ||||||
|  |     | Some link -> linkFunc (webLog ctx) (Permalink link) | ||||||
|  |     | None      -> $"alert('unknown item type {item.GetType().Name}')" | ||||||
|  | 
 | ||||||
|  | /// A filter to generate an absolute link | ||||||
|  | type AbsoluteLinkFilter () = | ||||||
|  |     static member AbsoluteLink (ctx : Context, item : obj) = | ||||||
|  |         permalink ctx item WebLog.absoluteUrl | ||||||
|  | 
 | ||||||
|  | /// A filter to generate a link with posts categorized under the given category | ||||||
|  | type CategoryLinkFilter () = | ||||||
|  |     static member CategoryLink (ctx : Context, catObj : obj) = | ||||||
|  |         match catObj with | ||||||
|  |         | :? DisplayCategory as cat   -> Some cat.slug | ||||||
|  |         | :? DropProxy       as proxy -> Option.ofObj proxy["slug"] |> Option.map string | ||||||
|  |         | _ -> None | ||||||
|  |         |> function | ||||||
|  |         | Some slug -> WebLog.relativeUrl (webLog ctx) (Permalink $"category/{slug}/") | ||||||
|  |         | None      -> $"alert('unknown category object type {catObj.GetType().Name}')" | ||||||
|  |          | ||||||
|  | 
 | ||||||
|  | /// A filter to generate a link that will edit a page | ||||||
|  | type EditPageLinkFilter () = | ||||||
|  |     static member EditPageLink (ctx : Context, pageObj : obj) = | ||||||
|  |         match pageObj with | ||||||
|  |         | :? DisplayPage as page  -> Some page.id | ||||||
|  |         | :? DropProxy   as proxy -> Option.ofObj proxy["id"] |> Option.map string | ||||||
|  |         | :? String      as theId -> Some theId | ||||||
|  |         | _ -> None | ||||||
|  |         |> function | ||||||
|  |         | Some pageId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/page/{pageId}/edit") | ||||||
|  |         | None        -> $"alert('unknown page object type {pageObj.GetType().Name}')" | ||||||
|  |      | ||||||
|  | /// A filter to generate a link that will edit a post | ||||||
|  | type EditPostLinkFilter () = | ||||||
|  |     static member EditPostLink (ctx : Context, postObj : obj) = | ||||||
|  |         match postObj with | ||||||
|  |         | :? PostListItem as post  -> Some post.id | ||||||
|  |         | :? DropProxy    as proxy -> Option.ofObj proxy["id"] |> Option.map string | ||||||
|  |         | :? String       as theId -> Some theId | ||||||
|  |         | _ -> None | ||||||
|  |         |> function | ||||||
|  |         | Some postId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/post/{postId}/edit") | ||||||
|  |         | None        -> $"alert('unknown post object type {postObj.GetType().Name}')" | ||||||
|  |      | ||||||
|  | /// A filter to generate nav links, highlighting the active link (exact match) | ||||||
|  | type NavLinkFilter () = | ||||||
|  |     static member NavLink (ctx : Context, url : string, text : string) = | ||||||
|  |         let webLog = webLog ctx | ||||||
|  |         seq { | ||||||
|  |             "<li class=\"nav-item\"><a class=\"nav-link" | ||||||
|  |             if url = string ctx.Environments[0].["current_page"] then " active" | ||||||
|  |             "\" href=\"" | ||||||
|  |             WebLog.relativeUrl webLog (Permalink url) | ||||||
|  |             "\">" | ||||||
|  |             text | ||||||
|  |             "</a></li>" | ||||||
|  |         } | ||||||
|  |         |> Seq.fold (+) "" | ||||||
|  | 
 | ||||||
|  | /// A filter to generate a relative link | ||||||
|  | type RelativeLinkFilter () = | ||||||
|  |     static member RelativeLink (ctx : Context, item : obj) = | ||||||
|  |         permalink ctx item WebLog.relativeUrl | ||||||
|  | 
 | ||||||
|  | /// A filter to generate a link with posts tagged with the given tag | ||||||
|  | type TagLinkFilter () = | ||||||
|  |     static member TagLink (ctx : Context, tag : string) = | ||||||
|  |         ctx.Environments[0].["tag_mappings"] :?> TagMap list | ||||||
|  |         |> List.tryFind (fun it -> it.tag = tag) | ||||||
|  |         |> function | ||||||
|  |         | Some tagMap -> tagMap.urlValue | ||||||
|  |         | None        -> tag.Replace (" ", "+") | ||||||
|  |         |> function tagUrl -> WebLog.relativeUrl (webLog ctx) (Permalink $"tag/{tagUrl}/") | ||||||
|  |              | ||||||
|  | /// Create links for a user to log on or off, and a dashboard link if they are logged off | ||||||
|  | type UserLinksTag () = | ||||||
|  |     inherit Tag () | ||||||
|  |      | ||||||
|  |     override this.Render (context : Context, result : TextWriter) = | ||||||
|  |         let webLog = webLog context | ||||||
|  |         let link it = WebLog.relativeUrl webLog (Permalink it) | ||||||
|  |         seq { | ||||||
|  |             """<ul class="navbar-nav flex-grow-1 justify-content-end">""" | ||||||
|  |             match Convert.ToBoolean context.Environments[0].["logged_on"] with | ||||||
|  |             | true -> | ||||||
|  |                 $"""<li class="nav-item"><a class="nav-link" href="{link "admin"}">Dashboard</a></li>""" | ||||||
|  |                 $"""<li class="nav-item"><a class="nav-link" href="{link "user/log-off"}">Log Off</a></li>""" | ||||||
|  |             | false -> | ||||||
|  |                 $"""<li class="nav-item"><a class="nav-link" href="{link "user/log-on"}">Log On</a></li>""" | ||||||
|  |             "</ul>" | ||||||
|  |         } | ||||||
|  |         |> Seq.iter result.WriteLine | ||||||
|  | 
 | ||||||
|  | /// A filter to retrieve the value of a meta item from a list | ||||||
|  | //    (shorter than `{% assign item = list | where: "name", [name] | first %}{{ item.value }}`) | ||||||
|  | type ValueFilter () = | ||||||
|  |     static member Value (_ : Context, items : MetaItem list, name : string) = | ||||||
|  |         match items |> List.tryFind (fun it -> it.name = name) with | ||||||
|  |         | Some item -> item.value | ||||||
|  |         | None -> $"-- {name} not found --" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @ -1,6 +1,8 @@ | |||||||
| /// Handlers to manipulate admin functions | /// Handlers to manipulate admin functions | ||||||
| module MyWebLog.Handlers.Admin | module MyWebLog.Handlers.Admin | ||||||
| 
 | 
 | ||||||
|  | // TODO: remove requireUser, as this is applied in the router | ||||||
|  | 
 | ||||||
| open System.Collections.Generic | open System.Collections.Generic | ||||||
| open System.IO | open System.IO | ||||||
| 
 | 
 | ||||||
| @ -21,9 +23,9 @@ open RethinkDb.Driver.Net | |||||||
| 
 | 
 | ||||||
| // GET /admin | // GET /admin | ||||||
| let dashboard : HttpHandler = requireUser >=> fun next ctx -> task { | let dashboard : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     let webLogId = webLogId ctx |     let webLog = webLog ctx | ||||||
|     let conn     = conn ctx |     let conn   = conn   ctx | ||||||
|     let getCount (f : WebLogId -> IConnection -> Task<int>) = f webLogId conn |     let getCount (f : WebLogId -> IConnection -> Task<int>) = f webLog.id conn | ||||||
|     let! posts   = Data.Post.countByStatus Published |> getCount |     let! posts   = Data.Post.countByStatus Published |> getCount | ||||||
|     let! drafts  = Data.Post.countByStatus Draft     |> getCount |     let! drafts  = Data.Post.countByStatus Draft     |> getCount | ||||||
|     let! pages   = Data.Page.countAll                |> getCount |     let! pages   = Data.Page.countAll                |> getCount | ||||||
| @ -60,13 +62,13 @@ let listCategories : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| 
 | 
 | ||||||
| // GET /admin/category/{id}/edit | // GET /admin/category/{id}/edit | ||||||
| let editCategory catId : HttpHandler = requireUser >=> fun next ctx -> task { | let editCategory catId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     let  webLogId = webLogId ctx |     let  webLog = webLog ctx | ||||||
|     let  conn     = conn     ctx |     let  conn   = conn   ctx | ||||||
|     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" }) | ||||||
|         | _ -> |         | _ -> | ||||||
|             match! Data.Category.findById (CategoryId catId) webLogId conn with |             match! Data.Category.findById (CategoryId catId) webLog.id conn with | ||||||
|             | Some cat -> return Some ("Edit Category", cat) |             | Some cat -> return Some ("Edit Category", cat) | ||||||
|             | None -> return None |             | None -> return None | ||||||
|     } |     } | ||||||
| @ -86,12 +88,12 @@ let editCategory catId : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| // POST /admin/category/save | // POST /admin/category/save | ||||||
| let saveCategory : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let saveCategory : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     let! model    = ctx.BindFormAsync<EditCategoryModel> () |     let! model    = ctx.BindFormAsync<EditCategoryModel> () | ||||||
|     let  webLogId = webLogId ctx |     let  webLog   = webLog ctx | ||||||
|     let  conn     = conn     ctx |     let  conn     = conn   ctx | ||||||
|     let! category = task { |     let! category = task { | ||||||
|         match model.categoryId with |         match model.categoryId with | ||||||
|         | "new" -> return Some { Category.empty with id = CategoryId.create (); webLogId = webLogId } |         | "new" -> return Some { Category.empty with id = CategoryId.create (); webLogId = webLog.id } | ||||||
|         | catId -> return! Data.Category.findById (CategoryId catId) webLogId conn |         | catId -> return! Data.Category.findById (CategoryId catId) webLog.id conn | ||||||
|     } |     } | ||||||
|     match category with |     match category with | ||||||
|     | Some cat -> |     | Some cat -> | ||||||
| @ -105,20 +107,22 @@ let saveCategory : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - | |||||||
|         do! (match model.categoryId with "new" -> Data.Category.add | _ -> Data.Category.update) cat conn |         do! (match model.categoryId with "new" -> Data.Category.add | _ -> Data.Category.update) cat conn | ||||||
|         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! redirectToGet $"/admin/category/{CategoryId.toString cat.id}/edit" next ctx |         return! | ||||||
|  |             redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/category/{CategoryId.toString cat.id}/edit")) | ||||||
|  |                 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 = requireUser >=> validateCsrf >=> fun next ctx -> task { | let deleteCategory catId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     let webLogId = webLogId ctx |     let webLog = webLog ctx | ||||||
|     let conn     = conn     ctx |     let conn   = conn   ctx | ||||||
|     match! Data.Category.delete (CategoryId catId) webLogId conn with |     match! Data.Category.delete (CategoryId catId) webLog.id conn with | ||||||
|     | true -> |     | true -> | ||||||
|         do! CategoryCache.update ctx |         do! CategoryCache.update ctx | ||||||
|         do! addMessage ctx { UserMessage.success with message = "Category deleted successfully" } |         do! addMessage ctx { UserMessage.success with message = "Category deleted successfully" } | ||||||
|     | false -> do! addMessage ctx { UserMessage.error with message = "Category not found; cannot delete" } |     | false -> do! addMessage ctx { UserMessage.error with message = "Category not found; cannot delete" } | ||||||
|     return! redirectToGet "/admin/categories" next ctx |     return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/categories")) next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // -- PAGES -- | // -- PAGES -- | ||||||
| @ -126,7 +130,7 @@ let deleteCategory catId : HttpHandler = requireUser >=> validateCsrf >=> fun ne | |||||||
| // GET /admin/pages | // GET /admin/pages | ||||||
| // GET /admin/pages/page/{pageNbr} | // GET /admin/pages/page/{pageNbr} | ||||||
| let listPages pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { | let listPages pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     let  webLog = WebLogCache.get ctx |     let  webLog = webLog ctx | ||||||
|     let! pages  = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx) |     let! pages  = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx) | ||||||
|     return! |     return! | ||||||
|         Hash.FromAnonymousObject |         Hash.FromAnonymousObject | ||||||
| @ -142,7 +146,7 @@ let editPage pgId : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
|         match pgId with |         match pgId with | ||||||
|         | "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new" }) |         | "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new" }) | ||||||
|         | _ -> |         | _ -> | ||||||
|             match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with |             match! Data.Page.findByFullId (PageId pgId) (webLog ctx).id (conn ctx) with | ||||||
|             | Some page -> return Some ("Edit Page", page) |             | Some page -> return Some ("Edit Page", page) | ||||||
|             | None -> return None |             | None -> return None | ||||||
|     } |     } | ||||||
| @ -164,7 +168,7 @@ let editPage pgId : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| 
 | 
 | ||||||
| // GET /admin/page/{id}/permalinks | // GET /admin/page/{id}/permalinks | ||||||
| let editPagePermalinks pgId : HttpHandler = requireUser >=> fun next ctx -> task { | let editPagePermalinks pgId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with |     match! Data.Page.findByFullId (PageId pgId) (webLog ctx).id (conn ctx) with | ||||||
|     | Some pg -> |     | Some pg -> | ||||||
|         return! |         return! | ||||||
|             Hash.FromAnonymousObject {| |             Hash.FromAnonymousObject {| | ||||||
| @ -178,44 +182,46 @@ let editPagePermalinks pgId : HttpHandler = requireUser >=> fun next ctx -> task | |||||||
| 
 | 
 | ||||||
| // POST /admin/page/permalinks | // POST /admin/page/permalinks | ||||||
| let savePagePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let savePagePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     let! model = ctx.BindFormAsync<ManagePermalinksModel> () |     let  webLog = webLog ctx | ||||||
|     let  links = model.prior |> Array.map Permalink |> List.ofArray |     let! model  = ctx.BindFormAsync<ManagePermalinksModel> () | ||||||
|     match! Data.Page.updatePriorPermalinks (PageId model.id) (webLogId ctx) links (conn ctx) with |     let  links  = model.prior |> Array.map Permalink |> List.ofArray | ||||||
|  |     match! Data.Page.updatePriorPermalinks (PageId model.id) webLog.id links (conn ctx) with | ||||||
|     | true -> |     | true -> | ||||||
|         do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" } |         do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" } | ||||||
|         return! redirectToGet $"/admin/page/{model.id}/permalinks" next ctx |         return! redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/page/{model.id}/permalinks")) next ctx | ||||||
|     | false -> return! Error.notFound next ctx |     | false -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // POST /admin/page/{id}/delete | // POST /admin/page/{id}/delete | ||||||
| let deletePage pgId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let deletePage pgId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     match! Data.Page.delete (PageId pgId) (webLogId ctx) (conn ctx) with |     let webLog = webLog ctx | ||||||
|  |     match! Data.Page.delete (PageId pgId) webLog.id (conn ctx) with | ||||||
|     | true  -> do! addMessage ctx { UserMessage.success with message = "Page deleted successfully" } |     | true  -> do! addMessage ctx { UserMessage.success with message = "Page deleted successfully" } | ||||||
|     | false -> do! addMessage ctx { UserMessage.error with message = "Page not found; nothing deleted" } |     | false -> do! addMessage ctx { UserMessage.error with message = "Page not found; nothing deleted" } | ||||||
|     return! redirectToGet "/admin/pages" next ctx |     return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/pages")) next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| open System | open System | ||||||
| 
 | 
 | ||||||
| #nowarn "3511" | #nowarn "3511" | ||||||
| 
 | 
 | ||||||
| // POST /page/save | // POST /admin/page/save | ||||||
| let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     let! model    = ctx.BindFormAsync<EditPageModel> () |     let! model  = ctx.BindFormAsync<EditPageModel> () | ||||||
|     let  webLogId = webLogId ctx |     let  webLog = webLog ctx | ||||||
|     let  conn     = conn ctx |     let  conn   = conn ctx | ||||||
|     let  now      = DateTime.UtcNow |     let  now    = DateTime.UtcNow | ||||||
|     let! pg       = task { |     let! pg     = task { | ||||||
|         match model.pageId with |         match model.pageId with | ||||||
|         | "new" -> |         | "new" -> | ||||||
|             return Some |             return Some | ||||||
|                 { Page.empty with |                 { Page.empty with | ||||||
|                     id          = PageId.create () |                     id          = PageId.create () | ||||||
|                     webLogId    = webLogId |                     webLogId    = webLog.id | ||||||
|                     authorId    = userId ctx |                     authorId    = userId ctx | ||||||
|                     publishedOn = now |                     publishedOn = now | ||||||
|                 } |                 } | ||||||
|         | pgId -> return! Data.Page.findByFullId (PageId pgId) webLogId conn |         | pgId -> return! Data.Page.findByFullId (PageId pgId) webLog.id conn | ||||||
|     } |     } | ||||||
|     match pg with |     match pg with | ||||||
|     | Some page -> |     | Some page -> | ||||||
| @ -247,7 +253,8 @@ let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> ta | |||||||
|         do! (if model.pageId = "new" then Data.Page.add else Data.Page.update) page conn |         do! (if model.pageId = "new" then Data.Page.add else Data.Page.update) page conn | ||||||
|         if updateList then do! PageListCache.update ctx |         if updateList then do! PageListCache.update ctx | ||||||
|         do! addMessage ctx { UserMessage.success with message = "Page saved successfully" } |         do! addMessage ctx { UserMessage.success with message = "Page saved successfully" } | ||||||
|         return! redirectToGet $"/admin/page/{PageId.toString page.id}/edit" next ctx |         return! | ||||||
|  |             redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/page/{PageId.toString page.id}/edit")) next ctx | ||||||
|     | None -> return! Error.notFound next ctx |     | None -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -255,7 +262,7 @@ let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> ta | |||||||
| 
 | 
 | ||||||
| // GET /admin/settings | // GET /admin/settings | ||||||
| let settings : HttpHandler = requireUser >=> fun next ctx -> task { | let settings : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     let  webLog   = WebLogCache.get ctx |     let  webLog   = webLog ctx | ||||||
|     let! allPages = Data.Page.findAll webLog.id (conn ctx) |     let! allPages = Data.Page.findAll webLog.id (conn ctx) | ||||||
|     return! |     return! | ||||||
|         Hash.FromAnonymousObject |         Hash.FromAnonymousObject | ||||||
| @ -278,9 +285,10 @@ let settings : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| 
 | 
 | ||||||
| // POST /admin/settings | // POST /admin/settings | ||||||
| let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     let  conn  = conn ctx |     let  webLog = webLog ctx | ||||||
|     let! model = ctx.BindFormAsync<SettingsModel> () |     let  conn   = conn ctx | ||||||
|     match! Data.WebLog.findById (WebLogCache.get ctx).id conn with |     let! model  = ctx.BindFormAsync<SettingsModel> () | ||||||
|  |     match! Data.WebLog.findById webLog.id conn with | ||||||
|     | Some webLog -> |     | Some webLog -> | ||||||
|         let updated = |         let updated = | ||||||
|             { webLog with |             { webLog with | ||||||
| @ -294,10 +302,10 @@ let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - | |||||||
|         do! Data.WebLog.updateSettings updated conn |         do! Data.WebLog.updateSettings updated conn | ||||||
| 
 | 
 | ||||||
|         // Update cache |         // Update cache | ||||||
|         WebLogCache.set ctx updated |         WebLogCache.set updated | ||||||
|      |      | ||||||
|         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" next ctx |         return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin")) next ctx | ||||||
|     | None -> return! Error.notFound next ctx |     | None -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -305,7 +313,7 @@ let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - | |||||||
| 
 | 
 | ||||||
| // GET /admin/tag-mappings | // GET /admin/tag-mappings | ||||||
| let tagMappings : HttpHandler = requireUser >=> fun next ctx -> task { | let tagMappings : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     let! mappings = Data.TagMap.findByWebLogId (webLogId ctx) (conn ctx) |     let! mappings = Data.TagMap.findByWebLogId (webLog ctx).id (conn ctx) | ||||||
|     return! |     return! | ||||||
|         Hash.FromAnonymousObject |         Hash.FromAnonymousObject | ||||||
|             {|  csrf        = csrfToken ctx |             {|  csrf        = csrfToken ctx | ||||||
| @ -318,13 +326,12 @@ let tagMappings : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| 
 | 
 | ||||||
| // GET /admin/tag-mapping/{id}/edit | // GET /admin/tag-mapping/{id}/edit | ||||||
| let editMapping tagMapId : HttpHandler = requireUser >=> fun next ctx -> task { | let editMapping tagMapId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     let webLogId = webLogId ctx |     let isNew  = tagMapId = "new" | ||||||
|     let isNew    = tagMapId = "new" |     let tagMap = | ||||||
|     let tagMap   = |  | ||||||
|         if isNew then |         if isNew then | ||||||
|             Task.FromResult (Some { TagMap.empty with id = TagMapId "new" }) |             Task.FromResult (Some { TagMap.empty with id = TagMapId "new" }) | ||||||
|         else |         else | ||||||
|             Data.TagMap.findById (TagMapId tagMapId) webLogId (conn ctx) |             Data.TagMap.findById (TagMapId tagMapId) (webLog ctx).id (conn ctx) | ||||||
|     match! tagMap with |     match! tagMap with | ||||||
|     | Some tm -> |     | Some tm -> | ||||||
|         return! |         return! | ||||||
| @ -339,26 +346,29 @@ let editMapping tagMapId : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| 
 | 
 | ||||||
| // POST /admin/tag-mapping/save | // POST /admin/tag-mapping/save | ||||||
| let saveMapping : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let saveMapping : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     let  webLogId = webLogId ctx |     let  webLog = webLog ctx | ||||||
|     let  conn     = conn     ctx |     let  conn   = conn   ctx | ||||||
|     let! model    = ctx.BindFormAsync<EditTagMapModel> () |     let! model  = ctx.BindFormAsync<EditTagMapModel> () | ||||||
|     let  tagMap   = |     let  tagMap = | ||||||
|         if model.id = "new" then |         if model.id = "new" then | ||||||
|             Task.FromResult (Some { TagMap.empty with id = TagMapId.create (); webLogId = webLogId }) |             Task.FromResult (Some { TagMap.empty with id = TagMapId.create (); webLogId = webLog.id }) | ||||||
|         else |         else | ||||||
|             Data.TagMap.findById (TagMapId model.id) webLogId conn |             Data.TagMap.findById (TagMapId model.id) webLog.id conn | ||||||
|     match! tagMap with |     match! tagMap with | ||||||
|     | Some tm -> |     | Some tm -> | ||||||
|         do! Data.TagMap.save { tm with tag = model.tag.ToLower (); urlValue = model.urlValue.ToLower () } conn |         do! Data.TagMap.save { tm with tag = model.tag.ToLower (); urlValue = model.urlValue.ToLower () } conn | ||||||
|         do! addMessage ctx { UserMessage.success with message = "Tag mapping saved successfully" } |         do! addMessage ctx { UserMessage.success with message = "Tag mapping saved successfully" } | ||||||
|         return! redirectToGet $"/admin/tag-mapping/{TagMapId.toString tm.id}/edit" next ctx |         return! | ||||||
|  |             redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/tag-mapping/{TagMapId.toString tm.id}/edit")) | ||||||
|  |                 next ctx | ||||||
|     | None -> return! Error.notFound next ctx |     | None -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // POST /admin/tag-mapping/{id}/delete | // POST /admin/tag-mapping/{id}/delete | ||||||
| let deleteMapping tagMapId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let deleteMapping tagMapId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     match! Data.TagMap.delete (TagMapId tagMapId) (webLogId ctx) (conn ctx) with |     let webLog = webLog ctx | ||||||
|  |     match! Data.TagMap.delete (TagMapId tagMapId) webLog.id (conn ctx) 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! redirectToGet "/admin/tag-mappings" next ctx |     return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/tag-mappings")) next ctx | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,15 +3,19 @@ module MyWebLog.Handlers.Error | |||||||
| 
 | 
 | ||||||
| open System.Net | open System.Net | ||||||
| open System.Threading.Tasks | open System.Threading.Tasks | ||||||
| open Microsoft.AspNetCore.Http |  | ||||||
| open Giraffe | open Giraffe | ||||||
|  | open Microsoft.AspNetCore.Http | ||||||
|  | open MyWebLog | ||||||
| 
 | 
 | ||||||
| /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response | /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response | ||||||
| let notAuthorized : HttpHandler = fun next ctx -> | let notAuthorized : HttpHandler = fun next ctx -> task { | ||||||
|     (next, ctx) |     let webLog = ctx.Items["webLog"] :?> WebLog | ||||||
|     ||> match ctx.Request.Method with |     if ctx.Request.Method = "GET" then | ||||||
|         | "GET" -> redirectTo false $"/user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}" |         let returnUrl = WebUtility.UrlEncode ctx.Request.Path | ||||||
|         | _ -> setStatusCode 401 >=> fun _ _ -> Task.FromResult<HttpContext option> None |         return! redirectTo false (WebLog.relativeUrl webLog (Permalink $"user/log-on?returnUrl={returnUrl}")) next ctx | ||||||
|  |     else | ||||||
|  |         return! (setStatusCode 401 >=> fun _ _ -> Task.FromResult<HttpContext option> None) next ctx | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there | /// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there | ||||||
| let notFound : HttpHandler = | let notFound : HttpHandler = | ||||||
|  | |||||||
| @ -67,17 +67,18 @@ let generator (ctx : HttpContext) = | |||||||
|         generatorString <- Option.ofObj cfg["Generator"] |         generatorString <- Option.ofObj cfg["Generator"] | ||||||
|         defaultArg generatorString "generator not configured" |         defaultArg generatorString "generator not configured" | ||||||
| 
 | 
 | ||||||
| open DotLiquid |  | ||||||
| open MyWebLog | open MyWebLog | ||||||
| 
 | 
 | ||||||
|  | /// Get the web log for the request from the context (established by middleware) | ||||||
|  | let webLog (ctx : HttpContext) = | ||||||
|  |     ctx.Items["webLog"] :?> WebLog | ||||||
|  | 
 | ||||||
|  | open DotLiquid | ||||||
|  | 
 | ||||||
| /// Either get the web log from the hash, or get it from the cache and add it to the hash | /// Either get the web log from the hash, or get it from the cache and add it to the hash | ||||||
| let private deriveWebLogFromHash (hash : Hash) ctx = | let private deriveWebLogFromHash (hash : Hash) ctx = | ||||||
|     match hash.ContainsKey "web_log" with |     if hash.ContainsKey "web_log" then () else hash.Add ("web_log", webLog ctx) | ||||||
|     | true -> hash["web_log"] :?> WebLog |     hash["web_log"] :?> WebLog | ||||||
|     | false -> |  | ||||||
|         let wl = WebLogCache.get ctx |  | ||||||
|         hash.Add ("web_log", wl) |  | ||||||
|         wl |  | ||||||
| 
 | 
 | ||||||
| open Giraffe | open Giraffe | ||||||
| 
 | 
 | ||||||
| @ -118,9 +119,6 @@ let redirectToGet url : HttpHandler = fun next ctx -> task { | |||||||
|     return! redirectTo false url next ctx |     return! redirectTo false url next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Get the web log ID for the current request |  | ||||||
| let webLogId ctx = (WebLogCache.get ctx).id |  | ||||||
| 
 |  | ||||||
| open System.Security.Claims | open System.Security.Claims | ||||||
| 
 | 
 | ||||||
| /// Get the user ID for the current request | /// Get the user ID for the current request | ||||||
| @ -159,7 +157,7 @@ let templatesForTheme ctx (typ : string) = | |||||||
|     seq { |     seq { | ||||||
|         KeyValuePair.Create ("", $"- Default (single-{typ}) -") |         KeyValuePair.Create ("", $"- Default (single-{typ}) -") | ||||||
|         yield! |         yield! | ||||||
|             Path.Combine ("themes", (WebLogCache.get ctx).themePath) |             Path.Combine ("themes", (webLog ctx).themePath) | ||||||
|             |> Directory.EnumerateFiles |             |> Directory.EnumerateFiles | ||||||
|             |> Seq.filter (fun it -> it.EndsWith $"{typ}.liquid") |             |> Seq.filter (fun it -> it.EndsWith $"{typ}.liquid") | ||||||
|             |> Seq.map (fun it -> |             |> Seq.map (fun it -> | ||||||
|  | |||||||
| @ -2,21 +2,19 @@ | |||||||
| module MyWebLog.Handlers.Post | module MyWebLog.Handlers.Post | ||||||
| 
 | 
 | ||||||
| open System | open System | ||||||
| open Giraffe |  | ||||||
| open Microsoft.AspNetCore.Http |  | ||||||
| 
 | 
 | ||||||
| /// Split the "rest" capture for categories and tags into the page number and category/tag URL parts | /// Parse a slug and page number from an "everything else" URL | ||||||
| let private pathAndPageNumber (ctx : HttpContext) = | let private parseSlugAndPage (slugAndPage : string seq) = | ||||||
|     let slugs     = (string ctx.Request.RouteValues["slug"]).Split "/" |> Array.filter (fun it -> it <> "") |     let slugs   = (slugAndPage |> Seq.skip 1 |> Seq.head).Split "/" |> Array.filter (fun it -> it <> "") | ||||||
|     let pageIdx   = Array.IndexOf (slugs, "page") |     let pageIdx = Array.IndexOf (slugs, "page") | ||||||
|     let pageNbr   = |     let pageNbr = | ||||||
|         match pageIdx with |         match pageIdx with | ||||||
|         | -1 -> Some 1L |         | -1 -> Some 1 | ||||||
|         | idx when idx + 2 = slugs.Length -> Some (int64 slugs[pageIdx + 1]) |         | idx when idx + 2 = slugs.Length -> Some (int slugs[pageIdx + 1]) | ||||||
|         | _ -> None |         | _ -> None | ||||||
|     let slugParts = if pageIdx > 0 then Array.truncate pageIdx slugs else slugs |     let slugParts = if pageIdx > 0 then Array.truncate pageIdx slugs else slugs | ||||||
|     pageNbr, String.Join ("/", slugParts) |     pageNbr, String.Join ("/", slugParts) | ||||||
|      | 
 | ||||||
| /// The type of post list being prepared | /// The type of post list being prepared | ||||||
| type ListType = | type ListType = | ||||||
|     | AdminList |     | AdminList | ||||||
| @ -47,10 +45,11 @@ open DotLiquid | |||||||
| open MyWebLog.ViewModels | open MyWebLog.ViewModels | ||||||
| 
 | 
 | ||||||
| /// Convert a list of posts into items ready to be displayed | /// Convert a list of posts into items ready to be displayed | ||||||
| let private preparePostList webLog posts listType url pageNbr perPage ctx conn = task { | let private preparePostList webLog posts listType (url : string) pageNbr perPage ctx conn = task { | ||||||
|     let! authors     = getAuthors     webLog posts conn |     let! authors     = getAuthors     webLog posts conn | ||||||
|     let! tagMappings = getTagMappings webLog posts conn |     let! tagMappings = getTagMappings webLog posts conn | ||||||
|     let postItems = |     let  relUrl it   = Some <| WebLog.relativeUrl webLog (Permalink it) | ||||||
|  |     let  postItems   = | ||||||
|         posts |         posts | ||||||
|         |> Seq.ofList |         |> Seq.ofList | ||||||
|         |> Seq.truncate perPage |         |> Seq.truncate perPage | ||||||
| @ -65,24 +64,24 @@ let private preparePostList webLog posts listType url pageNbr perPage ctx conn = | |||||||
|         | _ -> Task.FromResult (None, None) |         | _ -> Task.FromResult (None, None) | ||||||
|     let newerLink = |     let newerLink = | ||||||
|         match listType, pageNbr with |         match listType, pageNbr with | ||||||
|         | SinglePost,   _  -> newerPost |> Option.map (fun p -> Permalink.toString p.permalink) |         | SinglePost,   _ -> newerPost |> Option.map (fun p -> Permalink.toString p.permalink) | ||||||
|         | _,            1L -> None |         | _,            1 -> None | ||||||
|         | PostList,     2L    when webLog.defaultPage = "posts" -> Some "" |         | PostList,     2    when webLog.defaultPage = "posts" -> Some "" | ||||||
|         | PostList,     _  -> Some $"page/{pageNbr - 1L}" |         | PostList,     _ -> relUrl $"page/{pageNbr - 1}" | ||||||
|         | CategoryList, 2L -> Some $"category/{url}/" |         | CategoryList, 2 -> relUrl $"category/{url}/" | ||||||
|         | CategoryList, _  -> Some $"category/{url}/page/{pageNbr - 1L}" |         | CategoryList, _ -> relUrl $"category/{url}/page/{pageNbr - 1}" | ||||||
|         | TagList,      2L -> Some $"tag/{url}/" |         | TagList,      2 -> relUrl $"tag/{url}/" | ||||||
|         | TagList,      _  -> Some $"tag/{url}/page/{pageNbr - 1L}" |         | TagList,      _ -> relUrl $"tag/{url}/page/{pageNbr - 1}" | ||||||
|         | AdminList,    2L -> Some "admin/posts" |         | AdminList,    2 -> relUrl "admin/posts" | ||||||
|         | AdminList,    _  -> Some $"admin/posts/page/{pageNbr - 1L}" |         | AdminList,    _ -> relUrl $"admin/posts/page/{pageNbr - 1}" | ||||||
|     let olderLink = |     let olderLink = | ||||||
|         match listType, List.length posts > perPage with |         match listType, List.length posts > perPage with | ||||||
|         | SinglePost,   _     -> olderPost |> Option.map (fun p -> Permalink.toString p.permalink) |         | SinglePost,   _     -> olderPost |> Option.map (fun p -> Permalink.toString p.permalink) | ||||||
|         | _,            false -> None |         | _,            false -> None | ||||||
|         | PostList,     true  -> Some $"page/{pageNbr + 1L}" |         | PostList,     true  -> relUrl $"page/{pageNbr + 1}" | ||||||
|         | CategoryList, true  -> Some $"category/{url}/page/{pageNbr + 1L}" |         | CategoryList, true  -> relUrl $"category/{url}/page/{pageNbr + 1}" | ||||||
|         | TagList,      true  -> Some $"tag/{url}/page/{pageNbr + 1L}" |         | TagList,      true  -> relUrl $"tag/{url}/page/{pageNbr + 1}" | ||||||
|         | AdminList,    true  -> Some $"admin/posts/page/{pageNbr + 1L}" |         | AdminList,    true  -> relUrl $"admin/posts/page/{pageNbr + 1}" | ||||||
|     let model = |     let model = | ||||||
|         { posts      = postItems |         { posts      = postItems | ||||||
|           authors    = authors |           authors    = authors | ||||||
| @ -95,28 +94,30 @@ let private preparePostList webLog posts listType url pageNbr perPage ctx conn = | |||||||
|     return Hash.FromAnonymousObject {| model = model; categories = CategoryCache.get ctx; tag_mappings = tagMappings |} |     return Hash.FromAnonymousObject {| model = model; categories = CategoryCache.get ctx; tag_mappings = tagMappings |} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | open Giraffe | ||||||
|  | 
 | ||||||
| // GET /page/{pageNbr} | // GET /page/{pageNbr} | ||||||
| let pageOfPosts pageNbr : HttpHandler = fun next ctx -> task { | let pageOfPosts pageNbr : HttpHandler = fun next ctx -> task { | ||||||
|     let  webLog = WebLogCache.get ctx |     let  webLog = webLog ctx | ||||||
|     let  conn   = conn ctx |     let  conn   = conn   ctx | ||||||
|     let! posts  = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage conn |     let! posts  = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage conn | ||||||
|     let! hash   = preparePostList webLog posts PostList "" pageNbr webLog.postsPerPage ctx conn |     let! hash   = preparePostList webLog posts PostList "" pageNbr webLog.postsPerPage ctx conn | ||||||
|     let  title  = |     let  title  = | ||||||
|         match pageNbr, webLog.defaultPage with |         match pageNbr, webLog.defaultPage with | ||||||
|         | 1L, "posts" -> None |         | 1, "posts" -> None | ||||||
|         | _,  "posts" -> Some $"Page {pageNbr}" |         | _, "posts" -> Some $"Page {pageNbr}" | ||||||
|         | _,  _       -> Some $"Page {pageNbr} « Posts" |         | _,  _      -> Some $"Page {pageNbr} « Posts" | ||||||
|     match title with Some ttl -> hash.Add ("page_title", ttl) | None -> () |     match title with Some ttl -> hash.Add ("page_title", ttl) | None -> () | ||||||
|     if pageNbr = 1L && webLog.defaultPage = "posts" then hash.Add ("is_home", true) |     if pageNbr = 1 && webLog.defaultPage = "posts" then hash.Add ("is_home", true) | ||||||
|     return! themedView "index" next ctx hash |     return! themedView "index" next ctx hash | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GET /category/{slug}/ | // GET /category/{slug}/ | ||||||
| // GET /category/{slug}/page/{pageNbr} | // GET /category/{slug}/page/{pageNbr} | ||||||
| let pageOfCategorizedPosts : HttpHandler = fun next ctx -> task { | let pageOfCategorizedPosts slugAndPage : HttpHandler = fun next ctx -> task { | ||||||
|     let  webLog = WebLogCache.get ctx |     let webLog = webLog ctx | ||||||
|     let  conn   = conn ctx |     let conn   = conn   ctx | ||||||
|     match pathAndPageNumber ctx with |     match parseSlugAndPage slugAndPage with | ||||||
|     | Some pageNbr, slug ->  |     | Some pageNbr, slug ->  | ||||||
|         let allCats = CategoryCache.get ctx |         let allCats = CategoryCache.get ctx | ||||||
|         let cat     = allCats |> Array.find (fun cat -> cat.slug = slug) |         let cat     = allCats |> Array.find (fun cat -> cat.slug = slug) | ||||||
| @ -130,7 +131,7 @@ let pageOfCategorizedPosts : HttpHandler = fun next ctx -> task { | |||||||
|         match! Data.Post.findPageOfCategorizedPosts webLog.id catIds pageNbr webLog.postsPerPage conn with |         match! Data.Post.findPageOfCategorizedPosts webLog.id catIds pageNbr webLog.postsPerPage conn with | ||||||
|         | posts when List.length posts > 0 -> |         | posts when List.length posts > 0 -> | ||||||
|             let! hash    = preparePostList webLog posts CategoryList cat.slug pageNbr webLog.postsPerPage ctx conn |             let! hash    = preparePostList webLog posts CategoryList cat.slug pageNbr webLog.postsPerPage ctx conn | ||||||
|             let  pgTitle = if pageNbr = 1L then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" |             let  pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" | ||||||
|             hash.Add ("page_title", $"{cat.name}: Category Archive{pgTitle}") |             hash.Add ("page_title", $"{cat.name}: Category Archive{pgTitle}") | ||||||
|             hash.Add ("subtitle", cat.description.Value) |             hash.Add ("subtitle", cat.description.Value) | ||||||
|             hash.Add ("is_category", true) |             hash.Add ("is_category", true) | ||||||
| @ -143,10 +144,10 @@ open System.Web | |||||||
| 
 | 
 | ||||||
| // GET /tag/{tag}/ | // GET /tag/{tag}/ | ||||||
| // GET /tag/{tag}/page/{pageNbr} | // GET /tag/{tag}/page/{pageNbr} | ||||||
| let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { | let pageOfTaggedPosts slugAndPage : HttpHandler = fun next ctx -> task { | ||||||
|     let webLog = WebLogCache.get ctx |     let webLog = webLog ctx | ||||||
|     let conn   = conn ctx |     let conn   = conn   ctx | ||||||
|     match pathAndPageNumber ctx with |     match parseSlugAndPage slugAndPage with | ||||||
|     | Some pageNbr, rawTag ->  |     | Some pageNbr, rawTag ->  | ||||||
|         let  urlTag = HttpUtility.UrlDecode rawTag |         let  urlTag = HttpUtility.UrlDecode rawTag | ||||||
|         let! tag    = backgroundTask { |         let! tag    = backgroundTask { | ||||||
| @ -157,7 +158,7 @@ let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { | |||||||
|         match! Data.Post.findPageOfTaggedPosts webLog.id tag pageNbr webLog.postsPerPage conn with |         match! Data.Post.findPageOfTaggedPosts webLog.id tag pageNbr webLog.postsPerPage conn with | ||||||
|         | posts when List.length posts > 0 -> |         | posts when List.length posts > 0 -> | ||||||
|             let! hash    = preparePostList webLog posts TagList rawTag pageNbr webLog.postsPerPage ctx conn |             let! hash    = preparePostList webLog posts TagList rawTag pageNbr webLog.postsPerPage ctx conn | ||||||
|             let  pgTitle = if pageNbr = 1L then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" |             let  pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" | ||||||
|             hash.Add ("page_title", $"Posts Tagged “{tag}”{pgTitle}") |             hash.Add ("page_title", $"Posts Tagged “{tag}”{pgTitle}") | ||||||
|             hash.Add ("is_tag", true) |             hash.Add ("is_tag", true) | ||||||
|             return! themedView "index" next ctx hash |             return! themedView "index" next ctx hash | ||||||
| @ -166,15 +167,18 @@ let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { | |||||||
|             let spacedTag = tag.Replace ("-", " ") |             let spacedTag = tag.Replace ("-", " ") | ||||||
|             match! Data.Post.findPageOfTaggedPosts webLog.id spacedTag pageNbr 1 conn with |             match! Data.Post.findPageOfTaggedPosts webLog.id spacedTag pageNbr 1 conn with | ||||||
|             | posts when List.length posts > 0 -> |             | posts when List.length posts > 0 -> | ||||||
|                 let endUrl = if pageNbr = 1L then "" else $"page/{pageNbr}" |                 let endUrl = if pageNbr = 1 then "" else $"page/{pageNbr}" | ||||||
|                 return! redirectTo true $"""/tag/{spacedTag.Replace (" ", "+")}/{endUrl}""" next ctx |                 return! | ||||||
|  |                     redirectTo true | ||||||
|  |                         (WebLog.relativeUrl webLog (Permalink $"""tag/{spacedTag.Replace (" ", "+")}/{endUrl}""")) | ||||||
|  |                         next ctx | ||||||
|             | _ -> return! Error.notFound next ctx |             | _ -> return! Error.notFound next ctx | ||||||
|     | None, _ -> return! Error.notFound next ctx |     | None, _ -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GET / | // GET / | ||||||
| let home : HttpHandler = fun next ctx -> task { | let home : HttpHandler = fun next ctx -> task { | ||||||
|     let webLog = WebLogCache.get ctx |     let webLog = webLog ctx | ||||||
|     match webLog.defaultPage with |     match webLog.defaultPage with | ||||||
|     | "posts" -> return! pageOfPosts 1 next ctx |     | "posts" -> return! pageOfPosts 1 next ctx | ||||||
|     | pageId -> |     | pageId -> | ||||||
| @ -190,6 +194,7 @@ let home : HttpHandler = fun next ctx -> task { | |||||||
|         | None -> return! Error.notFound next ctx |         | None -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| open System.IO | open System.IO | ||||||
| open System.ServiceModel.Syndication | open System.ServiceModel.Syndication | ||||||
| open System.Text.RegularExpressions | open System.Text.RegularExpressions | ||||||
| @ -198,12 +203,12 @@ open System.Xml | |||||||
| // GET /feed.xml | // GET /feed.xml | ||||||
| //   (Routing handled by catch-all handler for future configurability) | //   (Routing handled by catch-all handler for future configurability) | ||||||
| let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | ||||||
|     let  conn    = conn ctx |     let  conn    = conn   ctx | ||||||
|     let  webLog  = WebLogCache.get ctx |     let  webLog  = webLog ctx | ||||||
|     let  urlBase = $"https://{webLog.urlBase}/" |  | ||||||
|     // TODO: hard-coded number of items |     // TODO: hard-coded number of items | ||||||
|     let! posts   = Data.Post.findPageOfPublishedPosts webLog.id 1L 10 conn |     let! posts   = Data.Post.findPageOfPublishedPosts webLog.id 1 10 conn | ||||||
|     let! authors = getAuthors webLog posts conn |     let! authors = getAuthors     webLog posts conn | ||||||
|  |     let! tagMaps = getTagMappings webLog posts conn | ||||||
|     let  cats    = CategoryCache.get ctx |     let  cats    = CategoryCache.get ctx | ||||||
|      |      | ||||||
|     let toItem (post : Post) = |     let toItem (post : Post) = | ||||||
| @ -213,25 +218,29 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | |||||||
|             | txt when txt.Length < 255 -> txt |             | txt when txt.Length < 255 -> txt | ||||||
|             | txt -> $"{txt.Substring (0, 252)}..." |             | txt -> $"{txt.Substring (0, 252)}..." | ||||||
|         let item = SyndicationItem ( |         let item = SyndicationItem ( | ||||||
|             Id              = $"{urlBase}{Permalink.toString post.permalink}", |             Id              = WebLog.absoluteUrl webLog post.permalink, | ||||||
|             Title           = TextSyndicationContent.CreateHtmlContent post.title, |             Title           = TextSyndicationContent.CreateHtmlContent post.title, | ||||||
|             PublishDate     = DateTimeOffset post.publishedOn.Value, |             PublishDate     = DateTimeOffset post.publishedOn.Value, | ||||||
|             LastUpdatedTime = DateTimeOffset post.updatedOn, |             LastUpdatedTime = DateTimeOffset post.updatedOn, | ||||||
|             Content         = TextSyndicationContent.CreatePlaintextContent plainText) |             Content         = TextSyndicationContent.CreatePlaintextContent plainText) | ||||||
|         item.AddPermalink (Uri item.Id) |         item.AddPermalink (Uri item.Id) | ||||||
|          |          | ||||||
|         let encoded = post.text.Replace("src=\"/", $"src=\"{urlBase}").Replace ("href=\"/", $"href=\"{urlBase}") |         let encoded = | ||||||
|  |             post.text.Replace("src=\"/", $"src=\"{webLog.urlBase}/").Replace ("href=\"/", $"href=\"{webLog.urlBase}/") | ||||||
|         item.ElementExtensions.Add ("encoded", "http://purl.org/rss/1.0/modules/content/", encoded) |         item.ElementExtensions.Add ("encoded", "http://purl.org/rss/1.0/modules/content/", encoded) | ||||||
|         item.Authors.Add (SyndicationPerson ( |         item.Authors.Add (SyndicationPerson ( | ||||||
|             Name = (authors |> List.find (fun a -> a.name = WebLogUserId.toString post.authorId)).value)) |             Name = (authors |> List.find (fun a -> a.name = WebLogUserId.toString post.authorId)).value)) | ||||||
|         [ post.categoryIds |         [ post.categoryIds | ||||||
|           |> List.map (fun catId -> |           |> List.map (fun catId -> | ||||||
|               let cat = cats |> Array.find (fun c -> c.id = CategoryId.toString catId) |               let cat = cats |> Array.find (fun c -> c.id = CategoryId.toString catId) | ||||||
|               SyndicationCategory (cat.name, $"{urlBase}category/{cat.slug}/", cat.name)) |               SyndicationCategory (cat.name, WebLog.absoluteUrl webLog (Permalink $"category/{cat.slug}/"), cat.name)) | ||||||
|           post.tags |           post.tags | ||||||
|           |> List.map (fun tag -> |           |> List.map (fun tag -> | ||||||
|               let urlTag = tag.Replace (" ", "+") |               let urlTag = | ||||||
|               SyndicationCategory (tag, $"{urlBase}tag/{urlTag}/", $"{tag} (tag)")) |                   match tagMaps |> List.tryFind (fun tm -> tm.tag = tag) with | ||||||
|  |                   | Some tm -> tm.urlValue | ||||||
|  |                   | None -> tag.Replace (" ", "+") | ||||||
|  |               SyndicationCategory (tag, WebLog.absoluteUrl webLog (Permalink $"tag/{urlTag}/"), $"{tag} (tag)")) | ||||||
|         ] |         ] | ||||||
|         |> List.concat |         |> List.concat | ||||||
|         |> List.iter item.Categories.Add |         |> List.iter item.Categories.Add | ||||||
| @ -245,12 +254,12 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | |||||||
|     feed.Generator       <- generator ctx |     feed.Generator       <- generator ctx | ||||||
|     feed.Items           <- posts |> Seq.ofList |> Seq.map toItem |     feed.Items           <- posts |> Seq.ofList |> Seq.map toItem | ||||||
|     feed.Language        <- "en" |     feed.Language        <- "en" | ||||||
|     feed.Id              <- urlBase |     feed.Id              <- webLog.urlBase | ||||||
|      |      | ||||||
|     feed.Links.Add (SyndicationLink (Uri $"{urlBase}feed.xml", "self", "", "application/rss+xml", 0L)) |     feed.Links.Add (SyndicationLink (Uri $"{webLog.urlBase}/feed.xml", "self", "", "application/rss+xml", 0L)) | ||||||
|     feed.AttributeExtensions.Add |     feed.AttributeExtensions.Add | ||||||
|         (XmlQualifiedName ("content", "http://www.w3.org/2000/xmlns/"), "http://purl.org/rss/1.0/modules/content/") |         (XmlQualifiedName ("content", "http://www.w3.org/2000/xmlns/"), "http://purl.org/rss/1.0/modules/content/") | ||||||
|     feed.ElementExtensions.Add ("link", "", urlBase) |     feed.ElementExtensions.Add ("link", "", webLog.urlBase) | ||||||
|      |      | ||||||
|     use mem = new MemoryStream () |     use mem = new MemoryStream () | ||||||
|     use xml = XmlWriter.Create mem |     use xml = XmlWriter.Create mem | ||||||
| @ -266,15 +275,18 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | |||||||
| 
 | 
 | ||||||
| /// Sequence where the first returned value is the proper handler for the link | /// Sequence where the first returned value is the proper handler for the link | ||||||
| let private deriveAction ctx : HttpHandler seq = | let private deriveAction ctx : HttpHandler seq = | ||||||
|     let webLog    = WebLogCache.get ctx |     let webLog    = webLog ctx | ||||||
|     let conn      = conn ctx |     let conn      = conn   ctx | ||||||
|     let textLink  = string ctx.Request.RouteValues["link"] |     let _, extra  = WebLog.hostAndPath webLog | ||||||
|     let permalink = Permalink textLink |     let textLink  = if extra = "" then ctx.Request.Path.Value else ctx.Request.Path.Value.Substring extra.Length | ||||||
|     let await it  = (Async.AwaitTask >> Async.RunSynchronously) it |     let await it  = (Async.AwaitTask >> Async.RunSynchronously) it | ||||||
|     seq { |     seq { | ||||||
|  |         // Home page directory without the directory slash  | ||||||
|  |         if textLink = "" then yield redirectTo true (WebLog.relativeUrl webLog Permalink.empty) | ||||||
|  |         let permalink = Permalink (textLink.Substring 1) | ||||||
|         // Current post |         // Current post | ||||||
|         match Data.Post.findByPermalink permalink webLog.id conn |> await with |         match Data.Post.findByPermalink permalink webLog.id conn |> await with | ||||||
|         | Some post ->  |         | Some post -> | ||||||
|             let model = preparePostList webLog [ post ] SinglePost "" 1 1 ctx conn |> await |             let model = preparePostList webLog [ post ] SinglePost "" 1 1 ctx conn |> await | ||||||
|             model.Add ("page_title", post.title) |             model.Add ("page_title", post.title) | ||||||
|             yield fun next ctx -> themedView "single-post" next ctx model |             yield fun next ctx -> themedView "single-post" next ctx model | ||||||
| @ -288,27 +300,27 @@ let private deriveAction ctx : HttpHandler seq = | |||||||
|         | None -> () |         | None -> () | ||||||
|         // RSS feed |         // RSS feed | ||||||
|         // TODO: configure this via web log |         // TODO: configure this via web log | ||||||
|         if textLink = "feed.xml" then yield generateFeed |         if textLink = "/feed.xml" then yield generateFeed | ||||||
|         // Post differing only by trailing slash |         // Post differing only by trailing slash | ||||||
|         let altLink = Permalink (if textLink.EndsWith "/" then textLink[..textLink.Length - 2] else $"{textLink}/") |         let altLink = Permalink (if textLink.EndsWith "/" then textLink[..textLink.Length - 2] else $"{textLink}/") | ||||||
|         match Data.Post.findByPermalink altLink webLog.id conn |> await with |         match Data.Post.findByPermalink altLink webLog.id conn |> await with | ||||||
|         | Some post -> yield redirectTo true $"/{Permalink.toString post.permalink}" |         | Some post -> yield redirectTo true (WebLog.relativeUrl webLog post.permalink) | ||||||
|         | None -> () |         | None -> () | ||||||
|         // Page differing only by trailing slash |         // Page differing only by trailing slash | ||||||
|         match Data.Page.findByPermalink altLink webLog.id conn |> await with |         match Data.Page.findByPermalink altLink webLog.id conn |> await with | ||||||
|         | Some page -> yield redirectTo true $"/{Permalink.toString page.permalink}" |         | Some page -> yield redirectTo true (WebLog.relativeUrl webLog page.permalink) | ||||||
|         | None -> () |         | None -> () | ||||||
|         // Prior post |         // Prior post | ||||||
|         match Data.Post.findCurrentPermalink [ permalink; altLink ] webLog.id conn |> await with |         match Data.Post.findCurrentPermalink [ permalink; altLink ] webLog.id conn |> await with | ||||||
|         | Some link -> yield redirectTo true $"/{Permalink.toString link}" |         | Some link -> yield redirectTo true (WebLog.relativeUrl webLog link) | ||||||
|         | None -> () |         | None -> () | ||||||
|         // Prior page |         // Prior page | ||||||
|         match Data.Page.findCurrentPermalink [ permalink; altLink ] webLog.id conn |> await with |         match Data.Page.findCurrentPermalink [ permalink; altLink ] webLog.id conn |> await with | ||||||
|         | Some link -> yield redirectTo true $"/{Permalink.toString link}" |         | Some link -> yield redirectTo true (WebLog.relativeUrl webLog link) | ||||||
|         | None -> () |         | None -> () | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| // GET {**link} | // GET {all-of-the-above} | ||||||
| let catchAll : HttpHandler = fun next ctx -> task { | let catchAll : HttpHandler = fun next ctx -> task { | ||||||
|     match deriveAction ctx |> Seq.tryHead with |     match deriveAction ctx |> Seq.tryHead with | ||||||
|     | Some handler -> return! handler next ctx |     | Some handler -> return! handler next ctx | ||||||
| @ -318,8 +330,8 @@ let catchAll : HttpHandler = fun next ctx -> task { | |||||||
| // GET /admin/posts | // GET /admin/posts | ||||||
| // GET /admin/posts/page/{pageNbr} | // GET /admin/posts/page/{pageNbr} | ||||||
| let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { | let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     let  webLog = WebLogCache.get ctx |     let  webLog = webLog ctx | ||||||
|     let  conn   = conn ctx |     let  conn   = conn   ctx | ||||||
|     let! posts  = Data.Post.findPageOfPosts webLog.id pageNbr 25 conn |     let! posts  = Data.Post.findPageOfPosts webLog.id pageNbr 25 conn | ||||||
|     let! hash   = preparePostList webLog posts AdminList "" pageNbr 25 ctx conn |     let! hash   = preparePostList webLog posts AdminList "" pageNbr 25 ctx conn | ||||||
|     hash.Add ("page_title", "Posts") |     hash.Add ("page_title", "Posts") | ||||||
| @ -328,8 +340,8 @@ let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| 
 | 
 | ||||||
| // GET /admin/post/{id}/edit | // GET /admin/post/{id}/edit | ||||||
| let edit postId : HttpHandler = requireUser >=> fun next ctx -> task { | let edit postId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     let  webLog = WebLogCache.get ctx |     let  webLog = webLog ctx | ||||||
|     let  conn   = conn     ctx |     let  conn   = conn   ctx | ||||||
|     let! result = task { |     let! result = task { | ||||||
|         match postId with |         match postId with | ||||||
|         | "new" -> return Some ("Write a New Post", { Post.empty with id = PostId "new" }) |         | "new" -> return Some ("Write a New Post", { Post.empty with id = PostId "new" }) | ||||||
| @ -354,7 +366,7 @@ let edit postId : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| 
 | 
 | ||||||
| // GET /admin/post/{id}/permalinks | // GET /admin/post/{id}/permalinks | ||||||
| let editPermalinks postId : HttpHandler = requireUser >=> fun next ctx -> task { | let editPermalinks postId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|     match! Data.Post.findByFullId (PostId postId) (webLogId ctx) (conn ctx) with |     match! Data.Post.findByFullId (PostId postId) (webLog ctx).id (conn ctx) with | ||||||
|     | Some post -> |     | Some post -> | ||||||
|         return! |         return! | ||||||
|             Hash.FromAnonymousObject {| |             Hash.FromAnonymousObject {| | ||||||
| @ -368,41 +380,43 @@ let editPermalinks postId : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
| 
 | 
 | ||||||
| // POST /admin/post/permalinks | // POST /admin/post/permalinks | ||||||
| let savePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let savePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     let! model = ctx.BindFormAsync<ManagePermalinksModel> () |     let  webLog = webLog ctx | ||||||
|     let  links = model.prior |> Array.map Permalink |> List.ofArray |     let! model  = ctx.BindFormAsync<ManagePermalinksModel> () | ||||||
|     match! Data.Post.updatePriorPermalinks (PostId model.id) (webLogId ctx) links (conn ctx) with |     let  links  = model.prior |> Array.map Permalink |> List.ofArray | ||||||
|  |     match! Data.Post.updatePriorPermalinks (PostId model.id) webLog.id links (conn ctx) with | ||||||
|     | true -> |     | true -> | ||||||
|         do! addMessage ctx { UserMessage.success with message = "Post permalinks saved successfully" } |         do! addMessage ctx { UserMessage.success with message = "Post permalinks saved successfully" } | ||||||
|         return! redirectToGet $"/admin/post/{model.id}/permalinks" next ctx |         return! redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/post/{model.id}/permalinks")) next ctx | ||||||
|     | false -> return! Error.notFound next ctx |     | false -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // POST /admin/post/{id}/delete | // POST /admin/post/{id}/delete | ||||||
| let delete postId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let delete postId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     match! Data.Post.delete (PostId postId) (webLogId ctx) (conn ctx) with |     let webLog = webLog ctx | ||||||
|  |     match! Data.Post.delete (PostId postId) webLog.id (conn ctx) with | ||||||
|     | true  -> do! addMessage ctx { UserMessage.success with message = "Post deleted successfully" } |     | true  -> do! addMessage ctx { UserMessage.success with message = "Post deleted successfully" } | ||||||
|     | false -> do! addMessage ctx { UserMessage.error with message = "Post not found; nothing deleted" } |     | false -> do! addMessage ctx { UserMessage.error with message = "Post not found; nothing deleted" } | ||||||
|     return! redirectToGet "/admin/posts" next ctx |     return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/posts")) next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #nowarn "3511" | #nowarn "3511" | ||||||
| 
 | 
 | ||||||
| // POST /admin/post/save | // POST /admin/post/save | ||||||
| let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|     let! model    = ctx.BindFormAsync<EditPostModel> () |     let! model  = ctx.BindFormAsync<EditPostModel> () | ||||||
|     let  webLogId = webLogId ctx |     let  webLog = webLog ctx | ||||||
|     let  conn     = conn     ctx |     let  conn   = conn   ctx | ||||||
|     let  now      = DateTime.UtcNow |     let  now    = DateTime.UtcNow | ||||||
|     let! pst      = task { |     let! pst    = task { | ||||||
|         match model.postId with |         match model.postId with | ||||||
|         | "new" -> |         | "new" -> | ||||||
|             return Some |             return Some | ||||||
|                 { Post.empty with |                 { Post.empty with | ||||||
|                     id        = PostId.create () |                     id        = PostId.create () | ||||||
|                     webLogId  = webLogId |                     webLogId  = webLog.id | ||||||
|                     authorId  = userId ctx |                     authorId  = userId ctx | ||||||
|                 } |                 } | ||||||
|         | postId -> return! Data.Post.findByFullId (PostId postId) webLogId conn |         | postId -> return! Data.Post.findByFullId (PostId postId) webLog.id conn | ||||||
|     } |     } | ||||||
|     match pst with |     match pst with | ||||||
|     | Some post -> |     | Some post -> | ||||||
| @ -460,6 +474,7 @@ let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | |||||||
|                    |> List.length = List.length pst.Value.categoryIds) then |                    |> List.length = List.length pst.Value.categoryIds) then | ||||||
|             do! CategoryCache.update ctx |             do! CategoryCache.update ctx | ||||||
|         do! addMessage ctx { UserMessage.success with message = "Post saved successfully" } |         do! addMessage ctx { UserMessage.success with message = "Post saved successfully" } | ||||||
|         return! redirectToGet $"/admin/post/{PostId.toString post.id}/edit" next ctx |         return! | ||||||
|  |             redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/post/{PostId.toString post.id}/edit")) next ctx | ||||||
|     | None -> return! Error.notFound next ctx |     | None -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,75 +1,89 @@ | |||||||
| /// Routes for this application | /// Routes for this application | ||||||
| module MyWebLog.Handlers.Routes | module MyWebLog.Handlers.Routes | ||||||
| 
 | 
 | ||||||
|  | open Giraffe | ||||||
|  | open MyWebLog | ||||||
|  | 
 | ||||||
|  | let router : HttpHandler = choose [ | ||||||
|  |     GET >=> choose [ | ||||||
|  |         route "/" >=> Post.home | ||||||
|  |     ] | ||||||
|  |     subRoute "/admin" (requireUser >=> choose [ | ||||||
|  |         GET >=> choose [ | ||||||
|  |             route    "" >=> Admin.dashboard | ||||||
|  |             subRoute "/categor" (choose [ | ||||||
|  |                 route  "ies"       >=> Admin.listCategories | ||||||
|  |                 routef "y/%s/edit"     Admin.editCategory | ||||||
|  |             ]) | ||||||
|  |             subRoute "/page" (choose [ | ||||||
|  |                 route  "s"              >=> Admin.listPages 1 | ||||||
|  |                 routef "s/page/%i"          Admin.listPages | ||||||
|  |                 routef "/%s/edit"           Admin.editPage | ||||||
|  |                 routef "/%s/permalinks"     Admin.editPagePermalinks | ||||||
|  |             ]) | ||||||
|  |             subRoute "/post" (choose [ | ||||||
|  |                 route  "s"              >=> Post.all 1 | ||||||
|  |                 routef "s/page/%i"          Post.all | ||||||
|  |                 routef "/%s/edit"           Post.edit | ||||||
|  |                 routef "/%s/permalinks"     Post.editPermalinks | ||||||
|  |             ]) | ||||||
|  |             route    "/settings" >=> Admin.settings | ||||||
|  |             subRoute "/tag-mapping" (choose [ | ||||||
|  |                 route  "s"        >=> Admin.tagMappings | ||||||
|  |                 routef "/%s/edit"     Admin.editMapping | ||||||
|  |             ]) | ||||||
|  |             route    "/user/edit" >=> User.edit | ||||||
|  |         ] | ||||||
|  |         POST >=> choose [ | ||||||
|  |             subRoute "/category" (choose [ | ||||||
|  |                 route  "/save"      >=> Admin.saveCategory | ||||||
|  |                 routef "/%s/delete"     Admin.deleteCategory | ||||||
|  |             ]) | ||||||
|  |             subRoute "/page" (choose [ | ||||||
|  |                 route  "/save"       >=> Admin.savePage | ||||||
|  |                 route  "/permalinks" >=> Admin.savePagePermalinks | ||||||
|  |                 routef "/%s/delete"      Admin.deletePage | ||||||
|  |             ]) | ||||||
|  |             subRoute "/post" (choose [ | ||||||
|  |                 route  "/save"       >=> Post.save | ||||||
|  |                 route  "/permalinks" >=> Post.savePermalinks | ||||||
|  |                 routef "/%s/delete"      Post.delete | ||||||
|  |             ]) | ||||||
|  |             route    "/settings" >=> Admin.saveSettings | ||||||
|  |             subRoute "/tag-mapping" (choose [ | ||||||
|  |                 route  "/save"      >=> Admin.saveMapping | ||||||
|  |                 routef "/%s/delete"     Admin.deleteMapping | ||||||
|  |             ]) | ||||||
|  |             route    "/user/save" >=> User.save | ||||||
|  |         ] | ||||||
|  |     ]) | ||||||
|  |     GET >=> routexp "/category/(.*)"  Post.pageOfCategorizedPosts | ||||||
|  |     GET >=> routef  "/page/%i"        Post.pageOfPosts | ||||||
|  |     GET >=> routexp "/tag/(.*)"       Post.pageOfTaggedPosts | ||||||
|  |     subRoute "/user" (choose [ | ||||||
|  |         GET >=> choose [ | ||||||
|  |             route "/log-on"  >=> User.logOn None | ||||||
|  |             route "/log-off" >=> User.logOff | ||||||
|  |         ] | ||||||
|  |         POST >=> choose [ | ||||||
|  |             route "/log-on" >=> User.doLogOn | ||||||
|  |         ] | ||||||
|  |     ]) | ||||||
|  |     GET >=> Post.catchAll | ||||||
|  |     Error.notFound | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | /// Wrap a router in a sub-route | ||||||
|  | let routerWithPath extraPath : HttpHandler = | ||||||
|  |     subRoute extraPath router | ||||||
|  | 
 | ||||||
|  | /// Handler to apply Giraffe routing with a possible sub-route | ||||||
|  | let handleRoute : HttpHandler = fun next ctx -> task { | ||||||
|  |     let _, extraPath = WebLog.hostAndPath (webLog ctx) | ||||||
|  |     return! (if extraPath = "" then router else routerWithPath extraPath) next ctx | ||||||
|  | } | ||||||
|  | 
 | ||||||
| open Giraffe.EndpointRouting | open Giraffe.EndpointRouting | ||||||
| 
 | 
 | ||||||
| /// The endpoints defined in the above handlers | /// Endpoint-routed handler to deal with sub-routes | ||||||
| let endpoints = [ | let endpoint = [ route "{**url}" handleRoute ] | ||||||
|     GET [ |  | ||||||
|         route "/" Post.home |  | ||||||
|     ] |  | ||||||
|     subRoute "/admin" [ |  | ||||||
|         GET [ |  | ||||||
|             route    "" Admin.dashboard |  | ||||||
|             subRoute "/categor" [ |  | ||||||
|                 route  "ies"       Admin.listCategories |  | ||||||
|                 routef "y/%s/edit" Admin.editCategory |  | ||||||
|             ] |  | ||||||
|             subRoute "/page" [ |  | ||||||
|                 route  "s"              (Admin.listPages 1) |  | ||||||
|                 routef "s/page/%d"      Admin.listPages |  | ||||||
|                 routef "/%s/edit"       Admin.editPage |  | ||||||
|                 routef "/%s/permalinks" Admin.editPagePermalinks |  | ||||||
|             ] |  | ||||||
|             subRoute "/post" [ |  | ||||||
|                 route  "s"              (Post.all 1) |  | ||||||
|                 routef "s/page/%d"      Post.all |  | ||||||
|                 routef "/%s/edit"       Post.edit |  | ||||||
|                 routef "/%s/permalinks" Post.editPermalinks |  | ||||||
|             ] |  | ||||||
|             route    "/settings" Admin.settings |  | ||||||
|             subRoute "/tag-mapping" [ |  | ||||||
|                 route  "s"        Admin.tagMappings |  | ||||||
|                 routef "/%s/edit" Admin.editMapping |  | ||||||
|             ] |  | ||||||
|             route    "/user/edit" User.edit |  | ||||||
|         ] |  | ||||||
|         POST [ |  | ||||||
|             subRoute "/category" [ |  | ||||||
|                 route  "/save"      Admin.saveCategory |  | ||||||
|                 routef "/%s/delete" Admin.deleteCategory |  | ||||||
|             ] |  | ||||||
|             subRoute "/page" [ |  | ||||||
|                 route  "/save"       Admin.savePage |  | ||||||
|                 route  "/permalinks" Admin.savePagePermalinks |  | ||||||
|                 routef "/%s/delete"  Admin.deletePage |  | ||||||
|             ] |  | ||||||
|             subRoute "/post" [ |  | ||||||
|                 route  "/save"       Post.save |  | ||||||
|                 route  "/permalinks" Post.savePermalinks |  | ||||||
|                 routef "/%s/delete"  Post.delete |  | ||||||
|             ] |  | ||||||
|             route    "/settings" Admin.saveSettings |  | ||||||
|             subRoute "/tag-mapping" [ |  | ||||||
|                 route  "/save"      Admin.saveMapping |  | ||||||
|                 routef "/%s/delete" Admin.deleteMapping |  | ||||||
|             ] |  | ||||||
|             route    "/user/save" User.save |  | ||||||
|         ] |  | ||||||
|     ] |  | ||||||
|     GET [ |  | ||||||
|         route  "/category/{**slug}" Post.pageOfCategorizedPosts |  | ||||||
|         routef "/page/%d"           Post.pageOfPosts |  | ||||||
|         route  "/tag/{**slug}"      Post.pageOfTaggedPosts |  | ||||||
|     ] |  | ||||||
|     subRoute "/user" [ |  | ||||||
|         GET [ |  | ||||||
|             route "/log-on"  (User.logOn None) |  | ||||||
|             route "/log-off" User.logOff |  | ||||||
|         ] |  | ||||||
|         POST [ |  | ||||||
|             route "/log-on" User.doLogOn |  | ||||||
|         ] |  | ||||||
|     ] |  | ||||||
|     route "{**link}" Post.catchAll |  | ||||||
| ] |  | ||||||
|  | |||||||
| @ -41,7 +41,7 @@ open MyWebLog | |||||||
| // POST /user/log-on | // POST /user/log-on | ||||||
| let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { | let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { | ||||||
|     let! model  = ctx.BindFormAsync<LogOnModel> () |     let! model  = ctx.BindFormAsync<LogOnModel> () | ||||||
|     let  webLog = WebLogCache.get ctx |     let  webLog = webLog ctx | ||||||
|     match! Data.WebLogUser.findByEmail model.emailAddress webLog.id (conn ctx) with  |     match! Data.WebLogUser.findByEmail model.emailAddress webLog.id (conn ctx) with  | ||||||
|     | Some user when user.passwordHash = hashedPassword model.password user.userName user.salt -> |     | Some user when user.passwordHash = hashedPassword model.password user.userName user.salt -> | ||||||
|         let claims = seq { |         let claims = seq { | ||||||
| @ -56,7 +56,7 @@ let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { | |||||||
|             AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow)) |             AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow)) | ||||||
|         do! addMessage ctx |         do! addMessage ctx | ||||||
|                 { UserMessage.success with message = $"Logged on successfully | Welcome to {webLog.name}!" } |                 { UserMessage.success with message = $"Logged on successfully | Welcome to {webLog.name}!" } | ||||||
|         return! redirectToGet (match model.returnTo with Some url -> url | None -> "/admin") next ctx |         return! redirectToGet (defaultArg model.returnTo (WebLog.relativeUrl webLog (Permalink "admin"))) next ctx | ||||||
|     | _ -> |     | _ -> | ||||||
|         do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" } |         do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" } | ||||||
|         return! logOn model.returnTo next ctx |         return! logOn model.returnTo next ctx | ||||||
| @ -66,7 +66,7 @@ let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { | |||||||
| let logOff : HttpHandler = fun next ctx -> task { | let logOff : HttpHandler = fun next ctx -> task { | ||||||
|     do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme |     do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme | ||||||
|     do! addMessage ctx { UserMessage.info with message = "Log off successful" } |     do! addMessage ctx { UserMessage.info with message = "Log off successful" } | ||||||
|     return! redirectToGet "/" next ctx |     return! redirectToGet (WebLog.relativeUrl (webLog ctx) Permalink.empty) next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Display the user edit page, with information possibly filled in | /// Display the user edit page, with information possibly filled in | ||||||
| @ -107,7 +107,7 @@ let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | |||||||
|             do! Data.WebLogUser.update user conn |             do! Data.WebLogUser.update user conn | ||||||
|             let pwMsg = if model.newPassword = "" then "" else " and updated your password" |             let pwMsg = if model.newPassword = "" then "" else " and updated your password" | ||||||
|             do! addMessage ctx { UserMessage.success with message = $"Saved your information{pwMsg} successfully" } |             do! addMessage ctx { UserMessage.success with message = $"Saved your information{pwMsg} successfully" } | ||||||
|             return! redirectToGet "/admin/user/edit" next ctx |             return! redirectToGet (WebLog.relativeUrl (webLog ctx) (Permalink "admin/user/edit")) next ctx | ||||||
|         | None -> return! Error.notFound next ctx |         | None -> return! Error.notFound next ctx | ||||||
|     else |     else | ||||||
|         do! addMessage ctx { UserMessage.error with message = "Passwords did not match; no updates made" } |         do! addMessage ctx { UserMessage.error with message = "Passwords did not match; no updates made" } | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ | |||||||
|     <Compile Include="Handlers\Post.fs" /> |     <Compile Include="Handlers\Post.fs" /> | ||||||
|     <Compile Include="Handlers\User.fs" /> |     <Compile Include="Handlers\User.fs" /> | ||||||
|     <Compile Include="Handlers\Routes.fs" /> |     <Compile Include="Handlers\Routes.fs" /> | ||||||
|  |     <Compile Include="DotLiquidBespoke.fs" /> | ||||||
|     <Compile Include="Program.fs" /> |     <Compile Include="Program.fs" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,100 +1,23 @@ | |||||||
| open System.Collections.Generic | open Microsoft.AspNetCore.Http | ||||||
| open Microsoft.AspNetCore.Http |  | ||||||
| open Microsoft.Extensions.DependencyInjection |  | ||||||
| open MyWebLog | open MyWebLog | ||||||
| open RethinkDb.Driver.Net |  | ||||||
| open System |  | ||||||
| 
 | 
 | ||||||
| /// Middleware to derive the current web log | /// Middleware to derive the current web log | ||||||
| type WebLogMiddleware (next : RequestDelegate) = | type WebLogMiddleware (next : RequestDelegate) = | ||||||
| 
 | 
 | ||||||
|     member this.InvokeAsync (ctx : HttpContext) = task { |     member this.InvokeAsync (ctx : HttpContext) = task { | ||||||
|         match WebLogCache.exists ctx with |         if WebLogCache.exists ctx then | ||||||
|         | true -> return! next.Invoke ctx |             ctx.Items["webLog"] <- WebLogCache.get ctx | ||||||
|         | false -> |             if PageListCache.exists ctx then () else do! PageListCache.update ctx | ||||||
|             let conn = ctx.RequestServices.GetRequiredService<IConnection> () |             if CategoryCache.exists ctx then () else do! CategoryCache.update ctx | ||||||
|             match! Data.WebLog.findByHost (Cache.makeKey ctx) conn with |             return! next.Invoke ctx | ||||||
|             | Some webLog -> |         else | ||||||
|                 WebLogCache.set ctx webLog |             ctx.Response.StatusCode <- 404 | ||||||
|                 do! PageListCache.update ctx |  | ||||||
|                 do! CategoryCache.update ctx |  | ||||||
|                 return! next.Invoke ctx |  | ||||||
|             | None -> ctx.Response.StatusCode <- 404 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// DotLiquid filters | open System | ||||||
| module DotLiquidBespoke = | open Microsoft.Extensions.DependencyInjection | ||||||
|      | open RethinkDb.Driver.Net | ||||||
|     open System.IO |  | ||||||
|     open DotLiquid |  | ||||||
|     open MyWebLog.ViewModels |  | ||||||
| 
 |  | ||||||
|     /// A filter to generate a link with posts categorized under the given category |  | ||||||
|     type CategoryLinkFilter () = |  | ||||||
|         static member CategoryLink (_ : Context, catObj : obj) = |  | ||||||
|             match catObj with |  | ||||||
|             | :? DisplayCategory as cat -> $"/category/{cat.slug}/" |  | ||||||
|             | :? DropProxy as proxy -> $"""/category/{proxy["slug"]}/""" |  | ||||||
|             | _ -> $"alert('unknown category object type {catObj.GetType().Name}')" |  | ||||||
|      |  | ||||||
|     /// A filter to generate a link that will edit a page |  | ||||||
|     type EditPageLinkFilter () = |  | ||||||
|         static member EditPageLink (_ : Context, postId : string) = |  | ||||||
|             $"/admin/page/{postId}/edit" |  | ||||||
|          |  | ||||||
|     /// A filter to generate a link that will edit a post |  | ||||||
|     type EditPostLinkFilter () = |  | ||||||
|         static member EditPostLink (_ : Context, postId : string) = |  | ||||||
|             $"/admin/post/{postId}/edit" |  | ||||||
|          |  | ||||||
|     /// A filter to generate nav links, highlighting the active link (exact match) |  | ||||||
|     type NavLinkFilter () = |  | ||||||
|         static member NavLink (ctx : Context, url : string, text : string) = |  | ||||||
|             seq { |  | ||||||
|                 "<li class=\"nav-item\"><a class=\"nav-link" |  | ||||||
|                 if url = string ctx.Environments[0].["current_page"] then " active" |  | ||||||
|                 "\" href=\"/" |  | ||||||
|                 url |  | ||||||
|                 "\">" |  | ||||||
|                 text |  | ||||||
|                 "</a></li>" |  | ||||||
|             } |  | ||||||
|             |> Seq.fold (+) "" |  | ||||||
|      |  | ||||||
|     /// A filter to generate a link with posts tagged with the given tag |  | ||||||
|     type TagLinkFilter () = |  | ||||||
|         static member TagLink (ctx : Context, tag : string) = |  | ||||||
|             match ctx.Environments[0].["tag_mappings"] :?> TagMap list |  | ||||||
|                   |> List.tryFind (fun it -> it.tag = tag) with |  | ||||||
|             | Some tagMap -> $"/tag/{tagMap.urlValue}/" |  | ||||||
|             | None -> $"""/tag/{tag.Replace (" ", "+")}/""" |  | ||||||
|                  |  | ||||||
|     /// Create links for a user to log on or off, and a dashboard link if they are logged off |  | ||||||
|     type UserLinksTag () = |  | ||||||
|         inherit Tag () |  | ||||||
|          |  | ||||||
|         override this.Render (context : Context, result : TextWriter) = |  | ||||||
|             seq { |  | ||||||
|                 """<ul class="navbar-nav flex-grow-1 justify-content-end">""" |  | ||||||
|                 match Convert.ToBoolean context.Environments[0].["logged_on"] with |  | ||||||
|                 | true -> |  | ||||||
|                     """<li class="nav-item"><a class="nav-link" href="/admin">Dashboard</a></li>""" |  | ||||||
|                     """<li class="nav-item"><a class="nav-link" href="/user/log-off">Log Off</a></li>""" |  | ||||||
|                 | false -> |  | ||||||
|                     """<li class="nav-item"><a class="nav-link" href="/user/log-on">Log On</a></li>""" |  | ||||||
|                 "</ul>" |  | ||||||
|             } |  | ||||||
|             |> Seq.iter result.WriteLine |  | ||||||
|      |  | ||||||
|     /// A filter to retrieve the value of a meta item from a list |  | ||||||
|     //    (shorter than `{% assign item = list | where: "name", [name] | first %}{{ item.value }}`) |  | ||||||
|     type ValueFilter () = |  | ||||||
|         static member Value (_ : Context, items : MetaItem list, name : string) = |  | ||||||
|             match items |> List.tryFind (fun it -> it.name = name) with |  | ||||||
|             | Some item -> item.value |  | ||||||
|             | None -> $"-- {name} not found --" |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| /// Create the default information for a new web log | /// Create the default information for a new web log | ||||||
| module NewWebLog = | module NewWebLog = | ||||||
| @ -220,7 +143,9 @@ module NewWebLog = | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | open System.Collections.Generic | ||||||
| open DotLiquid | open DotLiquid | ||||||
|  | open DotLiquidBespoke | ||||||
| open Giraffe | open Giraffe | ||||||
| open Giraffe.EndpointRouting | open Giraffe.EndpointRouting | ||||||
| open Microsoft.AspNetCore.Antiforgery | open Microsoft.AspNetCore.Antiforgery | ||||||
| @ -257,6 +182,7 @@ let main args = | |||||||
|         task { |         task { | ||||||
|             let! conn = rethinkCfg.CreateConnectionAsync () |             let! conn = rethinkCfg.CreateConnectionAsync () | ||||||
|             do! Data.Startup.ensureDb rethinkCfg (loggerFac.CreateLogger (nameof Data.Startup)) conn |             do! Data.Startup.ensureDb rethinkCfg (loggerFac.CreateLogger (nameof Data.Startup)) conn | ||||||
|  |             do! WebLogCache.fill conn | ||||||
|             return conn |             return conn | ||||||
|         } |> Async.AwaitTask |> Async.RunSynchronously |         } |> Async.AwaitTask |> Async.RunSynchronously | ||||||
|     let _ = builder.Services.AddSingleton<IConnection> conn |     let _ = builder.Services.AddSingleton<IConnection> conn | ||||||
| @ -273,13 +199,12 @@ let main args = | |||||||
|     let _ = builder.Services.AddGiraffe () |     let _ = builder.Services.AddGiraffe () | ||||||
|      |      | ||||||
|     // Set up DotLiquid |     // Set up DotLiquid | ||||||
|     [ typeof<DotLiquidBespoke.CategoryLinkFilter>; typeof<DotLiquidBespoke.EditPageLinkFilter> |     [ typeof<AbsoluteLinkFilter>; typeof<CategoryLinkFilter>; typeof<EditPageLinkFilter>; typeof<EditPostLinkFilter> | ||||||
|       typeof<DotLiquidBespoke.EditPostLinkFilter>; typeof<DotLiquidBespoke.NavLinkFilter> |       typeof<NavLinkFilter>;      typeof<RelativeLinkFilter>; typeof<TagLinkFilter>;      typeof<ValueFilter> | ||||||
|       typeof<DotLiquidBespoke.TagLinkFilter>;      typeof<DotLiquidBespoke.ValueFilter> |  | ||||||
|     ] |     ] | ||||||
|     |> List.iter Template.RegisterFilter |     |> List.iter Template.RegisterFilter | ||||||
|      |      | ||||||
|     Template.RegisterTag<DotLiquidBespoke.UserLinksTag> "user_links" |     Template.RegisterTag<UserLinksTag> "user_links" | ||||||
|      |      | ||||||
|     [   // Domain types |     [   // Domain types | ||||||
|         typeof<MetaItem>; typeof<Page>; typeof<TagMap>; typeof<WebLog> |         typeof<MetaItem>; typeof<Page>; typeof<TagMap>; typeof<WebLog> | ||||||
| @ -308,7 +233,7 @@ let main args = | |||||||
|         let _ = app.UseStaticFiles () |         let _ = app.UseStaticFiles () | ||||||
|         let _ = app.UseRouting () |         let _ = app.UseRouting () | ||||||
|         let _ = app.UseSession () |         let _ = app.UseSession () | ||||||
|         let _ = app.UseGiraffe Handlers.Routes.endpoints |         let _ = app.UseGiraffe Handlers.Routes.endpoint | ||||||
| 
 | 
 | ||||||
|         app.Run() |         app.Run() | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article> | <article> | ||||||
|   <form action="/admin/category/save" method="post"> |   <form action="{{ "admin/category/save" | relative_link }}" method="post"> | ||||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> |     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||||
|     <input type="hidden" name="categoryId" value="{{ model.category_id }}"> |     <input type="hidden" name="categoryId" value="{{ model.category_id }}"> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article class="container"> | <article class="container"> | ||||||
|   <a href="/admin/category/new/edit" class="btn btn-primary btn-sm mb-3">Add a New Category</a> |   <a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Add a New Category</a> | ||||||
|   <table class="table table-sm table-hover"> |   <table class="table table-sm table-hover"> | ||||||
|     <thead> |     <thead> | ||||||
|       <tr> |       <tr> | ||||||
| @ -17,16 +17,19 @@ | |||||||
|             {%- endif %} |             {%- endif %} | ||||||
|             {{ cat.name }}<br> |             {{ cat.name }}<br> | ||||||
|             <small> |             <small> | ||||||
|             {%- if cat.post_count > 0 %} |               {%- if cat.post_count > 0 %} | ||||||
|               <a href="{{ cat | category_link }}" target="_blank"> |                 <a href="{{ cat | category_link }}" target="_blank"> | ||||||
|                 View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%} |                   View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%} | ||||||
|               </a> |                 </a> | ||||||
|  |                 <span class="text-muted"> • </span> | ||||||
|  |               {%- endif %} | ||||||
|  |               {%- capture cat_edit %}admin/category/{{ cat.id }}/edit{% endcapture -%} | ||||||
|  |               <a href="{{ cat_edit | relative_link }}">Edit</a> | ||||||
|               <span class="text-muted"> • </span> |               <span class="text-muted"> • </span> | ||||||
|             {%- endif %} |               {%- capture cat_del %}admin/category/{{ cat.id }}/delete{% endcapture -%} | ||||||
|               <a href="/admin/category/{{ cat.id }}/edit">Edit</a> |               {%- capture cat_del_link %}{{ cat_del | relative_link }}{% endcapture -%} | ||||||
|               <span class="text-muted"> • </span> |               <a href="{{ cat_del_link }}" class="text-danger" | ||||||
|               <a href="/admin/category/{{ cat.id }}/delete" class="text-danger" |                  onclick="return Admin.deleteCategory('{{ cat.name }}', '{{ cat_del_link }}')"> | ||||||
|                  onclick="return Admin.deleteCategory('{{ cat.id }}', '{{ cat.name }}')"> |  | ||||||
|                 Delete |                 Delete | ||||||
|               </a> |               </a> | ||||||
|             </small> |             </small> | ||||||
|  | |||||||
| @ -9,8 +9,8 @@ | |||||||
|             Published <span class="badge rounded-pill bg-secondary">{{ model.posts }}</span> |             Published <span class="badge rounded-pill bg-secondary">{{ model.posts }}</span> | ||||||
|               Drafts <span class="badge rounded-pill bg-secondary">{{ model.drafts }}</span> |               Drafts <span class="badge rounded-pill bg-secondary">{{ model.drafts }}</span> | ||||||
|           </h6> |           </h6> | ||||||
|           <a href="/admin/posts" class="btn btn-secondary me-2">View All</a> |           <a href="{{ "admin/posts" | relative_permalink }}" class="btn btn-secondary me-2">View All</a> | ||||||
|           <a href="/admin/post/new/edit" class="btn btn-primary">Write a New Post</a> |           <a href="{{ "admin/post/new/edit" | relative_permalink }}" class="btn btn-primary">Write a New Post</a> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </section> |     </section> | ||||||
| @ -22,8 +22,8 @@ | |||||||
|             All <span class="badge rounded-pill bg-secondary">{{ model.pages }}</span> |             All <span class="badge rounded-pill bg-secondary">{{ model.pages }}</span> | ||||||
|               Shown in Page List <span class="badge rounded-pill bg-secondary">{{ model.listed_pages }}</span> |               Shown in Page List <span class="badge rounded-pill bg-secondary">{{ model.listed_pages }}</span> | ||||||
|           </h6> |           </h6> | ||||||
|           <a href="/admin/pages" class="btn btn-secondary me-2">View All</a> |           <a href="{{ "admin/pages" | relative_link }}" class="btn btn-secondary me-2">View All</a> | ||||||
|           <a href="/admin/page/new/edit" class="btn btn-primary">Create a New Page</a> |           <a href="{{ "admin/page/new/edit" | relative_link }}" class="btn btn-primary">Create a New Page</a> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </section> |     </section> | ||||||
| @ -37,15 +37,15 @@ | |||||||
|             All <span class="badge rounded-pill bg-secondary">{{ model.categories }}</span> |             All <span class="badge rounded-pill bg-secondary">{{ model.categories }}</span> | ||||||
|               Top Level <span class="badge rounded-pill bg-secondary">{{ model.top_level_categories }}</span> |               Top Level <span class="badge rounded-pill bg-secondary">{{ model.top_level_categories }}</span> | ||||||
|           </h6> |           </h6> | ||||||
|           <a href="/admin/categories" class="btn btn-secondary me-2">View All</a> |           <a href="{{ "admin/categories" | relative_link }}" class="btn btn-secondary me-2">View All</a> | ||||||
|           <a href="/admin/category/new/edit" class="btn btn-secondary">Add a New Category</a> |           <a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-secondary">Add a New Category</a> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </section> |     </section> | ||||||
|   </div> |   </div> | ||||||
|   <div class="row pb-3"> |   <div class="row pb-3"> | ||||||
|     <div class="col text-end"> |     <div class="col text-end"> | ||||||
|       <a href="/admin/settings" class="btn btn-secondary">Modify Settings</a> |       <a href="{{ "admin/settings" | relative_link }}" class="btn btn-secondary">Modify Settings</a> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </article> | </article> | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ | |||||||
|     <header> |     <header> | ||||||
|       <nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100"> |       <nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100"> | ||||||
|         <div class="container-fluid"> |         <div class="container-fluid"> | ||||||
|           <a class="navbar-brand" href="/">{{ web_log.name }}</a> |           <a class="navbar-brand" href="{{ "" | relative_link }}">{{ web_log.name }}</a> | ||||||
|           <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" |           <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" | ||||||
|                   aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> |                   aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> | ||||||
|             <span class="navbar-toggler-icon"></span> |             <span class="navbar-toggler-icon"></span> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">Log On to {{ web_log.name }}</h2> | <h2 class="my-3">Log On to {{ web_log.name }}</h2> | ||||||
| <article class="py-3"> | <article class="py-3"> | ||||||
|   <form action="/user/log-on" method="post"> |   <form action="{{ "user/log-on" | 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 }}"> | ||||||
|     {% if model.return_to %} |     {% if model.return_to %} | ||||||
|       <input type="hidden" name="returnTo" value="{{ model.return_to.value }}"> |       <input type="hidden" name="returnTo" value="{{ model.return_to.value }}"> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article> | <article> | ||||||
|   <form action="/admin/page/save" method="post"> |   <form action="{{ "admin/page/save" | relative_link }}" method="post"> | ||||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> |     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||||
|     <input type="hidden" name="pageId" value="{{ model.page_id }}"> |     <input type="hidden" name="pageId" value="{{ model.page_id }}"> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
| @ -16,7 +16,8 @@ | |||||||
|                    value="{{ model.permalink }}"> |                    value="{{ model.permalink }}"> | ||||||
|             <label for="permalink">Permalink</label> |             <label for="permalink">Permalink</label> | ||||||
|             {%- if model.page_id != "new" %} |             {%- if model.page_id != "new" %} | ||||||
|               <span class="form-text"><a href="/admin/page/{{ model.page_id }}/permalinks">Manage Permalinks</a></span> |               {%- capture perm_edit %}admin/page/{{ model.page_id }}/permalinks{% endcapture -%} | ||||||
|  |               <span class="form-text"><a href="{{ perm_edit | relative_link }}">Manage Permalinks</a></span> | ||||||
|             {% endif -%} |             {% endif -%} | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article class="container"> | <article class="container"> | ||||||
|   <a href="/admin/page/new/edit" class="btn btn-primary btn-sm mb-3">Create a New Page</a> |   <a href="{{ "admin/page/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Create a New Page</a> | ||||||
|   <table class="table table-sm table-hover"> |   <table class="table table-sm table-hover"> | ||||||
|     <thead> |     <thead> | ||||||
|       <tr> |       <tr> | ||||||
| @ -17,12 +17,15 @@ | |||||||
|             {%- if pg.is_default %}   <span class="badge bg-success">HOME PAGE</span>{% endif -%} |             {%- if pg.is_default %}   <span class="badge bg-success">HOME PAGE</span>{% endif -%} | ||||||
|             {%- if pg.show_in_page_list %}   <span class="badge bg-primary">IN PAGE LIST</span> {% endif -%}<br> |             {%- if pg.show_in_page_list %}   <span class="badge bg-primary">IN PAGE LIST</span> {% endif -%}<br> | ||||||
|             <small> |             <small> | ||||||
|               <a href="/{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}" target="_blank">View Page</a> |               {%- capture pg_link %}{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}{% endcapture -%} | ||||||
|  |               <a href="{{ pg_link | relative_link }}" target="_blank">View Page</a> | ||||||
|               <span class="text-muted"> • </span> |               <span class="text-muted"> • </span> | ||||||
|               <a href="{{ pg.id | edit_page_link }}">Edit</a> |               <a href="{{ pg | edit_page_link }}">Edit</a> | ||||||
|               <span class="text-muted"> • </span> |               <span class="text-muted"> • </span> | ||||||
|               <a href="/admin/page/{{ pg.id }}/delete" class="text-danger" |               {%- capture pg_del %}admin/page/{{ pg.id }}/delete{% endcapture -%} | ||||||
|                  onclick="return Admin.deletePage('{{ pg.id }}', '{{ pg.title }}')"> |               {%- capture pg_del_link %}{{ pg_del | relative_link }}{% endcapture -%} | ||||||
|  |               <a href="{{ pg_del_link }}" class="text-danger" | ||||||
|  |                  onclick="return Admin.deletePage('{{ pg.title }}', '{{ pg_del_link }}')"> | ||||||
|                 Delete |                 Delete | ||||||
|               </a> |               </a> | ||||||
|             </small> |             </small> | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article> | <article> | ||||||
|   <form action="/admin/{{ model.entity }}/permalinks" method="post"> |   {%- capture form_action %}admin/{{ model.entity }}/permalinks{% endcapture -%} | ||||||
|  |   <form action="{{ form_action | relative_link }}" method="post"> | ||||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> |     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||||
|     <input type="hidden" name="id" value="{{ model.id }}"> |     <input type="hidden" name="id" value="{{ model.id }}"> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
| @ -10,7 +11,8 @@ | |||||||
|             <strong>{{ model.current_title }}</strong><br> |             <strong>{{ model.current_title }}</strong><br> | ||||||
|             <small class="text-muted"> |             <small class="text-muted"> | ||||||
|               <span class="fst-italic">{{ model.current_permalink }}</span><br> |               <span class="fst-italic">{{ model.current_permalink }}</span><br> | ||||||
|               <a href="/admin/{{ model.entity }}/{{ model.id }}/edit">« Back to Edit {{ model.entity | capitalize }}</a> |               {%- capture back_link %}admin/{{ model.entity }}/{{ model.id }}/edit{% endcapture -%} | ||||||
|  |               <a href="{{ back_link | relative_link }}">« Back to Edit {{ model.entity | capitalize }}</a> | ||||||
|             </small> |             </small> | ||||||
|           </p> |           </p> | ||||||
|         </div> |         </div> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article> | <article> | ||||||
|   <form action="/admin/post/save" method="post"> |   <form action="{{ "/admin/post/save" | relative_link }}" method="post"> | ||||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> |     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||||
|     <input type="hidden" name="postId" value="{{ model.post_id }}"> |     <input type="hidden" name="postId" value="{{ model.post_id }}"> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
| @ -16,7 +16,8 @@ | |||||||
|                    value="{{ model.permalink }}"> |                    value="{{ model.permalink }}"> | ||||||
|             <label for="permalink">Permalink</label> |             <label for="permalink">Permalink</label> | ||||||
|             {%- if model.page_id != "new" %} |             {%- if model.page_id != "new" %} | ||||||
|               <span class="form-text"><a href="/admin/post/{{ model.post_id }}/permalinks">Manage Permalinks</a></span> |               {%- capture perm_edit %}admin/post/{{ model.post_id }}/permalinks{% endcapture -%} | ||||||
|  |               <span class="form-text"><a href="{{ perm_edit | relative_link }}">Manage Permalinks</a></span> | ||||||
|             {% endif -%} |             {% endif -%} | ||||||
|           </div> |           </div> | ||||||
|           <div class="mb-2"> |           <div class="mb-2"> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article class="container"> | <article class="container"> | ||||||
|   <a href="/admin/post/new/edit" class="btn btn-primary btn-sm mb-3">Write a New Post</a> |   <a href="{{ "/admin/post/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Write a New Post</a> | ||||||
|   <table class="table table-sm table-hover"> |   <table class="table table-sm table-hover"> | ||||||
|     <thead> |     <thead> | ||||||
|       <tr> |       <tr> | ||||||
| @ -23,12 +23,14 @@ | |||||||
|           <td> |           <td> | ||||||
|             {{ post.title }}<br> |             {{ post.title }}<br> | ||||||
|             <small> |             <small> | ||||||
|               <a href="/{{ post.permalink }}" target="_blank">View Post</a> |               <a href="{{ post | relative_link }}" target="_blank">View Post</a> | ||||||
|               <span class="text-muted"> • </span> |               <span class="text-muted"> • </span> | ||||||
|               <a href="{{ post.id | edit_post_link }}">Edit</a> |               <a href="{{ post | edit_post_link }}">Edit</a> | ||||||
|               <span class="text-muted"> • </span> |               <span class="text-muted"> • </span> | ||||||
|               <a href="/admin/post/{{ pg.id }}/delete" class="text-danger" |               {%- capture post_del %}admin/post/{{ pg.id }}/delete{% endcapture -%} | ||||||
|                  onclick="return Admin.deletePost('{{ post.id }}', '{{ post.title }}')"> |               {%- capture post_del_link %}{{ post_del | relative_link }}{% endcapture -%} | ||||||
|  |               <a href="{{ post_del_link }}" class="text-danger" | ||||||
|  |                  onclick="return Admin.deletePost('{{ post.title }}', '{{ post_del_link }}')"> | ||||||
|                 Delete |                 Delete | ||||||
|               </a> |               </a> | ||||||
|             </small> |             </small> | ||||||
| @ -44,12 +46,12 @@ | |||||||
|     <div class="d-flex justify-content-evenly"> |     <div class="d-flex justify-content-evenly"> | ||||||
|       <div> |       <div> | ||||||
|         {% if model.newer_link %} |         {% if model.newer_link %} | ||||||
|           <p><a class="btn btn-default" href="/{{ model.newer_link.value }}">« Newer Posts</a></p> |           <p><a class="btn btn-default" href="{{ model.newer_link.value }}">« Newer Posts</a></p> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|       </div> |       </div> | ||||||
|       <div class="text-right"> |       <div class="text-right"> | ||||||
|         {% if model.older_link %} |         {% if model.older_link %} | ||||||
|           <p><a class="btn btn-default" href="/{{ model.older_link.value }}">Older Posts »</a></p> |           <p><a class="btn btn-default" href="{{ model.older_link.value }}">Older Posts »</a></p> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">{{ web_log.name }} Settings</h2> | <h2 class="my-3">{{ web_log.name }} Settings</h2> | ||||||
| <article> | <article> | ||||||
|   <form action="/admin/settings" 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"> | ||||||
|       <div class="row"> |       <div class="row"> | ||||||
|  | |||||||
| @ -1,12 +1,12 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article> | <article> | ||||||
|   <form action="/admin/tag-mapping/save" method="post"> |   <form action="{{ "admin/tag-mapping/save" | relative_link }}" method="post"> | ||||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> |     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||||
|     <input type="hidden" name="id" value="{{ model.id }}"> |     <input type="hidden" name="id" value="{{ model.id }}"> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
|       <div class="row mb-3"> |       <div class="row mb-3"> | ||||||
|         <div class="col"> |         <div class="col"> | ||||||
|           <a href="/admin/tag-mappings">« Back to Tag Mappings</a> |           <a href="{{ "admin/tag-mappings" | relative_link }}">« Back to Tag Mappings</a> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <div class="row mb-3"> |       <div class="row mb-3"> | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article class="container"> | <article class="container"> | ||||||
|   <a href="/admin/tag-mapping/new/edit" class="btn btn-primary btn-sm mb-3">Add a New Tag Mapping</a> |   <a href="{{ "admin/tag-mapping/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"> | ||||||
|  |     Add a New Tag Mapping | ||||||
|  |   </a> | ||||||
|   <table class="table table-sm table-hover"> |   <table class="table table-sm table-hover"> | ||||||
|     <thead> |     <thead> | ||||||
|       <tr> |       <tr> | ||||||
| @ -15,8 +17,13 @@ | |||||||
|           <td class="no-wrap"> |           <td class="no-wrap"> | ||||||
|             {{ map.tag }}<br> |             {{ map.tag }}<br> | ||||||
|             <small> |             <small> | ||||||
|               <a href="/admin/tag-mapping/{{ map_id }}/delete" class="text-danger" |               {%- capture map_edit %}admin/tag-mapping/{{ map_id }}/edit{% endcapture -%} | ||||||
|                  onclick="return Admin.deleteTagMapping('{{ map_id }}', '{{ map.tag }}')"> |               <a href="{{ map_edit | relative_link }}">Edit</a> | ||||||
|  |               <span class="text-muted"> • </span> | ||||||
|  |               {%- capture map_del %}admin/tag-mapping/{{ map_id }}/delete{% endcapture -%} | ||||||
|  |               {%- capture map_del_link %}{{ map_del | relative_link }}{% endcapture -%} | ||||||
|  |               <a href="{{ map_del_link }}" class="text-danger" | ||||||
|  |                  onclick="return Admin.deleteTagMapping('{{ map.tag }}', '{{ map_del_link }}')"> | ||||||
|                 Delete |                 Delete | ||||||
|               </a> |               </a> | ||||||
|             </small> |             </small> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <h2 class="my-3">{{ page_title }}</h2> | <h2 class="my-3">{{ page_title }}</h2> | ||||||
| <article> | <article> | ||||||
|   <form action="/admin/user/save" method="post"> |   <form action="{{ "admin/user/save" | relative_link }}" method="post"> | ||||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> |     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
|       <div class="row mb-3"> |       <div class="row mb-3"> | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|   <article class="content auto"> |   <article class="content auto"> | ||||||
|     {{ page.text }} |     {{ page.text }} | ||||||
|     {% if logged_on -%} |     {% if logged_on -%} | ||||||
|       <p><small><a href="{{ page.id | edit_page_link }}">Edit This Page</a></small></p> |       <p><small><a href="{{ page | edit_page_link }}">Edit This Page</a></small></p> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|   </article> |   </article> | ||||||
|   <aside class="app-sidebar"> |   <aside class="app-sidebar"> | ||||||
| @ -11,7 +11,10 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>PrayerTracker</strong><br> |           <strong>PrayerTracker</strong><br> | ||||||
|           <a href="/solutions/prayer-tracker" title="About PrayerTracker • Bit Badger Solutions">About</a> • |           <a href="{{ "solutions/prayer-tracker" | relative_link }}" | ||||||
|  |              title="About PrayerTracker • Bit Badger Solutions"> | ||||||
|  |             About | ||||||
|  |           </a> • | ||||||
|           <a href="https://prayer.bitbadger.solutions" title="PrayerTracker" target="_blank" rel="noopener">Visit</a> |           <a href="https://prayer.bitbadger.solutions" title="PrayerTracker" target="_blank" rel="noopener">Visit</a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description"> |         <p class="app-sidebar-description"> | ||||||
| @ -21,7 +24,10 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>myPrayerJournal</strong><br> |           <strong>myPrayerJournal</strong><br> | ||||||
|           <a href="/solutions/my-prayer-journal" title="About myPrayerJournal • Bit Badger Solutions">About</a> • |           <a href="{{ "solutions/my-prayer-journal" | relative_link }}" | ||||||
|  |              title="About myPrayerJournal • Bit Badger Solutions"> | ||||||
|  |             About | ||||||
|  |           </a> • | ||||||
|           <a href="https://prayerjournal.me" title="myPrayerJournal" target="_blank" rel="noopener">Visit</a> |           <a href="https://prayerjournal.me" title="myPrayerJournal" target="_blank" rel="noopener">Visit</a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description">Minimalist personal prayer journal</p> |         <p class="app-sidebar-description">Minimalist personal prayer journal</p> | ||||||
| @ -29,7 +35,9 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>Linux Resources</strong><br> |           <strong>Linux Resources</strong><br> | ||||||
|           <a href="https://blog.bitbadger.solutions/linux/" title="Linux Resources" target="_blank" rel="noopener">Visit</a> |           <a href="https://blog.bitbadger.solutions/linux/" title="Linux Resources" target="_blank" rel="noopener"> | ||||||
|  |             Visit | ||||||
|  |           </a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description">Handy information for Linux folks</p> |         <p class="app-sidebar-description">Handy information for Linux folks</p> | ||||||
|       </div> |       </div> | ||||||
| @ -39,7 +47,10 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>Futility Closet</strong><br> |           <strong>Futility Closet</strong><br> | ||||||
|           <a href="/solutions/futility-closet" title="About Futility Closet • Bit Badger Solutions">About</a> • |           <a href="{{ "solutions/futility-closet" | relative_link }}" | ||||||
|  |              title="About Futility Closet • Bit Badger Solutions"> | ||||||
|  |             About | ||||||
|  |           </a> • | ||||||
|           <a href="https://www.futilitycloset.com" title="Futility Closet" target="_blank" rel="noopener">Visit</a> |           <a href="https://www.futilitycloset.com" title="Futility Closet" target="_blank" rel="noopener">Visit</a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description">An idler’s miscellany of compendious amusements</p> |         <p class="app-sidebar-description">An idler’s miscellany of compendious amusements</p> | ||||||
| @ -47,7 +58,10 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>Mindy Mackenzie</strong><br> |           <strong>Mindy Mackenzie</strong><br> | ||||||
|           <a href="/solutions/mindy-mackenzie" title="About Mindy Mackenzie • Bit Badger Solutions">About</a> • |           <a href="{{ "solutions/mindy-mackenzie" | relative_link }}" | ||||||
|  |              title="About Mindy Mackenzie • Bit Badger Solutions"> | ||||||
|  |             About | ||||||
|  |           </a> • | ||||||
|           <a href="https://mindymackenzie.com" title="Mindy Mackenzie" target="_blank" rel="noopener">Visit</a> |           <a href="https://mindymackenzie.com" title="Mindy Mackenzie" target="_blank" rel="noopener">Visit</a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description"><em>WSJ</em>-best-selling author of <em>The Courage Solution</em></p> |         <p class="app-sidebar-description"><em>WSJ</em>-best-selling author of <em>The Courage Solution</em></p> | ||||||
| @ -55,7 +69,10 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>Riehl World News</strong><br> |           <strong>Riehl World News</strong><br> | ||||||
|           <a href="/solutions/riehl-world-news" title="About Riehl World News • Bit Badger Solutions">About</a> • |           <a href="{{ "solutions/riehl-world-news" | relative_link }}" | ||||||
|  |              title="About Riehl World News • Bit Badger Solutions"> | ||||||
|  |             About | ||||||
|  |           </a> • | ||||||
|           <a href="http://riehlworldview.com" title="Riehl World News" target="_blank" rel="noopener">Visit</a> |           <a href="http://riehlworldview.com" title="Riehl World News" target="_blank" rel="noopener">Visit</a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description">Riehl news for real people</p> |         <p class="app-sidebar-description">Riehl news for real people</p> | ||||||
| @ -66,7 +83,10 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>Bay Vista Baptist Church</strong><br> |           <strong>Bay Vista Baptist Church</strong><br> | ||||||
|           <a href="/solutions/bay-vista" title="About Bay Vista Baptist Church • Bit Badger Solutions">About</a> • |           <a href="{{ "solutions/bay-vista" | relative_link }}" | ||||||
|  |              title="About Bay Vista Baptist Church • Bit Badger Solutions"> | ||||||
|  |             About | ||||||
|  |           </a> • | ||||||
|           <a href="https://bayvista.org" title="Bay Vista Baptist Church" target="_blank" rel="noopener">Visit</a> |           <a href="https://bayvista.org" title="Bay Vista Baptist Church" target="_blank" rel="noopener">Visit</a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description">Biloxi, Mississippi</p> |         <p class="app-sidebar-description">Biloxi, Mississippi</p> | ||||||
| @ -74,8 +94,13 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>The Bit Badger Blog</strong><br> |           <strong>The Bit Badger Blog</strong><br> | ||||||
|           <a href="/solutions/tech-blog" title="About The Bit Badger Blog • Bit Badger Solutions">About</a> • |           <a href="{{ "solutions/tech-blog" | relative_link }}" | ||||||
|           <a href="https://blog.bitbadger.solutions" title="The Bit Badger Blog" target="_blank" rel="noopener">Visit</a> |              title="About The Bit Badger Blog • Bit Badger Solutions"> | ||||||
|  |             About | ||||||
|  |           </a> • | ||||||
|  |           <a href="https://blog.bitbadger.solutions" title="The Bit Badger Blog" target="_blank" rel="noopener"> | ||||||
|  |             Visit | ||||||
|  |           </a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description">Technical information (“geek stuff”) from Bit Badger Solutions</p> |         <p class="app-sidebar-description">Technical information (“geek stuff”) from Bit Badger Solutions</p> | ||||||
|       </div> |       </div> | ||||||
| @ -92,7 +117,9 @@ | |||||||
|       <div> |       <div> | ||||||
|         <p class="app-sidebar-name"> |         <p class="app-sidebar-name"> | ||||||
|           <strong>A Word from the Word</strong><br> |           <strong>A Word from the Word</strong><br> | ||||||
|           <a href="https://devotions.summershome.org" title="A Word from the Word" target="_blank" rel="noopener">Visit</a> |           <a href="https://devotions.summershome.org" title="A Word from the Word" target="_blank" rel="noopener"> | ||||||
|  |             Visit | ||||||
|  |           </a> | ||||||
|         </p> |         </p> | ||||||
|         <p class="app-sidebar-description">Devotions by Daniel</p> |         <p class="app-sidebar-description">Devotions by Daniel</p> | ||||||
|       </div> |       </div> | ||||||
|  | |||||||
| @ -11,13 +11,13 @@ | |||||||
|   <body> |   <body> | ||||||
|     <header class="site-header"> |     <header class="site-header"> | ||||||
|       <div class="header-logo"> |       <div class="header-logo"> | ||||||
|         <a href="/"> |         <a href="{{ "" | relative_link }}"> | ||||||
|           <img src="/themes/{{ web_log.theme_path }}/bitbadger.png" |           <img src="/themes/{{ web_log.theme_path }}/bitbadger.png" | ||||||
|                alt="A cartoon badger looking at a computer screen, with his paw on a mouse" |                alt="A cartoon badger looking at a computer screen, with his paw on a mouse" | ||||||
|                title="Bit Badger Solutions"> |                title="Bit Badger Solutions"> | ||||||
|         </a> |         </a> | ||||||
|       </div> |       </div> | ||||||
|       <div class="header-title"><a href="/">Bit Badger Solutions</a></div> |       <div class="header-title"><a href="{{ "" | relative_link }}">Bit Badger Solutions</a></div> | ||||||
|       <div class="header-spacer">   </div> |       <div class="header-spacer">   </div> | ||||||
|       <div class="header-social"> |       <div class="header-social"> | ||||||
|         <a href="https://twitter.com/Bit_Badger" title="Bit_Badger on Twitter" target="_blank"> |         <a href="https://twitter.com/Bit_Badger" title="Bit_Badger on Twitter" target="_blank"> | ||||||
| @ -33,14 +33,15 @@ | |||||||
|       <div> |       <div> | ||||||
|         <small> |         <small> | ||||||
|           {% if logged_on -%} |           {% if logged_on -%} | ||||||
|             <a href="/admin">Dashboard</a> ~ <a href="/user/log-off">Log Off</a> |             <a href="{{ "admin" | relative_link }}">Dashboard</a> ~ | ||||||
|  |             <a href="{{ "user/log-off" | relative_link }}">Log Off</a> | ||||||
|           {% else %} |           {% else %} | ||||||
|             <a href="/user/log-on">Log On</a> |             <a href="{{ "user/log-on" | relative_link }}">Log On</a> | ||||||
|           {% endif %} |           {% endif %} | ||||||
|         </small> |         </small> | ||||||
|       </div> |       </div> | ||||||
|       <div class="footer-by"> |       <div class="footer-by"> | ||||||
|         A <strong><a href="/">Bit Badger Solutions</a></strong> original design |         A <strong><a href="{{ "" | relative_link }}">Bit Badger Solutions</a></strong> original design | ||||||
|       </div> |       </div> | ||||||
|     </footer> |     </footer> | ||||||
|   </body> |   </body> | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| <article class="content auto"> | <article class="content auto"> | ||||||
|   <h1>{{ page.title }}</h1> |   <h1>{{ page.title }}</h1> | ||||||
|   {{ page.text }} |   {{ page.text }} | ||||||
|   <p><br><a href="/" title="Home">« Home</a></p> |   <p><br><a href="{{ "" | relative_link }}" title="Home">« Home</a></p> | ||||||
|   {% if logged_on -%} |   {% if logged_on -%} | ||||||
|     <p><small><a href="{{ page.id | edit_page_link }}">Edit This Page</a></small></p> |     <p><small><a href="{{ page | edit_page_link }}">Edit This Page</a></small></p> | ||||||
|   {% endif %} |   {% endif %} | ||||||
| </article> | </article> | ||||||
|  | |||||||
| @ -92,9 +92,9 @@ | |||||||
|         </div> |         </div> | ||||||
|       </section> |       </section> | ||||||
|     {%- endif %} |     {%- endif %} | ||||||
|     <p><br><a href="/solutions">« Back to All Solutions</a></p> |     <p><br><a href="{{ "solutions" | relative_link }}">« Back to All Solutions</a></p> | ||||||
|     {% if logged_on -%} |     {% if logged_on -%} | ||||||
|       <p><small><a href="{{ page.id | edit_page_link }}">Edit This Page</a></small></p> |       <p><small><a href="{{ page | edit_page_link }}">Edit This Page</a></small></p> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|   </article> |   </article> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -8,7 +8,8 @@ | |||||||
|   {%- for post in model.posts %} |   {%- for post in model.posts %} | ||||||
|     <article class="item"> |     <article class="item"> | ||||||
|       <h1 class="item-heading"> |       <h1 class="item-heading"> | ||||||
|         <a href="/{{ post.permalink }}" title="Permanent Link to "{{ post.title | strip_html | escape }}""> |         <a href="{{ post | relative_link }}" | ||||||
|  |            title="Permanent Link to "{{ post.title | strip_html | escape }}""> | ||||||
|           {{ post.title }} |           {{ post.title }} | ||||||
|         </a> |         </a> | ||||||
|       </h1> |       </h1> | ||||||
| @ -24,7 +25,7 @@ | |||||||
|         </span> |         </span> | ||||||
|         {% if logged_on %} |         {% if logged_on %} | ||||||
|           <span> |           <span> | ||||||
|             <a href="{{ post.id | edit_post_link }}"><i class="fa fa-pencil-square-o"></i> Edit Post</a> |             <a href="{{ post | edit_post_link }}"><i class="fa fa-pencil-square-o"></i> Edit Post</a> | ||||||
|           </span> |           </span> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|       </h4> |       </h4> | ||||||
| @ -34,12 +35,12 @@ | |||||||
|   <nav aria-label="pagination"> |   <nav aria-label="pagination"> | ||||||
|     <ul class="pager"> |     <ul class="pager"> | ||||||
|       {% if model.newer_link -%} |       {% if model.newer_link -%} | ||||||
|         <li class="previous item"><a href="/{{ model.newer_link.value }}">« Newer Posts</a></li> |         <li class="previous item"><a href="{{ model.newer_link.value }}">« Newer Posts</a></li> | ||||||
|       {%- else -%} |       {%- else -%} | ||||||
|         <li></li> |         <li></li> | ||||||
|       {% endif %} |       {% endif %} | ||||||
|       {% if model.older_link -%} |       {% if model.older_link -%} | ||||||
|         <li class="next item"><a href="/{{ model.older_link.value }}">Older Posts »</a></li> |         <li class="next item"><a href="{{ model.older_link.value }}">Older Posts »</a></li> | ||||||
|       {%- endif -%} |       {%- endif -%} | ||||||
|     </ul> |     </ul> | ||||||
|   </nav> |   </nav> | ||||||
| @ -74,7 +75,7 @@ | |||||||
|       {% for cat in categories -%} |       {% for cat in categories -%} | ||||||
|         {%- assign indent = cat.parent_names | size -%} |         {%- assign indent = cat.parent_names | size -%} | ||||||
|         <li class="cat-list-item"{% if indent > 0 %} style="padding-left:{{ indent }}rem;"{% endif %}> |         <li class="cat-list-item"{% if indent > 0 %} style="padding-left:{{ indent }}rem;"{% endif %}> | ||||||
|           <a href="/category/{{ cat.slug }}/" class="cat-list-link">{{ cat.name }}</a> |           <a href="{{ cat | category_link }}" class="cat-list-link">{{ cat.name }}</a> | ||||||
|           <span class="cat-list-count">{{ cat.post_count }}</span> |           <span class="cat-list-count">{{ cat.post_count }}</span> | ||||||
|         </li> |         </li> | ||||||
|       {%- endfor %} |       {%- endfor %} | ||||||
|  | |||||||
| @ -18,17 +18,19 @@ | |||||||
|     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"> |     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"> | ||||||
|     <link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css"> |     <link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css"> | ||||||
|     {%- if is_home %} |     {%- if is_home %} | ||||||
|       <link rel="alternate" type="application/rss+xml" title="{{ web_log.name | escape }}" href="/feed.xml"> |       <link rel="alternate" type="application/rss+xml" title="{{ web_log.name | escape }}" | ||||||
|  |             href="{{ "feed.xml" | relative_link }}"> | ||||||
|  |       <link rel="canonical" href="{{ "" | absolute_url }}"> | ||||||
|     {%- endif %} |     {%- endif %} | ||||||
|     <script src="/themes/{{ web_log.theme_path }}/djs.js"></script> |     <script src="/themes/{{ web_log.theme_path }}/djs.js"></script> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <nav class="site-header" role="navigation"> |     <nav class="site-header" role="navigation"> | ||||||
|       <p><a class="nav-home" href="/">{{ web_log.name }}</a></p> |       <p><a class="nav-home" href="{{ "" | relative_link }}">{{ web_log.name }}</a></p> | ||||||
|       {%- if web_log.subtitle %}<p>{{ web_log.subtitle.value }}</p>{% endif -%} |       {%- if web_log.subtitle %}<p>{{ web_log.subtitle.value }}</p>{% endif -%} | ||||||
|       <p class="nav-spacer"></p> |       <p class="nav-spacer"></p> | ||||||
|       {%- for page in page_list %} |       {%- for page in page_list %} | ||||||
|         <p class="desktop"><a href="/{{ page.permalink }}">{{ page.title }}</a></p> |         <p class="desktop"><a href="{{ page | relative_link }}">{{ page.title }}</a></p> | ||||||
|       {%- endfor %} |       {%- endfor %} | ||||||
|       <p class="desktop"> |       <p class="desktop"> | ||||||
|         <a href="https://devotions.summershome.org" target="_blank" rel="noopener">A Word from the Word</a> |         <a href="https://devotions.summershome.org" target="_blank" rel="noopener">A Word from the Word</a> | ||||||
| @ -38,7 +40,7 @@ | |||||||
|     {{ content }} |     {{ content }} | ||||||
|     <footer class="part-1" id="links"> |     <footer class="part-1" id="links"> | ||||||
|       {%- for page in page_list %} |       {%- for page in page_list %} | ||||||
|         <p class="mobile"><a href="{{ page.permalink }}">{{ page.title }}</a></p> |         <p class="mobile"><a href="{{ page | relative_link }}">{{ page.title }}</a></p> | ||||||
|       {%- endfor %} |       {%- endfor %} | ||||||
|       <p class="mobile"> |       <p class="mobile"> | ||||||
|         <a href="https://devotions.summershome.org" target="_blank" rel="noopener">A Word from the Word</a> |         <a href="https://devotions.summershome.org" target="_blank" rel="noopener">A Word from the Word</a> | ||||||
| @ -143,9 +145,9 @@ | |||||||
|         </a> |         </a> | ||||||
|         • Powered by <a href="https://github.com/bit-badger/myWebLog/tree/v2">myWebLog</a> • |         • Powered by <a href="https://github.com/bit-badger/myWebLog/tree/v2">myWebLog</a> • | ||||||
|         {% if logged_on %} |         {% if logged_on %} | ||||||
|           <a href="/admin">Dashboard</a> |           <a href="{{ "admin" | relative_link }}">Dashboard</a> | ||||||
|         {% else %} |         {% else %} | ||||||
|           <a href="/user/log-on">Log On</a> |           <a href="{{ "user/log-on" | relative_link }}">Log On</a> | ||||||
|         {%- endif %} |         {%- endif %} | ||||||
|       </div> |       </div> | ||||||
|     </footer> |     </footer> | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ | |||||||
|       {% endif %} |       {% endif %} | ||||||
|       <span title="Author"><i class="fa fa-user"></i> {{ model.authors | value: post.author_id }}</span> |       <span title="Author"><i class="fa fa-user"></i> {{ model.authors | value: post.author_id }}</span> | ||||||
|       {% if logged_on %} |       {% if logged_on %} | ||||||
|         <span><a href="{{ post.id | edit_post_link }}"><i class="fa fa-pencil-square-o"></i> Edit Post</a></span> |         <span><a href="{{ post | edit_post_link }}"><i class="fa fa-pencil-square-o"></i> Edit Post</a></span> | ||||||
|       {% endif %} |       {% endif %} | ||||||
|     </h4> |     </h4> | ||||||
|     <div>{{ post.text }}</div> |     <div>{{ post.text }}</div> | ||||||
| @ -27,7 +27,7 @@ | |||||||
|           {% assign cat = categories | where: "id", cat_id | first %} |           {% assign cat = categories | where: "id", cat_id | first %} | ||||||
|           <span class="no-wrap"> |           <span class="no-wrap"> | ||||||
|             <i class="fa fa-folder-open-o" title="Category"></i> |             <i class="fa fa-folder-open-o" title="Category"></i> | ||||||
|             <a href="{{ cat.slug | category_link }}" title="Categorized under “{{ cat.name | escape }}”"> |             <a href="{{ cat | category_link }}" title="Categorized under “{{ cat.name | escape }}”"> | ||||||
|              {{ cat.name }} |              {{ cat.name }} | ||||||
|             </a>     |             </a>     | ||||||
|           </span> |           </span> | ||||||
| @ -53,16 +53,16 @@ | |||||||
|       <ul class="pager"> |       <ul class="pager"> | ||||||
|         {% if model.newer_link -%} |         {% if model.newer_link -%} | ||||||
|           <li class="previous item"> |           <li class="previous item"> | ||||||
|             <h4 class="item-heading"><a href="/{{ model.newer_link.value }}">«</a> Previous Post</h4> |             <h4 class="item-heading"><a href="{{ model.newer_link.value }}">«</a> Previous Post</h4> | ||||||
|             <a href="/{{ model.newer_link.value }}">{{ model.newer_name.value }}</a> |             <a href="{{ model.newer_link.value }}">{{ model.newer_name.value }}</a> | ||||||
|           </li> |           </li> | ||||||
|         {%- else -%} |         {%- else -%} | ||||||
|           <li></li> |           <li></li> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         {% if model.older_link -%} |         {% if model.older_link -%} | ||||||
|           <li class="next item"> |           <li class="next item"> | ||||||
|             <h4 class="item-heading">Next Post <a href="/{{ model.older_link.value }}">»</a></h4> |             <h4 class="item-heading">Next Post <a href="{{ model.older_link.value }}">»</a></h4> | ||||||
|             <a href="/{{ model.older_link.value }}">{{ model.older_name.value }}</a> |             <a href="{{ model.older_link.value }}">{{ model.older_name.value }}</a> | ||||||
|           </li> |           </li> | ||||||
|         {%- endif -%} |         {%- endif -%} | ||||||
|       </ul> |       </ul> | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ | |||||||
|       <div class="col"> |       <div class="col"> | ||||||
|         <article> |         <article> | ||||||
|           <h1> |           <h1> | ||||||
|             <a href="/{{ post.permalink }}" title="Permanent link to "{{ post.title | escape }}""> |             <a href="{{ post | relative_link }}" title="Permanent link to "{{ post.title | escape }}""> | ||||||
|               {{ post.title }} |               {{ post.title }} | ||||||
|             </a> |             </a> | ||||||
|           </h1> |           </h1> | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ | |||||||
|     <header> |     <header> | ||||||
|       <nav class="navbar navbar-light bg-light navbar-expand-md justify-content-start px-2"> |       <nav class="navbar navbar-light bg-light navbar-expand-md justify-content-start px-2"> | ||||||
|         <div class="container-fluid"> |         <div class="container-fluid"> | ||||||
|           <a class="navbar-brand" href="/">{{ web_log.name }}</a> |           <a class="navbar-brand" href="{{ "" | relative_link }}">{{ web_log.name }}</a> | ||||||
|           <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" |           <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" | ||||||
|                   aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> |                   aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> | ||||||
|             <span class="navbar-toggler-icon"></span> |             <span class="navbar-toggler-icon"></span> | ||||||
|  | |||||||
| @ -10,7 +10,8 @@ | |||||||
|       <h1 class="home-title"> |       <h1 class="home-title"> | ||||||
|         <small class="home-lead">{{ post.published_on | date: "dddd, MMMM d, yyyy" }}</small><br> |         <small class="home-lead">{{ post.published_on | date: "dddd, MMMM d, yyyy" }}</small><br> | ||||||
|           |           | ||||||
|         <a href="/{{ post.permalink }}" title="Permanent Link to "{{ post.title | strip_html | escape }}""> |         <a href="{{ post | relative_link }}" | ||||||
|  |            title="Permanent Link to "{{ post.title | strip_html | escape }}""> | ||||||
|           {{ post.title }} |           {{ post.title }} | ||||||
|         </a> |         </a> | ||||||
|       </h1> |       </h1> | ||||||
| @ -39,18 +40,18 @@ | |||||||
|         {%- endfor %} |         {%- endfor %} | ||||||
|       </small><br> |       </small><br> | ||||||
|     {%- endif %} |     {%- endif %} | ||||||
|     {%- if logged_on %}<small><a href="{{ post.id | edit_post_link }}">Edit Post</a></small>{% endif %} |     {%- if logged_on %}<small><a href="{{ post | edit_post_link }}">Edit Post</a></small>{% endif %} | ||||||
|   </article> |   </article> | ||||||
| {%- endfor %} | {%- endfor %} | ||||||
| <div class="bottom-nav" role="navigation"> | <div class="bottom-nav" role="navigation"> | ||||||
|   <div class="nav-previous"> |   <div class="nav-previous"> | ||||||
|     {% if model.newer_link -%} |     {% if model.newer_link -%} | ||||||
|       <a href="/{{ model.newer_link.value }}">« Newer Posts</a> |       <a href="{{ model.newer_link.value }}">« Newer Posts</a> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|   </div> |   </div> | ||||||
|   <div class="nav-next"> |   <div class="nav-next"> | ||||||
|     {% if model.older_link -%} |     {% if model.older_link -%} | ||||||
|       <a href="/{{ model.older_link.value }}">Older Posts »</a> |       <a href="{{ model.older_link.value }}">Older Posts »</a> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -12,26 +12,26 @@ | |||||||
|         {{ page_title | strip_html }}{% if page_title and page_title != "" %} » {% endif %}{{ web_log.name }} |         {{ page_title | strip_html }}{% if page_title and page_title != "" %} » {% endif %}{{ web_log.name }} | ||||||
|       {%- endif %} |       {%- endif %} | ||||||
|     </title> |     </title> | ||||||
|     <link rel="stylesheet" href="/themes/tech-blog/style.css"> |     <link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css"> | ||||||
|     {% comment %}link(rel='canonical' href=config.url + url_for(page.path.replace('index.html', ''))) {% endcomment %} |  | ||||||
|     {%- if is_home %} |     {%- if is_home %} | ||||||
|       <link rel="alternate" type="application/rss+xml" |       <link rel="alternate" type="application/rss+xml" title="{{ web_log.name }}" | ||||||
|             title="{{ web_log.name }}" href="https://{{ web_log.urlBase }}/feed.xml"> |             href="{{ "feed.xml" | absolute_link }}"> | ||||||
|  |       <link rel="canonical" href="{{ "" | absolute_link }}"> | ||||||
|     {%- endif %} |     {%- endif %} | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <header class="site-header"> |     <header class="site-header"> | ||||||
|       <div class="header-logo"> |       <div class="header-logo"> | ||||||
|         <a href="/"> |         <a href="{{ "" | relative_link }}"> | ||||||
|           <img src="/themes/tech-blog/img/bitbadger.png" |           <img src="/themes/tech-blog/img/bitbadger.png" | ||||||
|                alt="A cartoon badger looking at a computer screen, with his paw on a mouse" |                alt="A cartoon badger looking at a computer screen, with his paw on a mouse" | ||||||
|                title="Bit Badger Solutions"> |                title="Bit Badger Solutions"> | ||||||
|         </a> |         </a> | ||||||
|       </div> |       </div> | ||||||
|       <div class="header-title"><a href="/">The Bit Badger Blog</a></div> |       <div class="header-title"><a href="{{ "" | relative_link }}">The Bit Badger Blog</a></div> | ||||||
|       <div class="header-spacer"> </div> |       <div class="header-spacer"> </div> | ||||||
|       <div class="header-social"> |       <div class="header-social"> | ||||||
|         <a href="/feed.xml" title="Subscribe to The Bit Badger Blog via RSS"> |         <a href="{{ "feed.xml" | relative_link }}" title="Subscribe to The Bit Badger Blog via RSS"> | ||||||
|           <img src="/themes/tech-blog/img/rss.png" alt="RSS"> |           <img src="/themes/tech-blog/img/rss.png" alt="RSS"> | ||||||
|         </a>     |         </a>     | ||||||
|         <a href="https://twitter.com/Bit_Badger" title="Bit_Badger on Twitter"> |         <a href="https://twitter.com/Bit_Badger" title="Bit_Badger on Twitter"> | ||||||
| @ -49,7 +49,7 @@ | |||||||
|       <aside class="blog-sidebar"> |       <aside class="blog-sidebar"> | ||||||
|         <div> |         <div> | ||||||
|           <div class="sidebar-head">Linux Resources</div> |           <div class="sidebar-head">Linux Resources</div> | ||||||
|           <ul><li><a href="/linux/">Browse Resources</a></li></ul> |           <ul><li><a href="{{ "linux/" | relative_link }}">Browse Resources</a></li></ul> | ||||||
|         </div> |         </div> | ||||||
|         <div> |         <div> | ||||||
|           <div class="sidebar-head">Categories</div> |           <div class="sidebar-head">Categories</div> | ||||||
| @ -76,9 +76,9 @@ | |||||||
|         Powered by <a href="https://github.com/bit-badger/myWebLog/tree/v2" target="_blank" rel="noopener">myWebLog</a> |         Powered by <a href="https://github.com/bit-badger/myWebLog/tree/v2" target="_blank" rel="noopener">myWebLog</a> | ||||||
|         • |         • | ||||||
|         {% if logged_on %} |         {% if logged_on %} | ||||||
|           <a href="/admin">Dashboard</a> |           <a href="{{ "admin" | relative_link }}">Dashboard</a> | ||||||
|         {% else %} |         {% else %} | ||||||
|           <a href="/user/log-on">Log On</a> |           <a href="{{ "user/log-on" | relative_link }}">Log On</a> | ||||||
|         {%- endif %} |         {%- endif %} | ||||||
|       </span> |       </span> | ||||||
|     </footer> |     </footer> | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| <article class="auto"> | <article class="auto"> | ||||||
|   <h1 class="entry-title">{{ page.title }}</h1> |   <h1 class="entry-title">{{ page.title }}</h1> | ||||||
|   <div class="entry-content">{{ page.text }}</div> |   <div class="entry-content">{{ page.text }}</div> | ||||||
|   {%- if logged_on %}<p><small><a href="{{ page.id | edit_page_link }}">Edit Page</a></small></p>{% endif %} |   {%- if logged_on %}<p><small><a href="{{ page | edit_page_link }}">Edit Page</a></small></p>{% endif %} | ||||||
| </article> | </article> | ||||||
| @ -37,8 +37,10 @@ | |||||||
|       </span> • |       </span> • | ||||||
|     {%- endif %} |     {%- endif %} | ||||||
|     Bookmark the |     Bookmark the | ||||||
|     <a href="/{{ post.permalink }}" rel="bookmark" |     <a href="{{ post | absolute_link }}" rel="bookmark" | ||||||
|        title="Permanent link to “{{ post.title | strip_html | escape }}”">permalink</a> |        title="Permanent link to “{{ post.title | strip_html | escape }}”"> | ||||||
|     {%- if logged_on %} • <a href="{{ post.id | edit_post_link }}">Edit Post</a>{% endif %} |       permalink | ||||||
|  |     </a> | ||||||
|  |     {%- if logged_on %} • <a href="{{ post | edit_post_link }}">Edit Post</a>{% endif %} | ||||||
|   </div> |   </div> | ||||||
| </article> | </article> | ||||||
|  | |||||||
| @ -156,58 +156,52 @@ | |||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Confirm and delete a category |    * Confirm and delete an item | ||||||
|    * @param id The ID of the category to be deleted |    * @param name The name of the item to be deleted | ||||||
|    * @param name The name of the category to be deleted |    * @param url The URL to which the form should be posted | ||||||
|    */ |    */ | ||||||
|   deleteCategory(id, name) { |   deleteItem(name, url) { | ||||||
|     if (confirm(`Are you sure you want to delete the category "${name}"? This action cannot be undone.`)) { |     if (confirm(`Are you sure you want to delete the ${name}? This action cannot be undone.`)) { | ||||||
|       const form = document.getElementById("deleteForm") |       const form = document.getElementById("deleteForm") | ||||||
|       form.action = `/admin/category/${id}/delete` |       form.action = url | ||||||
|       form.submit() |       form.submit() | ||||||
|     } |     } | ||||||
|     return false |     return false | ||||||
|   }, |   }, | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Confirm and delete a category | ||||||
|  |    * @param name The name of the category to be deleted | ||||||
|  |    * @param url The URL to which the form should be posted | ||||||
|  |    */ | ||||||
|  |   deleteCategory(name, url) { | ||||||
|  |     return this.deleteItem(`category "${name}"`, url) | ||||||
|  |   }, | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Confirm and delete a page |    * Confirm and delete a page | ||||||
|    * @param id The ID of the page to be deleted |  | ||||||
|    * @param title The title of the page to be deleted |    * @param title The title of the page to be deleted | ||||||
|  |    * @param url The URL to which the form should be posted | ||||||
|    */ |    */ | ||||||
|   deletePage(id, title) { |   deletePage(title, url) { | ||||||
|     if (confirm(`Are you sure you want to delete the page "${name}"? This action cannot be undone.`)) { |     return this.deleteItem(`page "${title}"`, url) | ||||||
|       const form = document.getElementById("deleteForm") |  | ||||||
|       form.action = `/admin/page/${id}/delete` |  | ||||||
|       form.submit() |  | ||||||
|     } |  | ||||||
|     return false |  | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Confirm and delete a post |    * Confirm and delete a post | ||||||
|    * @param id The ID of the post to be deleted |  | ||||||
|    * @param title The title of the post to be deleted |    * @param title The title of the post to be deleted | ||||||
|  |    * @param url The URL to which the form should be posted | ||||||
|    */ |    */ | ||||||
|   deletePost(id, title) { |   deletePost(title, url) { | ||||||
|     if (confirm(`Are you sure you want to delete the post "${name}"? This action cannot be undone.`)) { |     return this.deleteItem(`post "${title}"`, url) | ||||||
|       const form = document.getElementById("deleteForm") |  | ||||||
|       form.action = `/admin/post/${id}/delete` |  | ||||||
|       form.submit() |  | ||||||
|     } |  | ||||||
|     return false |  | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Confirm and delete a tag mapping |    * Confirm and delete a tag mapping | ||||||
|    * @param id The ID of the mapping to be deleted |  | ||||||
|    * @param tag The tag for which the mapping will be deleted |    * @param tag The tag for which the mapping will be deleted | ||||||
|  |    * @param url The URL to which the form should be posted | ||||||
|    */ |    */ | ||||||
|   deleteTagMapping(id, tag) { |   deleteTagMapping(tag, url) { | ||||||
|     if (confirm(`Are you sure you want to delete the mapping for "${tag}"? This action cannot be undone.`)) { |     return this.deleteItem(`mapping for "${tag}"`, url) | ||||||
|       const form = document.getElementById("deleteForm") |  | ||||||
|       form.action = `/admin/tag-mapping/${id}/delete` |  | ||||||
|       form.submit() |  | ||||||
|     } |  | ||||||
|     return false |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user