WIP on DotLiquid support

This commit is contained in:
2022-04-16 23:06:38 -04:00
parent 62f7896621
commit 98eb2b1785
78 changed files with 451 additions and 45 deletions

View File

@@ -0,0 +1,62 @@
namespace MyWebLog.Features.Admin
open Microsoft.AspNetCore.Authorization
open Microsoft.AspNetCore.Mvc
open Microsoft.AspNetCore.Mvc.Rendering
open MyWebLog
open MyWebLog.Features.Shared
open RethinkDb.Driver.Net
open System.Threading.Tasks
/// Controller for admin-specific displays and routes
[<Route "/admin">]
[<Authorize>]
type AdminController () =
inherit MyWebLogController ()
[<HttpGet "">]
member this.Index () = task {
let getCount (f : WebLogId -> IConnection -> Task<int>) = f this.WebLog.id this.Db
let! posts = Data.Post.countByStatus Published |> getCount
let! drafts = Data.Post.countByStatus Draft |> getCount
let! pages = Data.Page.countAll |> getCount
let! listed = Data.Page.countListed |> getCount
let! cats = Data.Category.countAll |> getCount
let! topCats = Data.Category.countTopLevel |> getCount
return this.View (DashboardModel (
this.WebLog,
Posts = posts,
Drafts = drafts,
Pages = pages,
ListedPages = listed,
Categories = cats,
TopLevelCategories = topCats
))
}
[<HttpGet "settings">]
member this.Settings() = task {
let! allPages = Data.Page.findAll this.WebLog.id this.Db
return this.View (SettingsModel (
this.WebLog,
DefaultPages =
(Seq.singleton (SelectListItem ("- {Resources.FirstPageOfPosts} -", "posts"))
|> Seq.append (allPages |> Seq.map (fun p -> SelectListItem (p.title, PageId.toString p.id))))
))
}
[<HttpPost "settings">]
member this.SaveSettings (model : SettingsModel) = task {
match! Data.WebLog.findByHost this.WebLog.urlBase this.Db with
| Some webLog ->
let updated = model.UpdateSettings webLog
do! Data.WebLog.updateSettings updated this.Db
// Update cache
WebLogCache.set (WebLogCache.hostToDb this.HttpContext) updated
// TODO: confirmation message
return this.RedirectToAction (nameof this.Index);
| None -> return this.NotFound ()
}

View File

@@ -0,0 +1,76 @@
namespace MyWebLog.Features.Admin
open MyWebLog
open MyWebLog.Features.Shared
/// The model used to display the dashboard
type DashboardModel (webLog) =
inherit MyWebLogModel (webLog)
/// The number of published posts
member val Posts = 0 with get, set
/// The number of post drafts
member val Drafts = 0 with get, set
/// The number of pages
member val Pages = 0 with get, set
/// The number of pages in the page list
member val ListedPages = 0 with get, set
/// The number of categories
member val Categories = 0 with get, set
/// The top-level categories
member val TopLevelCategories = 0 with get, set
open Microsoft.AspNetCore.Mvc.Rendering
open System.ComponentModel.DataAnnotations
/// View model for editing web log settings
type SettingsModel (webLog) =
inherit MyWebLogModel (webLog)
/// Default constructor
[<System.Obsolete "Only used for model binding; use the WebLogDetails constructor">]
new() = SettingsModel WebLog.empty
/// The name of the web log
[<Required (AllowEmptyStrings = false)>]
[<Display ( ResourceType = typeof<Resources>, Name = "Name")>]
member val Name = webLog.name with get, set
/// The subtitle of the web log
[<Display(ResourceType = typeof<Resources>, Name = "Subtitle")>]
member val Subtitle = (defaultArg webLog.subtitle "") with get, set
/// The default page
[<Required>]
[<Display(ResourceType = typeof<Resources>, Name = "DefaultPage")>]
member val DefaultPage = webLog.defaultPage with get, set
/// How many posts should appear on index pages
[<Required>]
[<Display(ResourceType = typeof<Resources>, Name = "PostsPerPage")>]
[<Range(0, 50)>]
member val PostsPerPage = webLog.postsPerPage with get, set
/// The time zone in which dates/times should be displayed
[<Required>]
[<Display(ResourceType = typeof<Resources>, Name = "TimeZone")>]
member val TimeZone = webLog.timeZone with get, set
/// Possible values for the default page
member val DefaultPages = Seq.empty<SelectListItem> with get, set
/// Update the settings object from the data in this form
member this.UpdateSettings (settings : WebLog) =
{ settings with
name = this.Name
subtitle = (match this.Subtitle with "" -> None | sub -> Some sub)
defaultPage = this.DefaultPage
postsPerPage = this.PostsPerPage
timeZone = this.TimeZone
}

