From d0cc9d0d7cc147dd6945894840d698898b85e829 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 1 Mar 2022 22:08:36 -0500 Subject: [PATCH] First cut at page add/edit --- .../Extensions/PageExtensions.cs | 24 +++ src/MyWebLog/Features/Admin/Index.cshtml | 18 +- src/MyWebLog/Features/Admin/Settings.cshtml | 2 +- src/MyWebLog/Features/Pages/All.cshtml | 37 ++++ src/MyWebLog/Features/Pages/Edit.cshtml | 56 ++++++ src/MyWebLog/Features/Pages/EditPageModel.cs | 99 ++++++++++ src/MyWebLog/Features/Pages/PageController.cs | 51 +++++- src/MyWebLog/Features/Pages/PageListModel.cs | 19 ++ .../Features/Shared/MyWebLogController.cs | 9 + .../Shared/TagHelpers/YesNoTagHelper.cs | 29 +++ src/MyWebLog/Features/_ViewImports.cshtml | 1 + src/MyWebLog/MyWebLog.csproj | 1 + src/MyWebLog/Properties/Resources.Designer.cs | 171 ++++++++++++++++++ src/MyWebLog/Properties/Resources.resx | 57 ++++++ 14 files changed, 558 insertions(+), 16 deletions(-) create mode 100644 src/MyWebLog/Features/Pages/All.cshtml create mode 100644 src/MyWebLog/Features/Pages/Edit.cshtml create mode 100644 src/MyWebLog/Features/Pages/EditPageModel.cs create mode 100644 src/MyWebLog/Features/Pages/PageListModel.cs create mode 100644 src/MyWebLog/Features/Shared/TagHelpers/YesNoTagHelper.cs diff --git a/src/MyWebLog.Data/Extensions/PageExtensions.cs b/src/MyWebLog.Data/Extensions/PageExtensions.cs index 235cb94..4d8c26f 100644 --- a/src/MyWebLog.Data/Extensions/PageExtensions.cs +++ b/src/MyWebLog.Data/Extensions/PageExtensions.cs @@ -33,6 +33,14 @@ public static class PageExtensions public static async Task FindById(this DbSet db, string id) => await db.SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false); + /// + /// Retrieve a page by its ID, including its revisions (non-tracked) + /// + /// The ID of the page to retrieve + /// The requested page (or null if it is not found) + public static async Task FindByIdWithRevisions(this DbSet db, string id) => + await db.Include(p => p.Revisions).SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false); + /// /// Retrieve a page by its permalink (non-tracked) /// @@ -40,4 +48,20 @@ public static class PageExtensions /// The requested page (or null if it is not found) public static async Task FindByPermalink(this DbSet db, string permalink) => await db.SingleOrDefaultAsync(p => p.Permalink == permalink).ConfigureAwait(false); + + /// + /// Retrieve a page of pages (non-tracked) + /// + /// The page number to retrieve + /// The pages + public static async Task> FindPageOfPages(this DbSet db, int pageNbr) => + await db.Skip((pageNbr - 1) * 50).Take(25).ToListAsync().ConfigureAwait(false); + + /// + /// Retrieve a page by its ID (tracked) + /// + /// The ID of the page to retrieve + /// The requested page (or null if it is not found) + public static async Task GetById(this DbSet db, string id) => + await db.AsTracking().Include(p => p.Revisions).SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false); } diff --git a/src/MyWebLog/Features/Admin/Index.cshtml b/src/MyWebLog/Features/Admin/Index.cshtml index 27fb44d..72ef232 100644 --- a/src/MyWebLog/Features/Admin/Index.cshtml +++ b/src/MyWebLog/Features/Admin/Index.cshtml @@ -13,8 +13,10 @@ @Resources.Published @Model.Posts   @Resources.Drafts @Model.Drafts - View All - Write a New Post + @Resources.ViewAll + + @Resources.WriteANewPost + @@ -26,8 +28,10 @@ @Resources.All @Model.Pages   @Resources.ShownInPageList @Model.ListedPages - View All - Create a New Page + @Resources.ViewAll + + @Resources.CreateANewPage + @@ -41,9 +45,9 @@ @Resources.All @Model.Categories   @Resources.TopLevel @Model.TopLevelCategories - View All + @Resources.ViewAll - Add a New Category + @Resources.AddANewCategory @@ -51,7 +55,7 @@ diff --git a/src/MyWebLog/Features/Admin/Settings.cshtml b/src/MyWebLog/Features/Admin/Settings.cshtml index 7ef5d76..51c19a6 100644 --- a/src/MyWebLog/Features/Admin/Settings.cshtml +++ b/src/MyWebLog/Features/Admin/Settings.cshtml @@ -1,7 +1,7 @@ @model SettingsModel @{ Layout = "_AdminLayout"; - ViewBag.Title = "Web Log Settings"; + ViewBag.Title = Resources.WebLogSettings; }
diff --git a/src/MyWebLog/Features/Pages/All.cshtml b/src/MyWebLog/Features/Pages/All.cshtml new file mode 100644 index 0000000..3d474fd --- /dev/null +++ b/src/MyWebLog/Features/Pages/All.cshtml @@ -0,0 +1,37 @@ +@model PageListModel +@{ + Layout = "_AdminLayout"; + ViewBag.Title = Resources.Pages; +} +
+ @Resources.CreateANewPage + + + + + + + + + + + @foreach (var pg in Model.Pages) + { + + + + + + + } + +
@Resources.Actions@Resources.Title@Resources.InListQuestion@Resources.LastUpdated
+ @Resources.Edit + + @pg.Title + @if (pg.Id == Model.WebLog.DefaultPage) + { +   HOME PAGE + } + @pg.UpdatedOn.ToString(Resources.DateFormatString)
+
diff --git a/src/MyWebLog/Features/Pages/Edit.cshtml b/src/MyWebLog/Features/Pages/Edit.cshtml new file mode 100644 index 0000000..bd3482b --- /dev/null +++ b/src/MyWebLog/Features/Pages/Edit.cshtml @@ -0,0 +1,56 @@ +@model EditPageModel +@{ + Layout = "_AdminLayout"; + ViewBag.Title = Model.IsNew ? Resources.AddANewPage : Resources.EditPage; +} +
+

