Add settings update page

More dashboard UI work
This commit is contained in:
Daniel J. Summers 2022-02-27 23:05:22 -05:00
parent 8f94d7ddfe
commit f8c58dbc3d
13 changed files with 407 additions and 34 deletions

View File

@ -2,7 +2,7 @@
namespace MyWebLog.Data;
public static class CategoryEtensions
public static class CategoryExtensions
{
/// <summary>
/// Count all categories
@ -10,4 +10,11 @@ public static class CategoryEtensions
/// <returns>A count of all categories</returns>
public static async Task<int> CountAll(this DbSet<Category> db) =>
await db.CountAsync().ConfigureAwait(false);
/// <summary>
/// Count top-level categories (those that do not have a parent)
/// </summary>
/// <returns>A count of all top-level categories</returns>
public static async Task<int> CountTopLevel(this DbSet<Category> db) =>
await db.CountAsync(c => c.ParentId == null).ConfigureAwait(false);
}

View File

@ -11,6 +11,20 @@ public static class PageExtensions
public static async Task<int> CountAll(this DbSet<Page> db) =>
await db.CountAsync().ConfigureAwait(false);
/// <summary>
/// Count the number of pages in the page list
/// </summary>
/// <returns>The number of pages in the page list</returns>
public static async Task<int> CountListed(this DbSet<Page> db) =>
await db.CountAsync(p => p.ShowInPageList).ConfigureAwait(false);
/// <summary>
/// Retrieve all pages (non-tracked)
/// </summary>
/// <returns>A list of all pages</returns>
public static async Task<List<Page>> FindAll(this DbSet<Page> db) =>
await db.OrderBy(p => p.Title).ToListAsync().ConfigureAwait(false);
/// <summary>
/// Retrieve a page by its ID (non-tracked)
/// </summary>

View File

@ -5,10 +5,18 @@ namespace MyWebLog.Data;
public static class WebLogDetailsExtensions
{
/// <summary>
/// Find the details of a web log by its host
/// Find the details of a web log by its host (non-tracked)
/// </summary>
/// <param name="host">The host</param>
/// <returns>The web log (or null if not found)</returns>
public static async Task<WebLogDetails?> FindByHost(this DbSet<WebLogDetails> db, string host) =>
await db.FirstOrDefaultAsync(wld => wld.UrlBase == host).ConfigureAwait(false);
/// <summary>
/// Get the details of a web log by its host (tracked)
/// </summary>
/// <param name="host">The host</param>
/// <returns>The web log (or null if not found)</returns>
public static async Task<WebLogDetails?> GetByHost(this DbSet<WebLogDetails> db, string host) =>
await db.AsTracking().FirstOrDefaultAsync(wld => wld.UrlBase == host).ConfigureAwait(false);
}

View File

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MyWebLog.Features.Admin;
@ -6,6 +8,7 @@ namespace MyWebLog.Features.Admin;
/// Controller for admin-specific displays and routes
/// </summary>
[Route("/admin")]
[Authorize]
public class AdminController : MyWebLogController
{
/// <inheritdoc />
@ -18,6 +21,33 @@ public class AdminController : MyWebLogController
Posts = await Db.Posts.CountByStatus(PostStatus.Published),
Drafts = await Db.Posts.CountByStatus(PostStatus.Draft),
Pages = await Db.Pages.CountAll(),
Categories = await Db.Categories.CountAll()
ListedPages = await Db.Pages.CountListed(),
Categories = await Db.Categories.CountAll(),
TopLevelCategories = await Db.Categories.CountTopLevel()
});
[HttpGet("settings")]
public async Task<IActionResult> Settings() =>
View(new SettingsModel(WebLog)
{
DefaultPages = Enumerable.Repeat(new SelectListItem($"- {Resources.FirstPageOfPosts} -", "posts"), 1)
.Concat((await Db.Pages.FindAll()).Select(p => new SelectListItem(p.Title, p.Id)))
});
[HttpPost("settings")]
public async Task<IActionResult> SaveSettings(SettingsModel model)
{
var details = await Db.WebLogDetails.GetByHost(WebLog.UrlBase);
if (details is null) return NotFound();
model.PopulateSettings(details);
await Db.SaveChangesAsync();
// Update cache
WebLogCache.Set(WebLogCache.HostToDb(HttpContext), (await Db.WebLogDetails.FindByHost(WebLog.UrlBase))!);
// TODO: confirmation message
return RedirectToAction(nameof(Index));
}
}