View File

@@ -0,0 +1,61 @@
@model DashboardModel
@{
Layout = "_AdminLayout";
ViewBag.Title = Resources.Dashboard;
}
<article class="container pt-3">
<div class="row">
<section class="col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3">
<div class="card">
<header class="card-header text-white bg-primary">@Resources.Posts</header>
<div class="card-body">
<h6 class="card-subtitle text-muted pb-3">
@Resources.Published <span class="badge rounded-pill bg-secondary">@Model.Posts</span>
&nbsp; @Resources.Drafts <span class="badge rounded-pill bg-secondary">@Model.Drafts</span>
</h6>
<a asp-action="All" asp-controller="Post" class="btn btn-secondary me-2">@Resources.ViewAll</a>
<a asp-action="Edit" asp-controller="Post" asp-route-id="new" class="btn btn-primary">
@Resources.WriteANewPost
</a>
</div>
</div>
</section>
<section class="col-lg-5 col-xl-4 pb-3">
<div class="card">
<header class="card-header text-white bg-primary">@Resources.Pages</header>
<div class="card-body">
<h6 class="card-subtitle text-muted pb-3">
@Resources.All <span class="badge rounded-pill bg-secondary">@Model.Pages</span>
&nbsp; @Resources.ShownInPageList <span class="badge rounded-pill bg-secondary">@Model.ListedPages</span>
</h6>
<a asp-action="All" asp-controller="Page" class="btn btn-secondary me-2">@Resources.ViewAll</a>
<a asp-action="Edit" asp-controller="Page" asp-route-id="new" class="btn btn-primary">
@Resources.CreateANewPage
</a>
</div>
</div>
</section>
</div>
<div class="row">
<section class="col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3">
<div class="card">
<header class="card-header text-white bg-secondary">@Resources.Categories</header>
<div class="card-body">
<h6 class="card-subtitle text-muted pb-3">
@Resources.All <span class="badge rounded-pill bg-secondary">@Model.Categories</span>
&nbsp; @Resources.TopLevel <span class="badge rounded-pill bg-secondary">@Model.TopLevelCategories</span>
</h6>
<a asp-action="All" asp-controller="Category" class="btn btn-secondary me-2">@Resources.ViewAll</a>
<a asp-action="Edit" asp-controller="Category" asp-route-id="new" class="btn btn-secondary">
@Resources.AddANewCategory
</a>
</div>
</div>
</section>
</div>
<div class="row pb-3">
<div class="col text-end">
<a asp-action="Settings" class="btn btn-secondary">@Resources.ModifySettings</a>
</div>
</div>
</article>

View File

@@ -0,0 +1,15 @@
namespace MyWebLog.Features.Pages
open MyWebLog
open MyWebLog.Features.Shared
/// The model used to render a single page
type SinglePageModel (page : Page, webLog) =
inherit MyWebLogModel (webLog)
/// The page to be rendered
member _.Page with get () = page
/// Is this the home page?
member _.IsHome with get() = PageId.toString page.id = webLog.defaultPage

