First cut at page add/edit

This commit is contained in:
Daniel J. Summers 2022-03-01 22:08:36 -05:00
parent f8c58dbc3d
commit d0cc9d0d7c
14 changed files with 558 additions and 16 deletions

View File

@ -33,6 +33,14 @@ public static class PageExtensions
public static async Task<Page?> FindById(this DbSet<Page> db, string id) =>
await db.SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false);
/// <summary>
/// Retrieve a page by its ID, including its revisions (non-tracked)
/// </summary>
/// <param name="id">The ID of the page to retrieve</param>
/// <returns>The requested page (or null if it is not found)</returns>
public static async Task<Page?> FindByIdWithRevisions(this DbSet<Page> db, string id) =>
await db.Include(p => p.Revisions).SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false);
/// <summary>
/// Retrieve a page by its permalink (non-tracked)
/// </summary>
@ -40,4 +48,20 @@ public static class PageExtensions
/// <returns>The requested page (or null if it is not found)</returns>
public static async Task<Page?> FindByPermalink(this DbSet<Page> db, string permalink) =>
await db.SingleOrDefaultAsync(p => p.Permalink == permalink).ConfigureAwait(false);
/// <summary>
/// Retrieve a page of pages (non-tracked)
/// </summary>
/// <param name="pageNbr">The page number to retrieve</param>
/// <returns>The pages</returns>
public static async Task<List<Page>> FindPageOfPages(this DbSet<Page> db, int pageNbr) =>
await db.Skip((pageNbr - 1) * 50).Take(25).ToListAsync().ConfigureAwait(false);
/// <summary>
/// Retrieve a page by its ID (tracked)
/// </summary>
/// <param name="id">The ID of the page to retrieve</param>
/// <returns>The requested page (or null if it is not found)</returns>
public static async Task<Page?> GetById(this DbSet<Page> db, string id) =>
await db.AsTracking().Include(p => p.Revisions).SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false);
}

View File

@ -13,8 +13,10 @@
@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>
<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>
@ -26,8 +28,10 @@
@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>
<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>
@ -41,9 +45,9 @@
@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="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">
Add a New Category
@Resources.AddANewCategory
</a>
</div>
</div>
@ -51,7 +55,7 @@
</div>
<div class="row pb-3">
<div class="col text-end">
<a asp-action="Settings" class="btn btn-secondary">Modify Settings</a>
<a asp-action="Settings" class="btn btn-secondary">@Resources.ModifySettings</a>
</div>
</div>
</article>

View File

@ -1,7 +1,7 @@
@model SettingsModel
@{
Layout = "_AdminLayout";
ViewBag.Title = "Web Log Settings";
ViewBag.Title = Resources.WebLogSettings;
}
<article class="pt-3">
<form asp-action="SaveSettings" method="post">

View File

@ -0,0 +1,37 @@
@model PageListModel
@{
Layout = "_AdminLayout";
ViewBag.Title = Resources.Pages;
}
<article class="container">
<a asp-action="Edit" asp-route-id="new" class="btn btn-primary btn-sm my-3">@Resources.CreateANewPage</a>
<table class="table table-sm table-striped table-hover">
<thead>
<tr>
<th scope="col">@Resources.Actions</th>
<th scope="col">@Resources.Title</th>
<th scope="col">@Resources.InListQuestion</th>
<th scope="col">@Resources.LastUpdated</th>
</tr>
</thead>
<tbody>
@foreach (var pg in Model.Pages)
{
<tr>
<td>
<a asp-action="Edit" asp-route-id="@pg.Id">@Resources.Edit</a>
</td>
<td>
@pg.Title
@if (pg.Id == Model.WebLog.DefaultPage)
{
<text> &nbsp; </text><span class="badge bg-success">HOME PAGE</span>
}
</td>
<td><yes-no asp-for="pg.ShowInPageList" /></td>
<td>@pg.UpdatedOn.ToString(Resources.DateFormatString)</td>
</tr>
}
</tbody>
</table>
</article>

View File