View File

@ -20,11 +20,21 @@ public class DashboardModel : MyWebLogModel
/// </summary>
public int Pages { get; set; } = 0;
/// <summary>
/// The number of pages in the page list
/// </summary>
public int ListedPages { get; set; } = 0;
/// <summary>
/// The number of categories
/// </summary>
public int Categories { get; set; } = 0;
/// <summary>
/// The top-level categories
/// </summary>
public int TopLevelCategories { get; set; } = 0;
/// <inheritdoc />
public DashboardModel(WebLogDetails webLog) : base(webLog) { }
}

View File

@ -3,33 +3,55 @@
Layout = "_AdminLayout";
ViewBag.Title = Resources.Dashboard;
}
<article class="container">
<article class="container pt-3">
<div class="row">
<section class="col col-sm-4">
<h3>@Resources.Posts</h3>
<p>@string.Format(Resources.ThereAreXPublishedPostsAndYDrafts, Model.Posts, Model.Drafts)</p>
<p>
<a asp-action="All" asp-controller="Post" class="btn btn-secondary me-2">View All</a>
<a asp-action="Edit" asp-controller="Post" asp-route-id="new" class="btn btn-primary">Write a New Post</a>
</p>
<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">View All</a>
<a asp-action="Edit" asp-controller="Post" asp-route-id="new" class="btn btn-primary">Write a New Post</a>
</div>
</div>
</section>
<section class="col col-sm-4">
<h3>@Resources.Pages</h3>
<p>@string.Format(Resources.ThereAreXPages, Model.Pages)</p>
<p>
<a asp-action="All" asp-controller="Page" class="btn btn-secondary me-2">View All</a>
<a asp-action="Edit" asp-controller="Page" asp-route-id="new" class="btn btn-primary">Create a New Page</a>
</p>
</section>
<section class="col col-sm-4">
<h3>@Resources.Categories</h3>
<p>@string.Format(Resources.ThereAreXCategories, Model.Categories)</p>
<p>
<a asp-action="All" asp-controller="Category" class="btn btn-secondary me-2">View All</a>
<a asp-action="Edit" asp-controller="Category" asp-route-id="new" class="btn btn-secondary">
Add a New Category
</a>
</p>
<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">View All</a>
<a asp-action="Edit" asp-controller="Page" asp-route-id="new" class="btn btn-primary">Create a New Page</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">View All</a>
<a asp-action="Edit" asp-controller="Category" asp-route-id="new" class="btn btn-secondary">
Add a New Category
</a>
</div>
</div>
</section>
</div>
<div class="row pb-3">
<div class="col text-end">
<a asp-action="Settings" class="btn btn-secondary">Modify Settings</a>
</div>
</div>
</article>

View File

@ -0,0 +1,50 @@
@model SettingsModel
@{
Layout = "_AdminLayout";
ViewBag.Title = "Web Log Settings";
}
<article class="pt-3">
<form asp-action="SaveSettings" method="post">
<div class="container">
<div class="row">
<div class="col-12 col-md-6 col-xl-4 offset-xl-2 pb-3">
<div class="form-floating">
<input type="text" asp-for="Name" class="form-control">
<label asp-for="Name"></label>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4 pb-3">
<div class="form-floating">
<input type="text" asp-for="Subtitle" class="form-control">
<label asp-for="Subtitle"></label>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-4 col-xl-2 offset-xl-2 pb-3">
<div class="form-floating">
<input type="number" asp-for="PostsPerPage" class="form-control" min="0" max="50">
<label asp-for="PostsPerPage"></label>
</div>
</div>
<div class="col-12 col-md-4 col-xl-3 pb-3">
<div class="form-floating">
<input type="text" asp-for="TimeZone" class="form-control">
<label asp-for="TimeZone"></label>
</div>
</div>
<div class="col-12 col-md-4 col-xl-3 pb-3">
<div class="form-floating">
<select asp-for="DefaultPage" asp-items="Model.DefaultPages" class="form-control"></select>
<label asp-for="DefaultPage"></label>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col text-center">
<button type="submit" class="btn btn-primary">@Resources.SaveChanges</button>
</div>
</div>
</div>
</form>
</article>