@ViewBag.Title

+ + +
+
+
+
+ + + +
+
+
+
+
+
+ + + +
+
+
+
+ + +
+
+
+
+
+     + + + + +
+
+
+
+ +
+
+
+
+ +
+
+
+ +
diff --git a/src/MyWebLog/Features/Pages/EditPageModel.cs b/src/MyWebLog/Features/Pages/EditPageModel.cs new file mode 100644 index 0000000..5e0cd69 --- /dev/null +++ b/src/MyWebLog/Features/Pages/EditPageModel.cs @@ -0,0 +1,99 @@ +using System.ComponentModel.DataAnnotations; + +namespace MyWebLog.Features.Pages; + +/// +/// Model used to edit pages +/// +public class EditPageModel : MyWebLogModel +{ + /// + /// The ID of the page being edited + /// + public string PageId { get; set; } = "new"; + + /// + /// Whether this is a new page + /// + public bool IsNew => PageId == "new"; + + /// + /// The title of the page + /// + [Display(ResourceType = typeof(Resources), Name = "Title")] + [Required(AllowEmptyStrings = false)] + public string Title { get; set; } = ""; + + /// + /// The permalink for the page + /// + [Display(ResourceType = typeof(Resources), Name = "Permalink")] + [Required(AllowEmptyStrings = false)] + public string Permalink { get; set; } = ""; + + /// + /// Whether this page is shown in the page list + /// + [Display(ResourceType = typeof(Resources), Name = "ShowInPageList")] + public bool IsShownInPageList { get; set; } = false; + + /// + /// The source format for the text + /// + public RevisionSource Source { get; set; } = RevisionSource.Html; + + /// + /// The text of the page + /// + [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()) { } + + /// + public EditPageModel(WebLogDetails webLog) : base(webLog) { } + + /// + /// Create a model from an existing page + /// + /// The page from which the model will be created + /// The web log to which the page belongs + /// A populated model + 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 + }; + } + + /// + /// Populate a page from the values contained in this page + /// + /// The page to be populated + /// The populated page + 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; + } +} diff --git a/src/MyWebLog/Features/Pages/PageController.cs b/src/MyWebLog/Features/Pages/PageController.cs index 353b5a8..9fc26d5 100644 --- a/src/MyWebLog/Features/Pages/PageController.cs +++ b/src/MyWebLog/Features/Pages/PageController.cs @@ -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 { + /// + /// Pipeline with most extensions enabled + /// + private readonly MarkdownPipeline _pipeline = new MarkdownPipelineBuilder() + .UseSmartyPants().UseAdvancedExtensions().Build(); + /// public PageController(WebLogDbContext db) : base(db) { } [HttpGet("all")] - public async Task All() - { - await Task.CompletedTask; - throw new NotImplementedException(); - } + [HttpGet("all/page/{pageNbr:int}")] + public async Task All(int? pageNbr) => + View(new PageListModel(await Db.Pages.FindPageOfPages(pageNbr ?? 1), WebLog)); [HttpGet("{id}/edit")] public async Task 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 Save(EditPageModel model) + { + var page = model.PopulatePage(model.IsNew + ? new() + { + Id = WebLogDbContext.NewId(), + AuthorId = UserId, + PublishedOn = DateTime.UtcNow, + Revisions = new List() + } + : 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)); } } diff --git a/src/MyWebLog/Features/Pages/PageListModel.cs b/src/MyWebLog/Features/Pages/PageListModel.cs new file mode 100644 index 0000000..056d233 --- /dev/null +++ b/src/MyWebLog/Features/Pages/PageListModel.cs @@ -0,0 +1,19 @@ +namespace MyWebLog.Features.Pages; + +/// +/// View model for viewing a list of pages +/// +public class PageListModel : MyWebLogModel +{ + public IList Pages { get; init; } + + /// + /// Constructor + /// + /// The pages to display + /// The web log details + public PageListModel(IList pages, WebLogDetails webLog) : base(webLog) + { + Pages = pages; + } +} diff --git a/src/MyWebLog/Features/Shared/MyWebLogController.cs b/src/MyWebLog/Features/Shared/MyWebLogController.cs index c45d05d..f26b38b 100644 --- a/src/MyWebLog/Features/Shared/MyWebLogController.cs +++ b/src/MyWebLog/Features/Shared/MyWebLogController.cs @@ -1,7 +1,11 @@ using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; namespace MyWebLog.Features.Shared; +/// +/// Base class for myWebLog controllers +/// public abstract class MyWebLogController : Controller { /// @@ -14,6 +18,11 @@ public abstract class MyWebLogController : Controller /// protected WebLogDetails WebLog => WebLogCache.Get(HttpContext); + /// + /// The ID of the currently authenticated user + /// + protected string UserId => User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? ""; + /// /// Constructor /// diff --git a/src/MyWebLog/Features/Shared/TagHelpers/YesNoTagHelper.cs b/src/MyWebLog/Features/Shared/TagHelpers/YesNoTagHelper.cs new file mode 100644 index 0000000..945c561 --- /dev/null +++ b/src/MyWebLog/Features/Shared/TagHelpers/YesNoTagHelper.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace MyWebLog.Features.Shared.TagHelpers; + +/// +/// Write a Yes or No based on a boolean value +/// +public class YesNoTagHelper : TagHelper +{ + /// + /// The attribute in question + /// + [HtmlAttributeName("asp-for")] + public bool For { get; set; } = false; + + /// + /// Optional; if set, that value will be wrapped with <strong> instead of <span> + /// + [HtmlAttributeName("asp-strong-if")] + public bool? StrongIf { get; set; } + + /// + 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); + } +} diff --git a/src/MyWebLog/Features/_ViewImports.cshtml b/src/MyWebLog/Features/_ViewImports.cshtml index 65c93a1..5538471 100644 --- a/src/MyWebLog/Features/_ViewImports.cshtml +++ b/src/MyWebLog/Features/_ViewImports.cshtml @@ -4,3 +4,4 @@ @using MyWebLog.Properties @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, MyWebLog diff --git a/src/MyWebLog/MyWebLog.csproj b/src/MyWebLog/MyWebLog.csproj index 0f4f91a..325e63e 100644 --- a/src/MyWebLog/MyWebLog.csproj +++ b/src/MyWebLog/MyWebLog.csproj @@ -11,6 +11,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MyWebLog/Properties/Resources.Designer.cs b/src/MyWebLog/Properties/Resources.Designer.cs index 8396edf..7046e4e 100644 --- a/src/MyWebLog/Properties/Resources.Designer.cs +++ b/src/MyWebLog/Properties/Resources.Designer.cs @@ -60,6 +60,33 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to Actions. + /// + public static string Actions { + get { + return ResourceManager.GetString("Actions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add a New Category. + /// + public static string AddANewCategory { + get { + return ResourceManager.GetString("AddANewCategory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add a New Page. + /// + public static string AddANewPage { + get { + return ResourceManager.GetString("AddANewPage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Admin. /// @@ -87,6 +114,15 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to Create a New Page. + /// + public static string CreateANewPage { + get { + return ResourceManager.GetString("CreateANewPage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Dashboard. /// @@ -96,6 +132,15 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to MMMM d, yyyy. + /// + public static string DateFormatString { + get { + return ResourceManager.GetString("DateFormatString", resourceCulture); + } + } + /// /// Looks up a localized string similar to Default Page. /// @@ -114,6 +159,24 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to Edit. + /// + public static string Edit { + get { + return ResourceManager.GetString("Edit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit Page. + /// + public static string EditPage { + get { + return ResourceManager.GetString("EditPage", resourceCulture); + } + } + /// /// Looks up a localized string similar to E-mail Address. /// @@ -132,6 +195,24 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to In List?. + /// + public static string InListQuestion { + get { + return ResourceManager.GetString("InListQuestion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Last Updated. + /// + public static string LastUpdated { + get { + return ResourceManager.GetString("LastUpdated", resourceCulture); + } + } + /// /// Looks up a localized string similar to Log Off. /// @@ -159,6 +240,15 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to Modify Settings. + /// + public static string ModifySettings { + get { + return ResourceManager.GetString("ModifySettings", resourceCulture); + } + } + /// /// Looks up a localized string similar to Name. /// @@ -168,6 +258,15 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to No. + /// + public static string No { + get { + return ResourceManager.GetString("No", resourceCulture); + } + } + /// /// Looks up a localized string similar to Pages. /// @@ -177,6 +276,15 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to Page Text. + /// + public static string PageText { + get { + return ResourceManager.GetString("PageText", resourceCulture); + } + } + /// /// Looks up a localized string similar to Password. /// @@ -186,6 +294,15 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to Permalink. + /// + public static string Permalink { + get { + return ResourceManager.GetString("Permalink", resourceCulture); + } + } + /// /// Looks up a localized string similar to Posts. /// @@ -222,6 +339,15 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to Show in Page List. + /// + public static string ShowInPageList { + get { + return ResourceManager.GetString("ShowInPageList", resourceCulture); + } + } + /// /// Looks up a localized string similar to Shown in Page List. /// @@ -276,6 +402,15 @@ namespace MyWebLog.Properties { } } + /// + /// Looks up a localized string similar to Title. + /// + public static string Title { + get { + return ResourceManager.GetString("Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Top Level. /// @@ -284,5 +419,41 @@ namespace MyWebLog.Properties { return ResourceManager.GetString("TopLevel", resourceCulture); } } + + /// + /// Looks up a localized string similar to View All. + /// + public static string ViewAll { + get { + return ResourceManager.GetString("ViewAll", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Web Log Settings. + /// + public static string WebLogSettings { + get { + return ResourceManager.GetString("WebLogSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Write a New Post. + /// + public static string WriteANewPost { + get { + return ResourceManager.GetString("WriteANewPost", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yes. + /// + public static string Yes { + get { + return ResourceManager.GetString("Yes", resourceCulture); + } + } } } diff --git a/src/MyWebLog/Properties/Resources.resx b/src/MyWebLog/Properties/Resources.resx index d56cd09..1b52285 100644 --- a/src/MyWebLog/Properties/Resources.resx +++ b/src/MyWebLog/Properties/Resources.resx @@ -117,6 +117,15 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Actions + + + Add a New Category + + + Add a New Page + Admin @@ -126,21 +135,39 @@ Categories + + Create a New Page + Dashboard + + MMMM d, yyyy + Default Page Drafts + + Edit + + + Edit Page + E-mail Address First Page of Posts + + In List? + + + Last Updated + Log Off @@ -150,15 +177,27 @@ Log On to + + Modify Settings + Name + + No + Pages + + Page Text + Password + + Permalink + Posts @@ -171,6 +210,9 @@ Save Changes + + Show in Page List + Shown in Page List @@ -189,7 +231,22 @@ Time Zone + + Title + Top Level + + View All + + + Web Log Settings + + + Write a New Post + + + Yes + \ No newline at end of file