@ -0,0 +1,56 @@
@model EditPageModel
@{
Layout = "_AdminLayout";
ViewBag.Title = Model.IsNew ? Resources.AddANewPage : Resources.EditPage;
}
<article>
<h2 class="pb-3">@ViewBag.Title</h2>
<form asp-action="Save" asp-route-id="@Model.PageId" method="post">
<input type="hidden" asp-for="PageId">
<div class="container">
<div class="row mb-3">
<div class="col">
<div class="form-floating">
<input type="text" asp-for="Title" class="form-control" autofocus>
<label asp-for="Title"></label>
<span asp-validation-for="Title"></span>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-9">
<div class="form-floating">
<input type="text" asp-for="Permalink" class="form-control">
<label asp-for="Permalink"></label>
<span asp-validation-for="Permalink"></span>
</div>
</div>
<div class="col-3 align-self-center">
<div class="form-check form-switch">
<input type="checkbox" asp-for="IsShownInPageList" class="form-check-input">
<label asp-for="IsShownInPageList" class="form-check-label"></label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col">
<label asp-for="Text"></label> &nbsp; &nbsp;
<input type="radio" asp-for="Source" id="source_html" class="btn-check" value="@RevisionSource.Html">
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
<input type="radio" asp-for="Source" id="source_md" class="btn-check" value="@RevisionSource.Markdown">
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
</div>
</div>
<div class="row mb-3">
<div class="col">
<textarea asp-for="Text" class="form-control" rows="10"></textarea>
</div>
</div>
<div class="row mb-3">
<div class="col">
<button type="submit" class="btn btn-primary">@Resources.SaveChanges</button>
</div>
</div>
</div>
</form>
</article>

View File