View File

@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using System.ComponentModel.DataAnnotations;
namespace MyWebLog.Features.Admin;
/// <summary>
/// View model for editing web log settings
/// </summary>
public class SettingsModel : MyWebLogModel
{
/// <summary>
/// The name of the web log
/// </summary>
[Required(AllowEmptyStrings = false)]
[Display(ResourceType = typeof(Resources), Name = "Name")]
public string Name { get; set; } = "";
/// <summary>
/// The subtitle of the web log
/// </summary>
[Display(ResourceType = typeof(Resources), Name = "Subtitle")]
public string Subtitle { get; set; } = "";
/// <summary>
/// The default page
/// </summary>
[Required]
[Display(ResourceType = typeof(Resources), Name = "DefaultPage")]
public string DefaultPage { get; set; } = "";
/// <summary>
/// How many posts should appear on index pages
/// </summary>
[Required]
[Display(ResourceType = typeof(Resources), Name = "PostsPerPage")]
[Range(0, 50)]
public byte PostsPerPage { get; set; } = 10;
/// <summary>
/// The time zone in which dates/times should be displayed
/// </summary>
[Required]
[Display(ResourceType = typeof(Resources), Name = "TimeZone")]
public string TimeZone { get; set; } = "";
/// <summary>
/// Possible values for the default page
/// </summary>
public IEnumerable<SelectListItem> DefaultPages { get; set; } = Enumerable.Empty<SelectListItem>();
[Obsolete("Only used for model binding; use the WebLogDetails constructor")]
public SettingsModel() : base(new()) { }
/// <inheritdoc />
public SettingsModel(WebLogDetails webLog) : base(webLog)
{
Name = webLog.Name;
Subtitle = webLog.Subtitle ?? "";
DefaultPage = webLog.DefaultPage;
PostsPerPage = webLog.PostsPerPage;
TimeZone = webLog.TimeZone;
}
/// <summary>
/// Populate the settings object from the data in this form
/// </summary>
/// <param name="settings">The settings to be updated</param>
public void PopulateSettings(WebLogDetails settings)
{
settings.Name = Name;
settings.Subtitle = Subtitle == "" ? null : Subtitle;
settings.DefaultPage = DefaultPage;
settings.PostsPerPage = PostsPerPage;
settings.TimeZone = TimeZone;
}
}

View File

@ -33,7 +33,6 @@ public class PostController : MyWebLogController
[HttpGet("~/{*permalink}")]
public async Task<IActionResult> CatchAll(string permalink)
{
Console.Write($"Got permalink |{permalink}|");
var post = await Db.Posts.FindByPermalink(permalink);
if (post != null)
{
@ -48,8 +47,9 @@ public class PostController : MyWebLogController
// TOOD: search prior permalinks for posts and pages
await Task.CompletedTask;
throw new NotImplementedException();
// We tried, we really tried...
Console.Write($"Returning 404 for permalink |{permalink}|");
return NotFound();
}
[HttpGet("all")]

View File

@ -25,7 +25,6 @@
</nav>
</header>
<main>
<h2 class="pb-3">@ViewBag.Title</h2>
@* Each.Messages
@Current.ToDisplay
@EndEach *@

View File

@ -3,7 +3,8 @@
Layout = "_AdminLayout";
ViewBag.Title = @Resources.LogOn;
}
<article>
<h2 class="p-3 ">@Resources.LogOnTo @Model.WebLog.Name</h2>
<article class="pb-3">
<form asp-action="DoLogOn" asp-controller="User" method="post">
<div class="container">
<div class="row pb-3">