View File

@@ -0,0 +1,65 @@
namespace MyWebLog.Features.Posts
open Microsoft.AspNetCore.Authorization
open Microsoft.AspNetCore.Mvc
open MyWebLog
open MyWebLog.Features.Pages
open MyWebLog.Features.Shared
open System
open System.Threading.Tasks
/// Handle post-related requests
[<Route "/post">]
[<Authorize>]
type PostController () =
inherit MyWebLogController ()
[<HttpGet "~/">]
[<AllowAnonymous>]
member this.Index () = task {
match this.WebLog.defaultPage with
| "posts" -> return! this.PageOfPosts 1
| pageId ->
match! Data.Page.findById (PageId pageId) this.WebLog.id this.Db with
| Some page ->
return this.ThemedView (defaultArg page.template "SinglePage", SinglePageModel (page, this.WebLog))
| None -> return this.NotFound ()
}
[<HttpGet "~/page/{pageNbr:int}">]
[<AllowAnonymous>]
member this.PageOfPosts (pageNbr : int) = task {
let! posts = Data.Post.findPageOfPublishedPosts this.WebLog.id pageNbr this.WebLog.postsPerPage this.Db
return this.ThemedView ("Index", MultiplePostModel (posts, this.WebLog))
}
[<HttpGet "~/{*link}">]
member this.CatchAll (link : string) = task {
let permalink = Permalink link
match! Data.Post.findByPermalink permalink this.WebLog.id this.Db with
| Some post -> return this.NotFound ()
// TODO: return via single-post action
| None ->
match! Data.Page.findByPermalink permalink this.WebLog.id this.Db with
| Some page ->
return this.ThemedView (defaultArg page.template "SinglePage", SinglePageModel (page, this.WebLog))
| None ->
// TOOD: search prior permalinks for posts and pages
// We tried, we really tried...
Console.Write($"Returning 404 for permalink |{permalink}|");
return this.NotFound ()
}
[<HttpGet "all">]
member this.All () = task {
do! Task.CompletedTask;
NotImplementedException () |> raise
}
[<HttpGet "{id}/edit">]
member this.Edit(postId : string) = task {
do! Task.CompletedTask;
NotImplementedException () |> raise
}

View File

@@ -0,0 +1,11 @@
namespace MyWebLog.Features.Posts
open MyWebLog
open MyWebLog.Features.Shared
/// The model used to render multiple posts
type MultiplePostModel (posts : Post seq, webLog) =
inherit MyWebLogModel (webLog)
/// The posts to be rendered
member _.Posts with get () = posts

View File

@@ -0,0 +1,45 @@
namespace MyWebLog.Features.Shared
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.DependencyInjection
open MyWebLog
open RethinkDb.Driver.Net
open System.Security.Claims
/// Base class for myWebLog controllers
type MyWebLogController () =
inherit Controller ()
/// The data context to use to fulfil this request
member this.Db with get () = this.HttpContext.RequestServices.GetRequiredService<IConnection> ()
/// The details for the current web log
member this.WebLog with get () = WebLogCache.getByCtx this.HttpContext
/// The ID of the currently authenticated user
member this.UserId with get () =
this.User.Claims
|> Seq.tryFind (fun c -> c.Type = ClaimTypes.NameIdentifier)
|> Option.map (fun c -> c.Value)
|> Option.defaultValue ""
/// Retern a themed view
member this.ThemedView (template : string, model : obj) : IActionResult =
// TODO: get actual version
this.ViewData["Version"] <- "2"
this.View (template, model)
/// Return a 404 response
member _.NotFound () : IActionResult =
base.NotFound ()
/// Redirect to an action in this controller
member _.RedirectToAction action : IActionResult =
base.RedirectToAction action
/// Base model class for myWebLog views
type MyWebLogModel (webLog : WebLog) =
/// The details for the web log
member _.WebLog with get () = webLog