@ -0,0 +1,99 @@
using System.ComponentModel.DataAnnotations;
namespace MyWebLog.Features.Pages;
/// <summary>
/// Model used to edit pages
/// </summary>
public class EditPageModel : MyWebLogModel
{
/// <summary>
/// The ID of the page being edited
/// </summary>
public string PageId { get; set; } = "new";
/// <summary>
/// Whether this is a new page
/// </summary>
public bool IsNew => PageId == "new";
/// <summary>
/// The title of the page
/// </summary>
[Display(ResourceType = typeof(Resources), Name = "Title")]
[Required(AllowEmptyStrings = false)]
public string Title { get; set; } = "";
/// <summary>
/// The permalink for the page
/// </summary>
[Display(ResourceType = typeof(Resources), Name = "Permalink")]
[Required(AllowEmptyStrings = false)]
public string Permalink { get; set; } = "";
/// <summary>
/// Whether this page is shown in the page list
/// </summary>
[Display(ResourceType = typeof(Resources), Name = "ShowInPageList")]
public bool IsShownInPageList { get; set; } = false;
/// <summary>
/// The source format for the text
/// </summary>
public RevisionSource Source { get; set; } = RevisionSource.Html;
/// <summary>
/// The text of the page
/// </summary>
[Display(ResourceType = typeof(Resources), Name = "PageText")]
[Required(AllowEmptyStrings = false)]
public string Text { get; set; } = "";
[Obsolete("Only used for model binding; use the WebLogDetails constructor")]
public EditPageModel() : base(new()) { }
/// <inheritdoc />
public EditPageModel(WebLogDetails webLog) : base(webLog) { }
/// <summary>
/// Create a model from an existing page
/// </summary>
/// <param name="page">The page from which the model will be created</param>
/// <param name="webLog">The web log to which the page belongs</param>
/// <returns>A populated model</returns>
public static EditPageModel CreateFromPage(Page page, WebLogDetails webLog)
{
var lastRev = page.Revisions.OrderByDescending(r => r.AsOf).First();
return new(webLog)
{
PageId = page.Id,
Title = page.Title,
Permalink = page.Permalink,
IsShownInPageList = page.ShowInPageList,
Source = lastRev.SourceType,
Text = lastRev.Text
};
}
/// <summary>
/// Populate a page from the values contained in this page
/// </summary>
/// <param name="page">The page to be populated</param>
/// <returns>The populated page</returns>
public Page? PopulatePage(Page? page)
{
if (page == null) return null;
page.Title = Title;
page.Permalink = Permalink;
page.ShowInPageList = IsShownInPageList;
page.Revisions.Add(new()
{
Id = WebLogDbContext.NewId(),
AsOf = DateTime.UtcNow,
SourceType = Source,
Text = Text
});
return page;
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Markdig;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MyWebLog.Features.Pages;
@ -10,20 +11,54 @@ namespace MyWebLog.Features.Pages;
[Authorize]
public class PageController : MyWebLogController
{
/// <summary>
/// Pipeline with most extensions enabled
/// </summary>
private readonly MarkdownPipeline _pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants().UseAdvancedExtensions().Build();
/// <inheritdoc />
public PageController(WebLogDbContext db) : base(db) { }
[HttpGet("all")]
public async Task<IActionResult> All()
{
await Task.CompletedTask;
throw new NotImplementedException();
}
[HttpGet("all/page/{pageNbr:int}")]
public async Task<IActionResult> All(int? pageNbr) =>
View(new PageListModel(await Db.Pages.FindPageOfPages(pageNbr ?? 1), WebLog));
[HttpGet("{id}/edit")]
public async Task<IActionResult> Edit(string id)
{
await Task.CompletedTask;
throw new NotImplementedException();
if (id == "new") return View(new EditPageModel(WebLog));
var page = await Db.Pages.FindByIdWithRevisions(id);
if (page == null) return NotFound();
return View(EditPageModel.CreateFromPage(page, WebLog));
}
[HttpPost("{id}/edit")]
public async Task<IActionResult> Save(EditPageModel model)
{
var page = model.PopulatePage(model.IsNew
? new()
{
Id = WebLogDbContext.NewId(),
AuthorId = UserId,
PublishedOn = DateTime.UtcNow,
Revisions = new List<PageRevision>()
}
: await Db.Pages.GetById(model.PageId));
if (page == null) return NotFound();
page.Text = model.Source == RevisionSource.Html ? model.Text : Markdown.ToHtml(model.Text, _pipeline);
page.UpdatedOn = DateTime.UtcNow;
if (model.IsNew) await Db.Pages.AddAsync(page);
await Db.SaveChangesAsync();
// TODO: confirmation
return RedirectToAction(nameof(All));
}
}

View File

@ -0,0 +1,19 @@
namespace MyWebLog.Features.Pages;
/// <summary>
/// View model for viewing a list of pages
/// </summary>
public class PageListModel : MyWebLogModel
{
public IList<Page> Pages { get; init; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="pages">The pages to display</param>
/// <param name="webLog">The web log details</param>
public PageListModel(IList<Page> pages, WebLogDetails webLog) : base(webLog)
{
Pages = pages;
}
}

View File

@ -1,7 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace MyWebLog.Features.Shared;
/// <summary>
/// Base class for myWebLog controllers
/// </summary>
public abstract class MyWebLogController : Controller
{
/// <summary>
@ -14,6 +18,11 @@ public abstract class MyWebLogController : Controller
/// </summary>
protected WebLogDetails WebLog => WebLogCache.Get(HttpContext);
/// <summary>
/// The ID of the currently authenticated user
/// </summary>
protected string UserId => User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "";
/// <summary>
/// Constructor
/// </summary>

View File

@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebLog.Features.Shared.TagHelpers;
/// <summary>
/// Write a Yes or No based on a boolean value
/// </summary>
public class YesNoTagHelper : TagHelper
{
/// <summary>
/// The attribute in question
/// </summary>
[HtmlAttributeName("asp-for")]
public bool For { get; set; } = false;
/// <summary>
/// Optional; if set, that value will be wrapped with &lt;strong&gt; instead of &lt;span&gt;
/// </summary>
[HtmlAttributeName("asp-strong-if")]
public bool? StrongIf { get; set; }
/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = For == StrongIf ? "strong" : "span";
output.TagMode = TagMode.StartTagAndEndTag;
output.Content.Append(For ? Resources.Yes : Resources.No);
}
}

View File

@ -4,3 +4,4 @@
@using MyWebLog.Properties
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyWebLog

View File

@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.27.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -60,6 +60,33 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Actions.
/// </summary>
public static string Actions {
get {
return ResourceManager.GetString("Actions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add a New Category.
/// </summary>
public static string AddANewCategory {
get {
return ResourceManager.GetString("AddANewCategory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add a New Page.
/// </summary>
public static string AddANewPage {
get {
return ResourceManager.GetString("AddANewPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Admin.
/// </summary>
@ -87,6 +114,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Create a New Page.
/// </summary>
public static string CreateANewPage {
get {
return ResourceManager.GetString("CreateANewPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dashboard.
/// </summary>
@ -96,6 +132,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to MMMM d, yyyy.
/// </summary>
public static string DateFormatString {
get {
return ResourceManager.GetString("DateFormatString", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Default Page.
/// </summary>
@ -114,6 +159,24 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
public static string Edit {
get {
return ResourceManager.GetString("Edit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit Page.
/// </summary>
public static string EditPage {
get {
return ResourceManager.GetString("EditPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to E-mail Address.
/// </summary>
@ -132,6 +195,24 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to In List?.
/// </summary>
public static string InListQuestion {
get {
return ResourceManager.GetString("InListQuestion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Last Updated.
/// </summary>
public static string LastUpdated {
get {
return ResourceManager.GetString("LastUpdated", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Log Off.
/// </summary>
@ -159,6 +240,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Modify Settings.
/// </summary>
public static string ModifySettings {
get {
return ResourceManager.GetString("ModifySettings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
@ -168,6 +258,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to No.
/// </summary>
public static string No {
get {
return ResourceManager.GetString("No", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pages.
/// </summary>
@ -177,6 +276,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Page Text.
/// </summary>
public static string PageText {
get {
return ResourceManager.GetString("PageText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Password.
/// </summary>
@ -186,6 +294,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Permalink.
/// </summary>
public static string Permalink {
get {
return ResourceManager.GetString("Permalink", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Posts.
/// </summary>
@ -222,6 +339,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Show in Page List.
/// </summary>
public static string ShowInPageList {
get {
return ResourceManager.GetString("ShowInPageList", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shown in Page List.
/// </summary>
@ -276,6 +402,15 @@ namespace MyWebLog.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Title.
/// </summary>
public static string Title {
get {
return ResourceManager.GetString("Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Top Level.
/// </summary>
@ -284,5 +419,41 @@ namespace MyWebLog.Properties {
return ResourceManager.GetString("TopLevel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to View All.
/// </summary>
public static string ViewAll {
get {
return ResourceManager.GetString("ViewAll", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Web Log Settings.
/// </summary>
public static string WebLogSettings {
get {
return ResourceManager.GetString("WebLogSettings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Write a New Post.
/// </summary>
public static string WriteANewPost {
get {
return ResourceManager.GetString("WriteANewPost", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Yes.
/// </summary>
public static string Yes {
get {
return ResourceManager.GetString("Yes", resourceCulture);
}
}
}
}

View File

@ -117,6 +117,15 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Actions" xml:space="preserve">
<value>Actions</value>
</data>
<data name="AddANewCategory" xml:space="preserve">
<value>Add a New Category</value>
</data>
<data name="AddANewPage" xml:space="preserve">
<value>Add a New Page</value>
</data>
<data name="Admin" xml:space="preserve">
<value>Admin</value>
</data>
@ -126,21 +135,39 @@
<data name="Categories" xml:space="preserve">
<value>Categories</value>
</data>
<data name="CreateANewPage" xml:space="preserve">
<value>Create a New Page</value>
</data>
<data name="Dashboard" xml:space="preserve">
<value>Dashboard</value>
</data>
<data name="DateFormatString" xml:space="preserve">
<value>MMMM d, yyyy</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="Edit" xml:space="preserve">
<value>Edit</value>
</data>
<data name="EditPage" xml:space="preserve">
<value>Edit Page</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="InListQuestion" xml:space="preserve">
<value>In List?</value>
</data>
<data name="LastUpdated" xml:space="preserve">
<value>Last Updated</value>
</data>
<data name="LogOff" xml:space="preserve">
<value>Log Off</value>
</data>
@ -150,15 +177,27 @@
<data name="LogOnTo" xml:space="preserve">
<value>Log On to</value>
</data>
<data name="ModifySettings" xml:space="preserve">
<value>Modify Settings</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="No" xml:space="preserve">
<value>No</value>
</data>
<data name="Pages" xml:space="preserve">
<value>Pages</value>
</data>
<data name="PageText" xml:space="preserve">
<value>Page Text</value>
</data>
<data name="Password" xml:space="preserve">
<value>Password</value>
</data>
<data name="Permalink" xml:space="preserve">
<value>Permalink</value>
</data>
<data name="Posts" xml:space="preserve">
<value>Posts</value>
</data>
@ -171,6 +210,9 @@
<data name="SaveChanges" xml:space="preserve">
<value>Save Changes</value>
</data>
<data name="ShowInPageList" xml:space="preserve">
<value>Show in Page List</value>
</data>
<data name="ShownInPageList" xml:space="preserve">
<value>Shown in Page List</value>
</data>
@ -189,7 +231,22 @@
<data name="TimeZone" xml:space="preserve">
<value>Time Zone</value>
</data>
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
<data name="TopLevel" xml:space="preserve">
<value>Top Level</value>
</data>
<data name="ViewAll" xml:space="preserve">
<value>View All</value>
</data>
<data name="WebLogSettings" xml:space="preserve">
<value>Web Log Settings</value>
</data>
<data name="WriteANewPost" xml:space="preserve">
<value>Write a New Post</value>
</data>
<data name="Yes" xml:space="preserve">
<value>Yes</value>
</data>
</root>