View File

@ -69,6 +69,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to All.
/// </summary>
public static string All {
get {
return ResourceManager.GetString("All", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Categories.
/// </summary>
@ -87,6 +96,24 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Default Page.
/// </summary>
public static string DefaultPage {
get {
return ResourceManager.GetString("DefaultPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Drafts.
/// </summary>
public static string Drafts {
get {
return ResourceManager.GetString("Drafts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to E-mail Address.
/// </summary>
@ -96,6 +123,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to First Page of Posts.
/// </summary>
public static string FirstPageOfPosts {
get {
return ResourceManager.GetString("FirstPageOfPosts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Log Off.
/// </summary>
@ -114,6 +150,24 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Log On to.
/// </summary>
public static string LogOnTo {
get {
return ResourceManager.GetString("LogOnTo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pages.
/// </summary>
@ -141,6 +195,51 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Posts per Page.
/// </summary>
public static string PostsPerPage {
get {
return ResourceManager.GetString("PostsPerPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Published.
/// </summary>
public static string Published {
get {
return ResourceManager.GetString("Published", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save Changes.
/// </summary>
public static string SaveChanges {
get {
return ResourceManager.GetString("SaveChanges", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shown in Page List.
/// </summary>
public static string ShownInPageList {
get {
return ResourceManager.GetString("ShownInPageList", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Subtitle.
/// </summary>
public static string Subtitle {
get {
return ResourceManager.GetString("Subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to There are {0} categories.
/// </summary>
@ -167,5 +266,23 @@ namespace MyWebLog.Properties {
return ResourceManager.GetString("ThereAreXPublishedPostsAndYDrafts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Time Zone.
/// </summary>
public static string TimeZone {
get {
return ResourceManager.GetString("TimeZone", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Top Level.
/// </summary>
public static string TopLevel {
get {
return ResourceManager.GetString("TopLevel", resourceCulture);
}
}
}
}

View File

@ -120,21 +120,39 @@
<data name="Admin" xml:space="preserve">
<value>Admin</value>
</data>
<data name="All" xml:space="preserve">
<value>All</value>
</data>
<data name="Categories" xml:space="preserve">
<value>Categories</value>
</data>
<data name="Dashboard" xml:space="preserve">
<value>Dashboard</value>
</data>
<data name="DefaultPage" xml:space="preserve">
<value>Default Page</value>
</data>
<data name="Drafts" xml:space="preserve">
<value>Drafts</value>
</data>
<data name="EmailAddress" xml:space="preserve">
<value>E-mail Address</value>
</data>
<data name="FirstPageOfPosts" xml:space="preserve">
<value>First Page of Posts</value>
</data>
<data name="LogOff" xml:space="preserve">
<value>Log Off</value>
</data>
<data name="LogOn" xml:space="preserve">
<value>Log On</value>
</data>
<data name="LogOnTo" xml:space="preserve">
<value>Log On to</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Pages" xml:space="preserve">
<value>Pages</value>
</data>
@ -144,6 +162,21 @@
<data name="Posts" xml:space="preserve">
<value>Posts</value>
</data>
<data name="PostsPerPage" xml:space="preserve">
<value>Posts per Page</value>
</data>
<data name="Published" xml:space="preserve">
<value>Published</value>
</data>
<data name="SaveChanges" xml:space="preserve">
<value>Save Changes</value>
</data>
<data name="ShownInPageList" xml:space="preserve">
<value>Shown in Page List</value>
</data>
<data name="Subtitle" xml:space="preserve">
<value>Subtitle</value>
</data>
<data name="ThereAreXCategories" xml:space="preserve">
<value>There are {0} categories</value>
</data>
@ -153,4 +186,10 @@
<data name="ThereAreXPublishedPostsAndYDrafts" xml:space="preserve">
<value>There are {0} published posts and {1} drafts</value>
</data>
<data name="TimeZone" xml:space="preserve">
<value>Time Zone</value>
</data>
<data name="TopLevel" xml:space="preserve">
<value>Top Level</value>
</data>
</root>