Add page edit
- Add nav link and user log-on link filter/tag - Add page list support - Add prior permalink index/search - Remove v2 C# projects
This commit is contained in:
parent
8ce2d5a2ed
commit
48e6d3edfa
1
src/MyWebLog.CS/Db/.gitignore
vendored
1
src/MyWebLog.CS/Db/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
*.db*
|
|
|
@ -1,53 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features.Admin;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controller for admin-specific displays and routes
|
|
||||||
/// </summary>
|
|
||||||
[Route("/admin")]
|
|
||||||
[Authorize]
|
|
||||||
public class AdminController : MyWebLogController
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public AdminController(WebLogDbContext db) : base(db) { }
|
|
||||||
|
|
||||||
[HttpGet("")]
|
|
||||||
public async Task<IActionResult> Index() =>
|
|
||||||
View(new DashboardModel(WebLog)
|
|
||||||
{
|
|
||||||
Posts = await Db.Posts.CountByStatus(PostStatus.Published),
|
|
||||||
Drafts = await Db.Posts.CountByStatus(PostStatus.Draft),
|
|
||||||
Pages = await Db.Pages.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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
namespace MyWebLog.Features.Admin;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The model used to display the dashboard
|
|
||||||
/// </summary>
|
|
||||||
public class DashboardModel : MyWebLogModel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The number of published posts
|
|
||||||
/// </summary>
|
|
||||||
public int Posts { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of post drafts
|
|
||||||
/// </summary>
|
|
||||||
public int Drafts { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of pages
|
|
||||||
/// </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) { }
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
@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>
|
|
||||||
@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>
|
|
||||||
@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>
|
|
||||||
@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>
|
|
|
@ -1,50 +0,0 @@
|
||||||
@model SettingsModel
|
|
||||||
@{
|
|
||||||
Layout = "_AdminLayout";
|
|
||||||
ViewBag.Title = Resources.WebLogSettings;
|
|
||||||
}
|
|
||||||
<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>
|
|
|
@ -1,76 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features.Categories;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle routes for categories
|
|
||||||
/// </summary>
|
|
||||||
[Route("/category")]
|
|
||||||
[Authorize]
|
|
||||||
public class CategoryController : MyWebLogController
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CategoryController(WebLogDbContext db) : base(db) { }
|
|
||||||
|
|
||||||
[HttpGet("all")]
|
|
||||||
public async Task<IActionResult> All()
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/edit")]
|
|
||||||
public async Task<IActionResult> Edit(string id)
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A controller model convention that identifies the feature in which a controller exists
|
|
||||||
/// </summary>
|
|
||||||
public class FeatureControllerModelConvention : IControllerModelConvention
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A cache of controller types to features
|
|
||||||
/// </summary>
|
|
||||||
private static readonly ConcurrentDictionary<string, string> _features = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Derive the feature name from the controller's type
|
|
||||||
/// </summary>
|
|
||||||
private static string? GetFeatureName(TypeInfo typ)
|
|
||||||
{
|
|
||||||
var cacheKey = typ.FullName ?? "";
|
|
||||||
if (_features.ContainsKey(cacheKey)) return _features[cacheKey];
|
|
||||||
|
|
||||||
var tokens = cacheKey.Split('.');
|
|
||||||
if (tokens.Any(it => it == "Features"))
|
|
||||||
{
|
|
||||||
var feature = tokens.SkipWhile(it => it != "Features").Skip(1).Take(1).FirstOrDefault();
|
|
||||||
if (feature is not null)
|
|
||||||
{
|
|
||||||
_features[cacheKey] = feature;
|
|
||||||
return feature;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Apply(ControllerModel controller) =>
|
|
||||||
controller.Properties.Add("feature", GetFeatureName(controller.ControllerType));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Expand the location token with the feature name
|
|
||||||
/// </summary>
|
|
||||||
public class FeatureViewLocationExpander : IViewLocationExpander
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
|
||||||
IEnumerable<string> viewLocations)
|
|
||||||
{
|
|
||||||
_ = context ?? throw new ArgumentNullException(nameof(context));
|
|
||||||
_ = viewLocations ?? throw new ArgumentNullException(nameof(viewLocations));
|
|
||||||
if (context.ActionContext.ActionDescriptor is not ControllerActionDescriptor descriptor)
|
|
||||||
throw new ArgumentException("ActionDescriptor not found");
|
|
||||||
|
|
||||||
var feature = descriptor.Properties["feature"] as string ?? "";
|
|
||||||
foreach (var location in viewLocations)
|
|
||||||
yield return location.Replace("{2}", feature);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void PopulateValues(ViewLocationExpanderContext _) { }
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
@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> </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>
|
|
|
@ -1,56 +0,0 @@
|
||||||
@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>
|
|
||||||
<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>
|
|
|
@ -1,99 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
using Markdig;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features.Pages;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle routes for pages
|
|
||||||
/// </summary>
|
|
||||||
[Route("/page")]
|
|
||||||
[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")]
|
|
||||||
[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)
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
namespace MyWebLog.Features.Pages;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The model used to render a single page
|
|
||||||
/// </summary>
|
|
||||||
public class SinglePageModel : MyWebLogModel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The page to be rendered
|
|
||||||
/// </summary>
|
|
||||||
public Page Page { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is this the home page?
|
|
||||||
/// </summary>
|
|
||||||
public bool IsHome => Page.Id == WebLog.DefaultPage;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="page">The page to be rendered</param>
|
|
||||||
/// <param name="webLog">The details for the web log</param>
|
|
||||||
public SinglePageModel(Page page, WebLogDetails webLog) : base(webLog)
|
|
||||||
{
|
|
||||||
Page = page;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
namespace MyWebLog.Features.Posts;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The model used to render multiple posts
|
|
||||||
/// </summary>
|
|
||||||
public class MultiplePostModel : MyWebLogModel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The posts to be rendered
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<Post> Posts { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="posts">The posts to be rendered</param>
|
|
||||||
/// <param name="webLog">The details for the web log</param>
|
|
||||||
public MultiplePostModel(IEnumerable<Post> posts, WebLogDetails webLog) : base(webLog)
|
|
||||||
{
|
|
||||||
Posts = posts;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using MyWebLog.Features.Pages;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features.Posts;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle post-related requests
|
|
||||||
/// </summary>
|
|
||||||
[Route("/post")]
|
|
||||||
[Authorize]
|
|
||||||
public class PostController : MyWebLogController
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public PostController(WebLogDbContext db) : base(db) { }
|
|
||||||
|
|
||||||
[HttpGet("~/")]
|
|
||||||
[AllowAnonymous]
|
|
||||||
public async Task<IActionResult> Index()
|
|
||||||
{
|
|
||||||
if (WebLog.DefaultPage == "posts") return await PageOfPosts(1);
|
|
||||||
|
|
||||||
var page = await Db.Pages.FindById(WebLog.DefaultPage);
|
|
||||||
return page is null ? NotFound() : ThemedView(page.Template ?? "SinglePage", new SinglePageModel(page, WebLog));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("~/page/{pageNbr:int}")]
|
|
||||||
[AllowAnonymous]
|
|
||||||
public async Task<IActionResult> PageOfPosts(int pageNbr) =>
|
|
||||||
ThemedView("Index",
|
|
||||||
new MultiplePostModel(await Db.Posts.FindPageOfPublishedPosts(pageNbr, WebLog.PostsPerPage), WebLog));
|
|
||||||
|
|
||||||
[HttpGet("~/{*permalink}")]
|
|
||||||
public async Task<IActionResult> CatchAll(string permalink)
|
|
||||||
{
|
|
||||||
var post = await Db.Posts.FindByPermalink(permalink);
|
|
||||||
if (post != null)
|
|
||||||
{
|
|
||||||
// TODO: return via single-post action
|
|
||||||
}
|
|
||||||
|
|
||||||
var page = await Db.Pages.FindByPermalink(permalink);
|
|
||||||
if (page != null)
|
|
||||||
{
|
|
||||||
return ThemedView(page.Template ?? "SinglePage", new SinglePageModel(page, WebLog));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOOD: search prior permalinks for posts and pages
|
|
||||||
|
|
||||||
// We tried, we really tried...
|
|
||||||
Console.Write($"Returning 404 for permalink |{permalink}|");
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("all")]
|
|
||||||
public async Task<IActionResult> All()
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/edit")]
|
|
||||||
public async Task<IActionResult> Edit(string id)
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
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>
|
|
||||||
/// The data context to use to fulfil this request
|
|
||||||
/// </summary>
|
|
||||||
protected WebLogDbContext Db { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The details for the current web log
|
|
||||||
/// </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>
|
|
||||||
/// <param name="db">The data context to use to fulfil this request</param>
|
|
||||||
protected MyWebLogController(WebLogDbContext db) : base()
|
|
||||||
{
|
|
||||||
Db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ViewResult ThemedView(string template, object model)
|
|
||||||
{
|
|
||||||
// TODO: get actual version
|
|
||||||
ViewBag.Version = "2";
|
|
||||||
return View(template, model);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
namespace MyWebLog.Features.Shared;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base model class for myWebLog views
|
|
||||||
/// </summary>
|
|
||||||
public class MyWebLogModel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The details for the web log
|
|
||||||
/// </summary>
|
|
||||||
public WebLogDetails WebLog { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="webLog">The details for the web log</param>
|
|
||||||
protected MyWebLogModel(WebLogDetails webLog)
|
|
||||||
{
|
|
||||||
WebLog = webLog;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
|
||||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features.Shared.TagHelpers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Image tag helper to load a theme's image
|
|
||||||
/// </summary>
|
|
||||||
[HtmlTargetElement("img", Attributes = "asp-theme")]
|
|
||||||
public class ImageTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.ImageTagHelper
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The theme for which the image should be loaded
|
|
||||||
/// </summary>
|
|
||||||
[HtmlAttributeName("asp-theme")]
|
|
||||||
public string Theme { get; set; } = "";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public ImageTagHelper(IFileVersionProvider fileVersionProvider, HtmlEncoder htmlEncoder,
|
|
||||||
IUrlHelperFactory urlHelperFactory)
|
|
||||||
: base(fileVersionProvider, htmlEncoder, urlHelperFactory) { }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
|
||||||
{
|
|
||||||
if (Theme == "")
|
|
||||||
{
|
|
||||||
base.Process(context, output);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
output.Attributes.SetAttribute("src", $"~/{Theme}/img/{context.AllAttributes["src"]?.Value}");
|
|
||||||
ProcessUrlAttribute("src", output);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Infrastructure;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
|
||||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features.Shared.TagHelpers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tag helper to link stylesheets for a theme
|
|
||||||
/// </summary>
|
|
||||||
[HtmlTargetElement("link", Attributes = "asp-theme")]
|
|
||||||
public class LinkTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.LinkTagHelper
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The theme for which a style sheet should be loaded
|
|
||||||
/// </summary>
|
|
||||||
[HtmlAttributeName("asp-theme")]
|
|
||||||
public string Theme { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The style sheet to be loaded (defaults to "style")
|
|
||||||
/// </summary>
|
|
||||||
[HtmlAttributeName("asp-style")]
|
|
||||||
public string Style { get; set; } = "style";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public LinkTagHelper(IWebHostEnvironment hostingEnvironment, TagHelperMemoryCacheProvider cacheProvider,
|
|
||||||
IFileVersionProvider fileVersionProvider, HtmlEncoder htmlEncoder, JavaScriptEncoder javaScriptEncoder,
|
|
||||||
IUrlHelperFactory urlHelperFactory)
|
|
||||||
: base(hostingEnvironment, cacheProvider, fileVersionProvider, htmlEncoder, javaScriptEncoder, urlHelperFactory)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
|
||||||
{
|
|
||||||
if (Theme == "")
|
|
||||||
{
|
|
||||||
base.Process(context, output);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (context.AllAttributes["rel"]?.Value.ToString())
|
|
||||||
{
|
|
||||||
case "stylesheet":
|
|
||||||
output.Attributes.SetAttribute("href", $"~/{Theme}/css/{Style}.css");
|
|
||||||
break;
|
|
||||||
case "icon":
|
|
||||||
output.Attributes.SetAttribute("type", "image/x-icon");
|
|
||||||
output.Attributes.SetAttribute("href", $"~/{Theme}/img/favicon.ico");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ProcessUrlAttribute("href", output);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
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 <strong> instead of <span>
|
|
||||||
/// </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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
@model MyWebLogModel
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>@ViewBag.Title « @Resources.Admin « @Model.WebLog.Name</title>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
|
||||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="~/css/admin.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="~/">@Model.WebLog.Name</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
|
||||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarText">
|
|
||||||
<span class="navbar-text">@ViewBag.Title</span>
|
|
||||||
@await Html.PartialAsync("_LogOnOffPartial")
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
@* Each.Messages
|
|
||||||
@Current.ToDisplay
|
|
||||||
@EndEach *@
|
|
||||||
@RenderBody()
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12 text-end"><img src="~/img/logo-light.png" alt="myWebLog"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
|
||||||
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
|
||||||
@if (User is not null && (User.Identity?.IsAuthenticated ?? false))
|
|
||||||
{
|
|
||||||
<li class="nav-item"><a class="nav-link" asp-action="Index" asp-controller="Admin">@Resources.Dashboard</a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link" asp-action="LogOff" asp-controller="User">@Resources.LogOff</a></li>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<li class="nav-item"><a class="nav-link" asp-action="LogOn" asp-controller="User">@Resources.LogOn</a></li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
|
@ -1,28 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Expand the location token with the theme path
|
|
||||||
/// </summary>
|
|
||||||
public class ThemeViewLocationExpander : IViewLocationExpander
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
|
||||||
IEnumerable<string> viewLocations)
|
|
||||||
{
|
|
||||||
_ = context ?? throw new ArgumentNullException(nameof(context));
|
|
||||||
_ = viewLocations ?? throw new ArgumentNullException(nameof(viewLocations));
|
|
||||||
|
|
||||||
foreach (var location in viewLocations)
|
|
||||||
yield return location.Replace("{3}", context.Values["theme"]!);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void PopulateValues(ViewLocationExpanderContext context)
|
|
||||||
{
|
|
||||||
_ = context ?? throw new ArgumentNullException(nameof(context));
|
|
||||||
|
|
||||||
context.Values["theme"] = WebLogCache.Get(context.ActionContext.HttpContext).ThemePath;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
@model LogOnModel
|
|
||||||
@{
|
|
||||||
Layout = "_AdminLayout";
|
|
||||||
ViewBag.Title = @Resources.LogOn;
|
|
||||||
}
|
|
||||||
<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">
|
|
||||||
<div class="col col-md-6 col-lg-4 offset-lg-2">
|
|
||||||
<div class="form-floating">
|
|
||||||
<input type="email" asp-for="EmailAddress" class="form-control" autofocus>
|
|
||||||
<label asp-for="EmailAddress"></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-6 col-lg-4">
|
|
||||||
<div class="form-floating">
|
|
||||||
<input type="password" asp-for="Password" class="form-control">
|
|
||||||
<label asp-for="Password"></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row pb-3">
|
|
||||||
<div class="col text-center">
|
|
||||||
<button type="submit" class="btn btn-primary">@Resources.LogOn</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
|
@ -1,30 +0,0 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features.Users;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The model to use to allow a user to log on
|
|
||||||
/// </summary>
|
|
||||||
public class LogOnModel : MyWebLogModel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The user's e-mail address
|
|
||||||
/// </summary>
|
|
||||||
[Required(AllowEmptyStrings = false)]
|
|
||||||
[EmailAddress]
|
|
||||||
[Display(ResourceType = typeof(Resources), Name = "EmailAddress")]
|
|
||||||
public string EmailAddress { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's password
|
|
||||||
/// </summary>
|
|
||||||
[Required(AllowEmptyStrings = false)]
|
|
||||||
[Display(ResourceType = typeof(Resources), Name = "Password")]
|
|
||||||
public string Password { get; set; } = "";
|
|
||||||
|
|
||||||
[Obsolete("Only used for model binding; use the WebLogDetails constructor")]
|
|
||||||
public LogOnModel() : base(new()) { }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public LogOnModel(WebLogDetails webLog) : base(webLog) { }
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace MyWebLog.Features.Users;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Controller for the users feature
|
|
||||||
/// </summary>
|
|
||||||
[Route("/user")]
|
|
||||||
public class UserController : MyWebLogController
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Hash a password for a given user
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="plainText">The plain-text password</param>
|
|
||||||
/// <param name="email">The user's e-mail address</param>
|
|
||||||
/// <param name="salt">The user-specific salt</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
internal static string HashedPassword(string plainText, string email, Guid salt)
|
|
||||||
{
|
|
||||||
var allSalt = salt.ToByteArray().Concat(Encoding.UTF8.GetBytes(email)).ToArray();
|
|
||||||
using Rfc2898DeriveBytes alg = new(plainText, allSalt, 2_048);
|
|
||||||
return Convert.ToBase64String(alg.GetBytes(64));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public UserController(WebLogDbContext db) : base(db) { }
|
|
||||||
|
|
||||||
[HttpGet("log-on")]
|
|
||||||
public IActionResult LogOn() =>
|
|
||||||
View(new LogOnModel(WebLog));
|
|
||||||
|
|
||||||
[HttpPost("log-on")]
|
|
||||||
public async Task<IActionResult> DoLogOn(LogOnModel model)
|
|
||||||
{
|
|
||||||
var user = await Db.Users.FindByEmail(model.EmailAddress);
|
|
||||||
|
|
||||||
if (user == null || user.PasswordHash != HashedPassword(model.Password, user.UserName, user.Salt))
|
|
||||||
{
|
|
||||||
// TODO: make error, not 404
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Claim> claims = new()
|
|
||||||
{
|
|
||||||
new(ClaimTypes.NameIdentifier, user.Id),
|
|
||||||
new(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
|
|
||||||
new(ClaimTypes.GivenName, user.PreferredName),
|
|
||||||
new(ClaimTypes.Role, user.AuthorizationLevel.ToString())
|
|
||||||
};
|
|
||||||
ClaimsIdentity identity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
|
|
||||||
await HttpContext.SignInAsync(identity.AuthenticationType, new(identity),
|
|
||||||
new() { IssuedUtc = DateTime.UtcNow });
|
|
||||||
|
|
||||||
// TODO: confirmation message
|
|
||||||
|
|
||||||
return RedirectToAction("Index", "Admin");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("log-off")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> LogOff()
|
|
||||||
{
|
|
||||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
|
|
||||||
// TODO: confirmation message
|
|
||||||
|
|
||||||
return LocalRedirect("~/");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
@namespace MyWebLog.Features
|
|
||||||
|
|
||||||
@using MyWebLog
|
|
||||||
@using MyWebLog.Properties
|
|
||||||
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
||||||
@addTagHelper *, MyWebLog
|
|
|
@ -1,3 +0,0 @@
|
||||||
global using MyWebLog.Data;
|
|
||||||
global using MyWebLog.Features.Shared;
|
|
||||||
global using MyWebLog.Properties;
|
|
|
@ -1,40 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="wwwroot\img\" />
|
|
||||||
</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>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MyWebLog.DataCS\MyWebLog.DataCS.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Properties\Resources.Designer.cs">
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Update="Properties\Resources.resx">
|
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
|
@ -1,140 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using MyWebLog;
|
|
||||||
using MyWebLog.Features;
|
|
||||||
using MyWebLog.Features.Users;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
if (args.Length > 0 && args[0] == "init")
|
|
||||||
{
|
|
||||||
await InitDb();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Services.AddMvc(opts =>
|
|
||||||
{
|
|
||||||
opts.Conventions.Add(new FeatureControllerModelConvention());
|
|
||||||
opts.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
|
|
||||||
}).AddRazorOptions(opts =>
|
|
||||||
{
|
|
||||||
opts.ViewLocationFormats.Clear();
|
|
||||||
opts.ViewLocationFormats.Add("/Themes/{3}/{0}.cshtml");
|
|
||||||
opts.ViewLocationFormats.Add("/Themes/{3}/Shared/{0}.cshtml");
|
|
||||||
opts.ViewLocationFormats.Add("/Themes/Default/{0}.cshtml");
|
|
||||||
opts.ViewLocationFormats.Add("/Themes/Default/Shared/{0}.cshtml");
|
|
||||||
opts.ViewLocationFormats.Add("/Features/{2}/{1}/{0}.cshtml");
|
|
||||||
opts.ViewLocationFormats.Add("/Features/{2}/{0}.cshtml");
|
|
||||||
opts.ViewLocationFormats.Add("/Features/Shared/{0}.cshtml");
|
|
||||||
opts.ViewLocationExpanders.Add(new FeatureViewLocationExpander());
|
|
||||||
opts.ViewLocationExpanders.Add(new ThemeViewLocationExpander());
|
|
||||||
});
|
|
||||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
||||||
.AddCookie(opts =>
|
|
||||||
{
|
|
||||||
opts.ExpireTimeSpan = TimeSpan.FromMinutes(20);
|
|
||||||
opts.SlidingExpiration = true;
|
|
||||||
opts.AccessDeniedPath = "/forbidden";
|
|
||||||
});
|
|
||||||
builder.Services.AddAuthorization();
|
|
||||||
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
|
||||||
builder.Services.AddDbContext<WebLogDbContext>(o =>
|
|
||||||
{
|
|
||||||
// TODO: can get from DI?
|
|
||||||
var db = WebLogCache.HostToDb(new HttpContextAccessor().HttpContext!);
|
|
||||||
// "empty";
|
|
||||||
o.UseSqlite($"Data Source=Db/{db}.db");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load themes
|
|
||||||
Array.ForEach(Directory.GetFiles(Directory.GetCurrentDirectory(), "MyWebLog.Themes.*.dll"),
|
|
||||||
it => { Assembly.LoadFile(it); });
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Strict });
|
|
||||||
app.UseMiddleware<WebLogMiddleware>();
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseStaticFiles();
|
|
||||||
app.UseRouting();
|
|
||||||
app.UseAuthorization();
|
|
||||||
app.UseEndpoints(endpoints => endpoints.MapControllers());
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize a new database
|
|
||||||
/// </summary>
|
|
||||||
async Task InitDb()
|
|
||||||
{
|
|
||||||
if (args.Length != 5)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var db = new WebLogDbContext(new DbContextOptionsBuilder<WebLogDbContext>()
|
|
||||||
.UseSqlite($"Data Source=Db/{args[1].Replace(':', '_')}.db").Options);
|
|
||||||
await db.Database.MigrateAsync();
|
|
||||||
|
|
||||||
// Create the admin user
|
|
||||||
var salt = Guid.NewGuid();
|
|
||||||
var user = new WebLogUser
|
|
||||||
{
|
|
||||||
Id = WebLogDbContext.NewId(),
|
|
||||||
UserName = args[3],
|
|
||||||
FirstName = "Admin",
|
|
||||||
LastName = "User",
|
|
||||||
PreferredName = "Admin",
|
|
||||||
PasswordHash = UserController.HashedPassword(args[4], args[3], salt),
|
|
||||||
Salt = salt,
|
|
||||||
AuthorizationLevel = AuthorizationLevel.Administrator
|
|
||||||
};
|
|
||||||
await db.Users.AddAsync(user);
|
|
||||||
|
|
||||||
// Create the default home page
|
|
||||||
var home = new Page
|
|
||||||
{
|
|
||||||
Id = WebLogDbContext.NewId(),
|
|
||||||
AuthorId = user.Id,
|
|
||||||
Title = "Welcome to myWebLog!",
|
|
||||||
Permalink = "welcome-to-myweblog.html",
|
|
||||||
PublishedOn = DateTime.UtcNow,
|
|
||||||
UpdatedOn = DateTime.UtcNow,
|
|
||||||
Text = "<p>This is your default home page.</p>",
|
|
||||||
Revisions = new[]
|
|
||||||
{
|
|
||||||
new PageRevision
|
|
||||||
{
|
|
||||||
Id = WebLogDbContext.NewId(),
|
|
||||||
AsOf = DateTime.UtcNow,
|
|
||||||
SourceType = RevisionSource.Html,
|
|
||||||
Text = "<p>This is your default home page.</p>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await db.Pages.AddAsync(home);
|
|
||||||
|
|
||||||
// Add the details
|
|
||||||
var timeZone = TimeZoneInfo.Local.Id;
|
|
||||||
if (!TimeZoneInfo.Local.HasIanaId)
|
|
||||||
{
|
|
||||||
timeZone = TimeZoneInfo.TryConvertWindowsIdToIanaId(timeZone, out var ianaId)
|
|
||||||
? ianaId
|
|
||||||
: throw new TimeZoneNotFoundException($"Cannot find IANA timezone for {timeZone}");
|
|
||||||
}
|
|
||||||
var details = new WebLogDetails
|
|
||||||
{
|
|
||||||
Name = args[2],
|
|
||||||
UrlBase = args[1],
|
|
||||||
DefaultPage = home.Id,
|
|
||||||
TimeZone = timeZone
|
|
||||||
};
|
|
||||||
await db.WebLogDetails.AddAsync(details);
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
Console.WriteLine($"Successfully initialized database for {args[2]} with URL base {args[1]}");
|
|
||||||
}
|
|
459
src/MyWebLog.CS/Properties/Resources.Designer.cs
generated
459
src/MyWebLog.CS/Properties/Resources.Designer.cs
generated
|
@ -1,459 +0,0 @@
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
// Runtime Version:4.0.30319.42000
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
namespace MyWebLog.Properties {
|
|
||||||
using System;
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
|
||||||
/// </summary>
|
|
||||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
|
||||||
// class via a tool like ResGen or Visual Studio.
|
|
||||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
|
||||||
// with the /str option, or rebuild your VS project.
|
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
|
||||||
public class Resources {
|
|
||||||
|
|
||||||
private static global::System.Resources.ResourceManager resourceMan;
|
|
||||||
|
|
||||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
|
||||||
|
|
||||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
|
||||||
internal Resources() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the cached ResourceManager instance used by this class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
|
||||||
public static global::System.Resources.ResourceManager ResourceManager {
|
|
||||||
get {
|
|
||||||
if (object.ReferenceEquals(resourceMan, null)) {
|
|
||||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MyWebLog.Properties.Resources", typeof(Resources).Assembly);
|
|
||||||
resourceMan = temp;
|
|
||||||
}
|
|
||||||
return resourceMan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Overrides the current thread's CurrentUICulture property for all
|
|
||||||
/// resource lookups using this strongly typed resource class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
|
||||||
public static global::System.Globalization.CultureInfo Culture {
|
|
||||||
get {
|
|
||||||
return resourceCulture;
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
resourceCulture = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
public static string Admin {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Admin", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
public static string Categories {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Categories", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
public static string Dashboard {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Dashboard", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
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 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>
|
|
||||||
public static string EmailAddress {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("EmailAddress", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 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>
|
|
||||||
public static string LogOff {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("LogOff", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Log On.
|
|
||||||
/// </summary>
|
|
||||||
public static string LogOn {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("LogOn", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 Modify Settings.
|
|
||||||
/// </summary>
|
|
||||||
public static string ModifySettings {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ModifySettings", 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 No.
|
|
||||||
/// </summary>
|
|
||||||
public static string No {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("No", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Pages.
|
|
||||||
/// </summary>
|
|
||||||
public static string Pages {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Pages", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
public static string Password {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Password", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
public static string Posts {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Posts", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 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>
|
|
||||||
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>
|
|
||||||
public static string ThereAreXCategories {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ThereAreXCategories", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to There are {0} pages.
|
|
||||||
/// </summary>
|
|
||||||
public static string ThereAreXPages {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ThereAreXPages", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to There are {0} published posts and {1} drafts.
|
|
||||||
/// </summary>
|
|
||||||
public static string ThereAreXPublishedPostsAndYDrafts {
|
|
||||||
get {
|
|
||||||
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 Title.
|
|
||||||
/// </summary>
|
|
||||||
public static string Title {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Title", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Top Level.
|
|
||||||
/// </summary>
|
|
||||||
public static string TopLevel {
|
|
||||||
get {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,252 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<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>
|
|
||||||
<data name="All" xml:space="preserve">
|
|
||||||
<value>All</value>
|
|
||||||
</data>
|
|
||||||
<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>
|
|
||||||
<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="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>
|
|
||||||
<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="ShowInPageList" xml:space="preserve">
|
|
||||||
<value>Show in Page List</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>
|
|
||||||
<data name="ThereAreXPages" xml:space="preserve">
|
|
||||||
<value>There are {0} pages</value>
|
|
||||||
</data>
|
|
||||||
<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="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>
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:3330",
|
|
||||||
"sslPort": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"MyWebLog": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"applicationUrl": "http://localhost:5010",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
<footer>
|
|
||||||
<hr>
|
|
||||||
<div class="container-fluid text-end">
|
|
||||||
<img src="~/img/logo-dark.png" alt="myWebLog">
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
|
@ -1,20 +0,0 @@
|
||||||
@model MyWebLogModel
|
|
||||||
<header>
|
|
||||||
<nav class="navbar navbar-light bg-light navbar-expand-md justify-content-start px-2">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="~/">@Model.WebLog.Name</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
|
||||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarText">
|
|
||||||
@if (Model.WebLog.Subtitle is not null)
|
|
||||||
{
|
|
||||||
<span class="navbar-text">@Model.WebLog.Subtitle</span>
|
|
||||||
}
|
|
||||||
@* TODO: list pages for current web log *@
|
|
||||||
@await Html.PartialAsync("_LogOnOffPartial")
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
|
@ -1,35 +0,0 @@
|
||||||
@model MyWebLogModel
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width">
|
|
||||||
<meta name="generator" content="myWebLog 2">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
|
||||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
|
||||||
<link asp-theme="@Model.WebLog.ThemePath" />
|
|
||||||
@await RenderSectionAsync("Style", false)
|
|
||||||
<title>@ViewBag.Title « @Model.WebLog.Name</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
@if (IsSectionDefined("Header"))
|
|
||||||
{
|
|
||||||
@await RenderSectionAsync("Header")
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@await Html.PartialAsync("_DefaultHeader", Model)
|
|
||||||
}
|
|
||||||
<main>
|
|
||||||
@RenderBody()
|
|
||||||
</main>
|
|
||||||
@if (IsSectionDefined("Footer"))
|
|
||||||
{
|
|
||||||
@await RenderSectionAsync("Footer")
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
@await Html.PartialAsync("_DefaultFooter")
|
|
||||||
}
|
|
||||||
@await RenderSectionAsync("Script", false)
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,10 +0,0 @@
|
||||||
@using MyWebLog.Features.Pages
|
|
||||||
@model SinglePageModel
|
|
||||||
@{
|
|
||||||
Layout = "_Layout";
|
|
||||||
ViewBag.Title = Model.Page.Title;
|
|
||||||
}
|
|
||||||
<h2>@Model.Page.Title</h2>
|
|
||||||
<article>
|
|
||||||
@Html.Raw(Model.Page.Text)
|
|
||||||
</article>
|
|
|
@ -1,3 +0,0 @@
|
||||||
@namespace MyWebLog.Themes
|
|
||||||
|
|
||||||
@addTagHelper *, MyWebLog
|
|
|
@ -1,91 +0,0 @@
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace MyWebLog;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In-memory cache of web log details
|
|
||||||
/// </summary>
|
|
||||||
/// <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>
|
|
||||||
public static class WebLogCache
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The cache of web log details
|
|
||||||
/// </summary>
|
|
||||||
private static readonly ConcurrentDictionary<string, WebLogDetails> _cache = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transform a hostname to a database name
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ctx">The current HTTP context</param>
|
|
||||||
/// <returns>The hostname, with an underscore replacing a colon</returns>
|
|
||||||
public static string HostToDb(HttpContext ctx) => ctx.Request.Host.ToUriComponent().Replace(':', '_');
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Does a host exist in the cache?
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="host">The host in question</param>
|
|
||||||
/// <returns>True if it exists, false if not</returns>
|
|
||||||
public static bool Exists(string host) => _cache.ContainsKey(host);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the details for a web log via its host
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="host">The host which should be retrieved</param>
|
|
||||||
/// <returns>The web log details</returns>
|
|
||||||
public static WebLogDetails Get(string host) => _cache[host];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the details for a web log via its host
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ctx">The HTTP context for the request</param>
|
|
||||||
/// <returns>The web log details</returns>
|
|
||||||
public static WebLogDetails Get(HttpContext ctx) => _cache[HostToDb(ctx)];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the details for a particular host
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="host">The host for which details should be set</param>
|
|
||||||
/// <param name="details">The details to be set</param>
|
|
||||||
public static void Set(string host, WebLogDetails details) => _cache[host] = details;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Middleware to derive the current web log
|
|
||||||
/// </summary>
|
|
||||||
public class WebLogMiddleware
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The next action in the pipeline
|
|
||||||
/// </summary>
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="next">The next action in the pipeline</param>
|
|
||||||
public WebLogMiddleware(RequestDelegate next)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
|
||||||
{
|
|
||||||
var host = WebLogCache.HostToDb(context);
|
|
||||||
|
|
||||||
if (!WebLogCache.Exists(host))
|
|
||||||
{
|
|
||||||
var db = context.RequestServices.GetRequiredService<WebLogDbContext>();
|
|
||||||
var details = await db.WebLogDetails.FindByHost(context.Request.Host.ToUriComponent());
|
|
||||||
if (details == null)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = 404;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WebLogCache.Set(host, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _next.Invoke(context);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
footer {
|
|
||||||
background-color: #808080;
|
|
||||||
border-top: solid 1px black;
|
|
||||||
color: white;
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.0 KiB |
|
@ -83,6 +83,20 @@ module Startup =
|
||||||
withRetryOnce conn
|
withRetryOnce conn
|
||||||
}
|
}
|
||||||
()
|
()
|
||||||
|
// Prior permalinks are searched when a post or page permalink do not match the current URL
|
||||||
|
match indexes |> List.contains "priorPermalinks" with
|
||||||
|
| true -> ()
|
||||||
|
| false ->
|
||||||
|
log.LogInformation($"Creating index {table}.priorPermalinks...")
|
||||||
|
let! _ =
|
||||||
|
rethink {
|
||||||
|
withTable table
|
||||||
|
indexCreate "priorPermalinks"
|
||||||
|
indexOption Multi
|
||||||
|
write
|
||||||
|
withRetryOnce conn
|
||||||
|
}
|
||||||
|
()
|
||||||
| false -> ()
|
| false -> ()
|
||||||
// Users log on with e-mail
|
// Users log on with e-mail
|
||||||
match Table.WebLogUser = table with
|
match Table.WebLogUser = table with
|
||||||
|
@ -190,17 +204,27 @@ module Page =
|
||||||
withRetryDefault
|
withRetryDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve all pages for a web log
|
/// Retrieve all pages for a web log (excludes text, prior permalinks, and revisions)
|
||||||
let findAll (webLogId : WebLogId) =
|
let findAll (webLogId : WebLogId) =
|
||||||
rethink<Page list> {
|
rethink<Page list> {
|
||||||
withTable Table.Page
|
withTable Table.Page
|
||||||
getAll [ webLogId ] (nameof webLogId)
|
getAll [ webLogId ] (nameof webLogId)
|
||||||
without [ "priorPermalinks", "revisions" ]
|
without [ "text", "priorPermalinks", "revisions" ]
|
||||||
result
|
result
|
||||||
withRetryDefault
|
withRetryDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a page by its ID
|
/// Find a page by its ID (including prior permalinks and revisions)
|
||||||
|
let findByFullId (pageId : PageId) webLogId =
|
||||||
|
rethink<Page> {
|
||||||
|
withTable Table.Page
|
||||||
|
get pageId
|
||||||
|
resultOption
|
||||||
|
withRetryDefault
|
||||||
|
}
|
||||||
|
|> verifyWebLog webLogId (fun it -> it.webLogId)
|
||||||
|
|
||||||
|
/// Find a page by its ID (excludes prior permalinks and revisions)
|
||||||
let findById (pageId : PageId) webLogId =
|
let findById (pageId : PageId) webLogId =
|
||||||
rethink<Page> {
|
rethink<Page> {
|
||||||
withTable Table.Page
|
withTable Table.Page
|
||||||
|
@ -223,15 +247,30 @@ module Page =
|
||||||
}
|
}
|
||||||
|> tryFirst
|
|> tryFirst
|
||||||
|
|
||||||
/// Find a page by its ID (including permalinks and revisions)
|
/// Find the current permalink for a page by a prior permalink
|
||||||
let findByFullId (pageId : PageId) webLogId =
|
let findCurrentPermalink (permalink : Permalink) (webLogId : WebLogId) =
|
||||||
rethink<Page> {
|
rethink<Permalink list> {
|
||||||
withTable Table.Page
|
withTable Table.Page
|
||||||
get pageId
|
getAll [ permalink ] "priorPermalinks"
|
||||||
resultOption
|
filter [ "webLogId", webLogId :> obj ]
|
||||||
|
pluck [ "permalink" ]
|
||||||
|
limit 1
|
||||||
|
result
|
||||||
|
withRetryDefault
|
||||||
|
}
|
||||||
|
|> tryFirst
|
||||||
|
|
||||||
|
/// Find all pages in the page list for the given web log
|
||||||
|
let findListed (webLogId : WebLogId) =
|
||||||
|
rethink<Page list> {
|
||||||
|
withTable Table.Page
|
||||||
|
getAll [ webLogId ] (nameof webLogId)
|
||||||
|
filter [ "showInPageList", true :> obj ]
|
||||||
|
without [ "text", "priorPermalinks", "revisions" ]
|
||||||
|
orderBy "title"
|
||||||
|
result
|
||||||
withRetryDefault
|
withRetryDefault
|
||||||
}
|
}
|
||||||
|> verifyWebLog webLogId (fun it -> it.webLogId)
|
|
||||||
|
|
||||||
/// Find a list of pages (displayed in admin area)
|
/// Find a list of pages (displayed in admin area)
|
||||||
let findPageOfPages (webLogId : WebLogId) pageNbr =
|
let findPageOfPages (webLogId : WebLogId) pageNbr =
|
||||||
|
@ -246,6 +285,25 @@ module Page =
|
||||||
withRetryDefault
|
withRetryDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a page
|
||||||
|
let update (page : Page) =
|
||||||
|
rethink {
|
||||||
|
withTable Table.Page
|
||||||
|
get page.id
|
||||||
|
update [
|
||||||
|
"title", page.title
|
||||||
|
"permalink", page.permalink
|
||||||
|
"updatedOn", page.updatedOn
|
||||||
|
"showInPageList", page.showInPageList
|
||||||
|
"text", page.text
|
||||||
|
"priorPermalinks", page.priorPermalinks
|
||||||
|
"revisions", page.revisions
|
||||||
|
]
|
||||||
|
write
|
||||||
|
withRetryDefault
|
||||||
|
ignoreResult
|
||||||
|
}
|
||||||
|
|
||||||
/// Functions to manipulate posts
|
/// Functions to manipulate posts
|
||||||
module Post =
|
module Post =
|
||||||
|
|
||||||
|
@ -272,6 +330,19 @@ module Post =
|
||||||
}
|
}
|
||||||
|> tryFirst
|
|> tryFirst
|
||||||
|
|
||||||
|
/// Find the current permalink for a post by a prior permalink
|
||||||
|
let findCurrentPermalink (permalink : Permalink) (webLogId : WebLogId) =
|
||||||
|
rethink<Permalink list> {
|
||||||
|
withTable Table.Post
|
||||||
|
getAll [ permalink ] "priorPermalinks"
|
||||||
|
filter [ "webLogId", webLogId :> obj ]
|
||||||
|
pluck [ "permalink" ]
|
||||||
|
limit 1
|
||||||
|
result
|
||||||
|
withRetryDefault
|
||||||
|
}
|
||||||
|
|> tryFirst
|
||||||
|
|
||||||
/// Find posts to be displayed on a page
|
/// Find posts to be displayed on a page
|
||||||
let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage =
|
let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage =
|
||||||
rethink<Post list> {
|
rethink<Post list> {
|
||||||
|
@ -300,7 +371,7 @@ module WebLog =
|
||||||
ignoreResult
|
ignoreResult
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve web log details 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> {
|
||||||
withTable Table.WebLog
|
withTable Table.WebLog
|
||||||
|
@ -311,6 +382,15 @@ module WebLog =
|
||||||
}
|
}
|
||||||
|> tryFirst
|
|> tryFirst
|
||||||
|
|
||||||
|
/// Retrieve a web log by its ID
|
||||||
|
let findById (webLogId : WebLogId) =
|
||||||
|
rethink<WebLog> {
|
||||||
|
withTable Table.WebLog
|
||||||
|
get webLogId
|
||||||
|
resultOption
|
||||||
|
withRetryDefault
|
||||||
|
}
|
||||||
|
|
||||||
/// Update web log settings
|
/// Update web log settings
|
||||||
let updateSettings (webLog : WebLog) =
|
let updateSettings (webLog : WebLog) =
|
||||||
rethink {
|
rethink {
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A category under which a post may be identfied
|
|
||||||
/// </summary>
|
|
||||||
public class Category
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the category
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The displayed name
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The slug (used in category URLs)
|
|
||||||
/// </summary>
|
|
||||||
public string Slug { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A longer description of the category
|
|
||||||
/// </summary>
|
|
||||||
public string? Description { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The parent ID of this category (if a subcategory)
|
|
||||||
/// </summary>
|
|
||||||
public string? ParentId { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The parent of this category (if a subcategory)
|
|
||||||
/// </summary>
|
|
||||||
public Category? Parent { get; set; } = default;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The posts assigned to this category
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<Post> Posts { get; set; } = default!;
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A comment on a post
|
|
||||||
/// </summary>
|
|
||||||
public class Comment
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the comment
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the post to which this comment applies
|
|
||||||
/// </summary>
|
|
||||||
public string PostId { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The post to which this comment applies
|
|
||||||
/// </summary>
|
|
||||||
public Post Post { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the comment to which this comment is a reply
|
|
||||||
/// </summary>
|
|
||||||
public string? InReplyToId { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The comment to which this comment is a reply
|
|
||||||
/// </summary>
|
|
||||||
public Comment? InReplyTo { get; set; } = default;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the commentor
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The e-mail address of the commentor
|
|
||||||
/// </summary>
|
|
||||||
public string Email { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The URL of the commentor's personal website
|
|
||||||
/// </summary>
|
|
||||||
public string? Url { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The status of the comment
|
|
||||||
/// </summary>
|
|
||||||
public CommentStatus Status { get; set; } = CommentStatus.Pending;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When the comment was posted
|
|
||||||
/// </summary>
|
|
||||||
public DateTime PostedOn { get; set; } = DateTime.UtcNow;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The text of the comment
|
|
||||||
/// </summary>
|
|
||||||
public string Text { get; set; } = "";
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The source format for a revision
|
|
||||||
/// </summary>
|
|
||||||
public enum RevisionSource
|
|
||||||
{
|
|
||||||
/// <summary>Markdown text</summary>
|
|
||||||
Markdown,
|
|
||||||
/// <summary>HTML</summary>
|
|
||||||
Html
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A level of authorization for a given web log
|
|
||||||
/// </summary>
|
|
||||||
public enum AuthorizationLevel
|
|
||||||
{
|
|
||||||
/// <summary>The user may administer all aspects of a web log</summary>
|
|
||||||
Administrator,
|
|
||||||
/// <summary>The user is a known user of a web log</summary>
|
|
||||||
User
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Statuses for posts
|
|
||||||
/// </summary>
|
|
||||||
public enum PostStatus
|
|
||||||
{
|
|
||||||
/// <summary>The post should not be publicly available</summary>
|
|
||||||
Draft,
|
|
||||||
/// <summary>The post is publicly viewable</summary>
|
|
||||||
Published
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Statuses for post comments
|
|
||||||
/// </summary>
|
|
||||||
public enum CommentStatus
|
|
||||||
{
|
|
||||||
/// <summary>The comment is approved</summary>
|
|
||||||
Approved,
|
|
||||||
/// <summary>The comment has yet to be approved</summary>
|
|
||||||
Pending,
|
|
||||||
/// <summary>The comment was unsolicited and unwelcome</summary>
|
|
||||||
Spam
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
public static class CategoryExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Count all categories
|
|
||||||
/// </summary>
|
|
||||||
/// <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);
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
public static class PageExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Count the number of pages
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The number of pages</returns>
|
|
||||||
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>
|
|
||||||
/// <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?> 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>
|
|
||||||
/// <param name="permalink">The permalink</param>
|
|
||||||
/// <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.OrderBy(p => p.Title).Skip((pageNbr - 1) * 25).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);
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
public static class PostExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Count the posts in the given status
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="status">The status for which posts should be counted</param>
|
|
||||||
/// <returns>A count of the posts in the given status</returns>
|
|
||||||
public static async Task<int> CountByStatus(this DbSet<Post> db, PostStatus status) =>
|
|
||||||
await db.CountAsync(p => p.Status == status).ConfigureAwait(false);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve a post by its permalink (non-tracked)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="permalink">The possible post permalink</param>
|
|
||||||
/// <returns>The post matching the permalink, or null if none is found</returns>
|
|
||||||
public static async Task<Post?> FindByPermalink(this DbSet<Post> db, string permalink) =>
|
|
||||||
await db.SingleOrDefaultAsync(p => p.Id == permalink).ConfigureAwait(false);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve a page of published posts (non-tracked)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pageNbr">The page number to retrieve</param>
|
|
||||||
/// <param name="postsPerPage">The number of posts per page</param>
|
|
||||||
/// <returns>A list of posts representing the posts for the given page</returns>
|
|
||||||
public static async Task<List<Post>> FindPageOfPublishedPosts(this DbSet<Post> db, int pageNbr, int postsPerPage) =>
|
|
||||||
await db.Where(p => p.Status == PostStatus.Published)
|
|
||||||
.Skip((pageNbr - 1) * postsPerPage).Take(postsPerPage)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
public static class WebLogDetailsExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 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);
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
public static class WebLogUserExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Find a user by their log on information (non-tracked)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="email">The user's e-mail address</param>
|
|
||||||
/// <param name="pwHash">The hash of the password provided by the user</param>
|
|
||||||
/// <returns>The user, if the credentials match; null if they do not</returns>
|
|
||||||
public static async Task<WebLogUser?> FindByEmail(this DbSet<WebLogUser> db, string email) =>
|
|
||||||
await db.SingleOrDefaultAsync(wlu => wlu.UserName == email).ConfigureAwait(false);
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,521 +0,0 @@
|
||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using MyWebLog.Data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace MyWebLog.Data.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(WebLogDbContext))]
|
|
||||||
[Migration("20220307034307_Initial")]
|
|
||||||
partial class Initial
|
|
||||||
{
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.2");
|
|
||||||
|
|
||||||
modelBuilder.Entity("CategoryPost", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("CategoriesId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PostsId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("CategoriesId", "PostsId");
|
|
||||||
|
|
||||||
b.HasIndex("PostsId");
|
|
||||||
|
|
||||||
b.ToTable("CategoryPost");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Category", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ParentId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentId");
|
|
||||||
|
|
||||||
b.HasIndex("Slug");
|
|
||||||
|
|
||||||
b.ToTable("Category");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Comment", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("InReplyToId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PostId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("PostedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("Status")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("InReplyToId");
|
|
||||||
|
|
||||||
b.HasIndex("PostId");
|
|
||||||
|
|
||||||
b.ToTable("Comment");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Page", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Permalink")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("PublishedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("ShowInPageList")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Template")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("AuthorId");
|
|
||||||
|
|
||||||
b.HasIndex("Permalink");
|
|
||||||
|
|
||||||
b.ToTable("Page");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PagePermalink", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PageId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PageId");
|
|
||||||
|
|
||||||
b.ToTable("PagePermalink");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PageRevision", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("AsOf")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PageId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("SourceType")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PageId");
|
|
||||||
|
|
||||||
b.ToTable("PageRevision");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Post", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Permalink")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("PublishedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("Status")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("AuthorId");
|
|
||||||
|
|
||||||
b.HasIndex("Permalink");
|
|
||||||
|
|
||||||
b.ToTable("Post");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PostPermalink", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PostId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PostId");
|
|
||||||
|
|
||||||
b.ToTable("PostPermalink");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PostRevision", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("AsOf")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PostId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("SourceType")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PostId");
|
|
||||||
|
|
||||||
b.ToTable("PostRevision");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Tag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("Tag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.WebLogDetails", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("DefaultPage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<byte>("PostsPerPage")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ThemePath")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TimeZone")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UrlBase")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("WebLogDetails");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.WebLogUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("AuthorizationLevel")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("FirstName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("LastName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PreferredName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid>("Salt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("WebLogUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PostTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("PostsId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TagsName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("PostsId", "TagsName");
|
|
||||||
|
|
||||||
b.HasIndex("TagsName");
|
|
||||||
|
|
||||||
b.ToTable("PostTag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CategoryPost", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Category", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CategoriesId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("MyWebLog.Data.Post", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PostsId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Category", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Category", "Parent")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentId");
|
|
||||||
|
|
||||||
b.Navigation("Parent");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Comment", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Comment", "InReplyTo")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("InReplyToId");
|
|
||||||
|
|
||||||
b.HasOne("MyWebLog.Data.Post", "Post")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PostId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("InReplyTo");
|
|
||||||
|
|
||||||
b.Navigation("Post");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Page", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.WebLogUser", "Author")
|
|
||||||
.WithMany("Pages")
|
|
||||||
.HasForeignKey("AuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Author");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PagePermalink", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Page", "Page")
|
|
||||||
.WithMany("PriorPermalinks")
|
|
||||||
.HasForeignKey("PageId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Page");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PageRevision", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Page", "Page")
|
|
||||||
.WithMany("Revisions")
|
|
||||||
.HasForeignKey("PageId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Page");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Post", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.WebLogUser", "Author")
|
|
||||||
.WithMany("Posts")
|
|
||||||
.HasForeignKey("AuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Author");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PostPermalink", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Post", "Post")
|
|
||||||
.WithMany("PriorPermalinks")
|
|
||||||
.HasForeignKey("PostId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Post");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PostRevision", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Post", "Post")
|
|
||||||
.WithMany("Revisions")
|
|
||||||
.HasForeignKey("PostId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Post");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PostTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Post", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PostsId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("MyWebLog.Data.Tag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagsName")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Page", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("PriorPermalinks");
|
|
||||||
|
|
||||||
b.Navigation("Revisions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Post", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("PriorPermalinks");
|
|
||||||
|
|
||||||
b.Navigation("Revisions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.WebLogUser", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Pages");
|
|
||||||
|
|
||||||
b.Navigation("Posts");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,399 +0,0 @@
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace MyWebLog.Data.Migrations
|
|
||||||
{
|
|
||||||
public partial class Initial : Migration
|
|
||||||
{
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Category",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
ParentId = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Category", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Category_Category_ParentId",
|
|
||||||
column: x => x.ParentId,
|
|
||||||
principalTable: "Category",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Tag",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Name = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Tag", x => x.Name);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "WebLogDetails",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Subtitle = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
DefaultPage = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PostsPerPage = table.Column<byte>(type: "INTEGER", nullable: false),
|
|
||||||
ThemePath = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UrlBase = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
TimeZone = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_WebLogDetails", x => x.Name);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "WebLogUser",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UserName = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
FirstName = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
LastName = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PreferredName = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Salt = table.Column<Guid>(type: "TEXT", nullable: false),
|
|
||||||
Url = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
AuthorizationLevel = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_WebLogUser", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Page",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
AuthorId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Permalink = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PublishedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
|
|
||||||
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
|
|
||||||
ShowInPageList = table.Column<bool>(type: "INTEGER", nullable: false),
|
|
||||||
Template = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Text = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Page", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Page_WebLogUser_AuthorId",
|
|
||||||
column: x => x.AuthorId,
|
|
||||||
principalTable: "WebLogUser",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Post",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
AuthorId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Permalink = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PublishedOn = table.Column<DateTime>(type: "TEXT", nullable: true),
|
|
||||||
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Post", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Post_WebLogUser_AuthorId",
|
|
||||||
column: x => x.AuthorId,
|
|
||||||
principalTable: "WebLogUser",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PagePermalink",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PageId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Url = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PagePermalink", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PagePermalink_Page_PageId",
|
|
||||||
column: x => x.PageId,
|
|
||||||
principalTable: "Page",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PageRevision",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PageId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
AsOf = table.Column<DateTime>(type: "TEXT", nullable: false),
|
|
||||||
SourceType = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PageRevision", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PageRevision_Page_PageId",
|
|
||||||
column: x => x.PageId,
|
|
||||||
principalTable: "Page",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "CategoryPost",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
CategoriesId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PostsId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_CategoryPost", x => new { x.CategoriesId, x.PostsId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_CategoryPost_Category_CategoriesId",
|
|
||||||
column: x => x.CategoriesId,
|
|
||||||
principalTable: "Category",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_CategoryPost_Post_PostsId",
|
|
||||||
column: x => x.PostsId,
|
|
||||||
principalTable: "Post",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Comment",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PostId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
InReplyToId = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Email = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Url = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
PostedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Comment", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Comment_Comment_InReplyToId",
|
|
||||||
column: x => x.InReplyToId,
|
|
||||||
principalTable: "Comment",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Comment_Post_PostId",
|
|
||||||
column: x => x.PostId,
|
|
||||||
principalTable: "Post",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PostPermalink",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PostId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Url = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PostPermalink", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PostPermalink_Post_PostId",
|
|
||||||
column: x => x.PostId,
|
|
||||||
principalTable: "Post",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PostRevision",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
PostId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
AsOf = table.Column<DateTime>(type: "TEXT", nullable: false),
|
|
||||||
SourceType = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PostRevision", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PostRevision_Post_PostId",
|
|
||||||
column: x => x.PostId,
|
|
||||||
principalTable: "Post",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PostTag",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
PostsId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
TagsName = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PostTag", x => new { x.PostsId, x.TagsName });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PostTag_Post_PostsId",
|
|
||||||
column: x => x.PostsId,
|
|
||||||
principalTable: "Post",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PostTag_Tag_TagsName",
|
|
||||||
column: x => x.TagsName,
|
|
||||||
principalTable: "Tag",
|
|
||||||
principalColumn: "Name",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Category_ParentId",
|
|
||||||
table: "Category",
|
|
||||||
column: "ParentId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Category_Slug",
|
|
||||||
table: "Category",
|
|
||||||
column: "Slug");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_CategoryPost_PostsId",
|
|
||||||
table: "CategoryPost",
|
|
||||||
column: "PostsId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Comment_InReplyToId",
|
|
||||||
table: "Comment",
|
|
||||||
column: "InReplyToId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Comment_PostId",
|
|
||||||
table: "Comment",
|
|
||||||
column: "PostId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Page_AuthorId",
|
|
||||||
table: "Page",
|
|
||||||
column: "AuthorId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Page_Permalink",
|
|
||||||
table: "Page",
|
|
||||||
column: "Permalink");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PagePermalink_PageId",
|
|
||||||
table: "PagePermalink",
|
|
||||||
column: "PageId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PageRevision_PageId",
|
|
||||||
table: "PageRevision",
|
|
||||||
column: "PageId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Post_AuthorId",
|
|
||||||
table: "Post",
|
|
||||||
column: "AuthorId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Post_Permalink",
|
|
||||||
table: "Post",
|
|
||||||
column: "Permalink");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PostPermalink_PostId",
|
|
||||||
table: "PostPermalink",
|
|
||||||
column: "PostId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PostRevision_PostId",
|
|
||||||
table: "PostRevision",
|
|
||||||
column: "PostId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PostTag_TagsName",
|
|
||||||
table: "PostTag",
|
|
||||||
column: "TagsName");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "CategoryPost");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Comment");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PagePermalink");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PageRevision");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PostPermalink");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PostRevision");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PostTag");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "WebLogDetails");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Category");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Page");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Post");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Tag");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "WebLogUser");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,519 +0,0 @@
|
||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using MyWebLog.Data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace MyWebLog.Data.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(WebLogDbContext))]
|
|
||||||
partial class WebLogDbContextModelSnapshot : ModelSnapshot
|
|
||||||
{
|
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.2");
|
|
||||||
|
|
||||||
modelBuilder.Entity("CategoryPost", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("CategoriesId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PostsId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("CategoriesId", "PostsId");
|
|
||||||
|
|
||||||
b.HasIndex("PostsId");
|
|
||||||
|
|
||||||
b.ToTable("CategoryPost");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Category", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ParentId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentId");
|
|
||||||
|
|
||||||
b.HasIndex("Slug");
|
|
||||||
|
|
||||||
b.ToTable("Category");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Comment", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("InReplyToId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PostId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("PostedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("Status")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("InReplyToId");
|
|
||||||
|
|
||||||
b.HasIndex("PostId");
|
|
||||||
|
|
||||||
b.ToTable("Comment");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Page", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Permalink")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("PublishedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("ShowInPageList")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Template")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("AuthorId");
|
|
||||||
|
|
||||||
b.HasIndex("Permalink");
|
|
||||||
|
|
||||||
b.ToTable("Page");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PagePermalink", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PageId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PageId");
|
|
||||||
|
|
||||||
b.ToTable("PagePermalink");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PageRevision", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("AsOf")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PageId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("SourceType")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PageId");
|
|
||||||
|
|
||||||
b.ToTable("PageRevision");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Post", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Permalink")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("PublishedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("Status")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedOn")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("AuthorId");
|
|
||||||
|
|
||||||
b.HasIndex("Permalink");
|
|
||||||
|
|
||||||
b.ToTable("Post");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PostPermalink", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PostId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PostId");
|
|
||||||
|
|
||||||
b.ToTable("PostPermalink");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PostRevision", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("AsOf")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PostId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("SourceType")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PostId");
|
|
||||||
|
|
||||||
b.ToTable("PostRevision");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Tag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("Tag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.WebLogDetails", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("DefaultPage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<byte>("PostsPerPage")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ThemePath")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TimeZone")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UrlBase")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("WebLogDetails");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.WebLogUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("AuthorizationLevel")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("FirstName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("LastName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PreferredName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid>("Salt")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("WebLogUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PostTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("PostsId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("TagsName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("PostsId", "TagsName");
|
|
||||||
|
|
||||||
b.HasIndex("TagsName");
|
|
||||||
|
|
||||||
b.ToTable("PostTag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("CategoryPost", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Category", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CategoriesId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("MyWebLog.Data.Post", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PostsId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Category", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Category", "Parent")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentId");
|
|
||||||
|
|
||||||
b.Navigation("Parent");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Comment", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Comment", "InReplyTo")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("InReplyToId");
|
|
||||||
|
|
||||||
b.HasOne("MyWebLog.Data.Post", "Post")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PostId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("InReplyTo");
|
|
||||||
|
|
||||||
b.Navigation("Post");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Page", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.WebLogUser", "Author")
|
|
||||||
.WithMany("Pages")
|
|
||||||
.HasForeignKey("AuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Author");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PagePermalink", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Page", "Page")
|
|
||||||
.WithMany("PriorPermalinks")
|
|
||||||
.HasForeignKey("PageId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Page");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PageRevision", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Page", "Page")
|
|
||||||
.WithMany("Revisions")
|
|
||||||
.HasForeignKey("PageId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Page");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Post", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.WebLogUser", "Author")
|
|
||||||
.WithMany("Posts")
|
|
||||||
.HasForeignKey("AuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Author");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PostPermalink", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Post", "Post")
|
|
||||||
.WithMany("PriorPermalinks")
|
|
||||||
.HasForeignKey("PostId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Post");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.PostRevision", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Post", "Post")
|
|
||||||
.WithMany("Revisions")
|
|
||||||
.HasForeignKey("PostId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Post");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("PostTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("MyWebLog.Data.Post", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PostsId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("MyWebLog.Data.Tag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagsName")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Page", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("PriorPermalinks");
|
|
||||||
|
|
||||||
b.Navigation("Revisions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.Post", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("PriorPermalinks");
|
|
||||||
|
|
||||||
b.Navigation("Revisions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MyWebLog.Data.WebLogUser", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Pages");
|
|
||||||
|
|
||||||
b.Navigation("Posts");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<GenerateRuntimeConfigurationFiles>True</GenerateRuntimeConfigurationFiles>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
|
@ -1,67 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A page (text not associated with a date/time)
|
|
||||||
/// </summary>
|
|
||||||
public class Page
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of this page
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the author of this page
|
|
||||||
/// </summary>
|
|
||||||
public string AuthorId { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The author of this page
|
|
||||||
/// </summary>
|
|
||||||
public WebLogUser Author { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The title of the page
|
|
||||||
/// </summary>
|
|
||||||
public string Title { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The link at which this page is displayed
|
|
||||||
/// </summary>
|
|
||||||
public string Permalink { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The instant this page was published
|
|
||||||
/// </summary>
|
|
||||||
public DateTime PublishedOn { get; set; } = DateTime.MinValue;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The instant this page was last updated
|
|
||||||
/// </summary>
|
|
||||||
public DateTime UpdatedOn { get; set; } = DateTime.MinValue;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this page shows as part of the web log's navigation
|
|
||||||
/// </summary>
|
|
||||||
public bool ShowInPageList { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The template to use when rendering this page
|
|
||||||
/// </summary>
|
|
||||||
public string? Template { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The current text of the page
|
|
||||||
/// </summary>
|
|
||||||
public string Text { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Permalinks at which this page may have been previously served (useful for migrated content)
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<PagePermalink> PriorPermalinks { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Revisions of this page
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<PageRevision> Revisions { get; set; } = default!;
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A permalink which a post or page used to have
|
|
||||||
/// </summary>
|
|
||||||
public abstract class Permalink
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of this permalink
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The link
|
|
||||||
/// </summary>
|
|
||||||
public string Url { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A prior permalink for a page
|
|
||||||
/// </summary>
|
|
||||||
public class PagePermalink : Permalink
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the page to which this permalink belongs
|
|
||||||
/// </summary>
|
|
||||||
public string PageId { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The page to which this permalink belongs
|
|
||||||
/// </summary>
|
|
||||||
public Page Page { get; set; } = default!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A prior permalink for a post
|
|
||||||
/// </summary>
|
|
||||||
public class PostPermalink : Permalink
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the post to which this permalink belongs
|
|
||||||
/// </summary>
|
|
||||||
public string PostId { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The post to which this permalink belongs
|
|
||||||
/// </summary>
|
|
||||||
public Post Post { get; set; } = default!;
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A web log post
|
|
||||||
/// </summary>
|
|
||||||
public class Post
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of this post
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the author of this post
|
|
||||||
/// </summary>
|
|
||||||
public string AuthorId { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The author of the post
|
|
||||||
/// </summary>
|
|
||||||
public WebLogUser Author { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The status
|
|
||||||
/// </summary>
|
|
||||||
public PostStatus Status { get; set; } = PostStatus.Draft;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The title
|
|
||||||
/// </summary>
|
|
||||||
public string Title { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The link at which the post resides
|
|
||||||
/// </summary>
|
|
||||||
public string Permalink { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The instant on which the post was originally published
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? PublishedOn { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The instant on which the post was last updated
|
|
||||||
/// </summary>
|
|
||||||
public DateTime UpdatedOn { get; set; } = DateTime.MinValue;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The text of the post in HTML (ready to display) format
|
|
||||||
/// </summary>
|
|
||||||
public string Text { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The Ids of the categories to which this is assigned
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<Category> Categories { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The tags for the post
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<Tag> Tags { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Permalinks at which this post may have been previously served (useful for migrated content)
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<PostPermalink> PriorPermalinks { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The revisions for this post
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<PostRevision> Revisions { get; set; } = default!;
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A revision of a page or post
|
|
||||||
/// </summary>
|
|
||||||
public abstract class Revision
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of this revision
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When this revision was saved
|
|
||||||
/// </summary>
|
|
||||||
public DateTime AsOf { get; set; } = DateTime.UtcNow;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The source language (Markdown or HTML)
|
|
||||||
/// </summary>
|
|
||||||
public RevisionSource SourceType { get; set; } = RevisionSource.Html;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The text of the revision
|
|
||||||
/// </summary>
|
|
||||||
public string Text { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A revision of a page
|
|
||||||
/// </summary>
|
|
||||||
public class PageRevision : Revision
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the page to which this revision belongs
|
|
||||||
/// </summary>
|
|
||||||
public string PageId { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The page to which this revision belongs
|
|
||||||
/// </summary>
|
|
||||||
public Page Page { get; set; } = default!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A revision of a post
|
|
||||||
/// </summary>
|
|
||||||
public class PostRevision : Revision
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the post to which this revision applies
|
|
||||||
/// </summary>
|
|
||||||
public string PostId { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The post to which this revision applies
|
|
||||||
/// </summary>
|
|
||||||
public Post Post { get; set; } = default!;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A tag
|
|
||||||
/// </summary>
|
|
||||||
public class Tag
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the tag
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The posts with this tag assigned
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<Post> Posts { get; set; } = default!;
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Data context for web log data
|
|
||||||
/// </summary>
|
|
||||||
public sealed class WebLogDbContext : DbContext
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new ID (short GUID)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new short GUID</returns>
|
|
||||||
/// <remarks>https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID</remarks>
|
|
||||||
public static string NewId() =>
|
|
||||||
Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-')[..22];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The categories for the web log
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Category> Categories { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Comments on posts
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Comment> Comments { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pages
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Page> Pages { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Web log posts
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Post> Posts { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Post tags
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<Tag> Tags { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The users of the web log
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<WebLogUser> Users { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The details for the web log
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<WebLogDetails> WebLogDetails { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">Configuration options</param>
|
|
||||||
public WebLogDbContext(DbContextOptions<WebLogDbContext> options) : base(options) { }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
||||||
{
|
|
||||||
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
// Make tables use singular names
|
|
||||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
|
||||||
entityType.SetTableName(entityType.DisplayName().Split(' ')[0]);
|
|
||||||
|
|
||||||
// Tag and WebLogDetails use Name as its ID
|
|
||||||
modelBuilder.Entity<Tag>().HasKey(t => t.Name);
|
|
||||||
modelBuilder.Entity<WebLogDetails>().HasKey(wld => wld.Name);
|
|
||||||
|
|
||||||
// Index slugs and links
|
|
||||||
modelBuilder.Entity<Category>().HasIndex(c => c.Slug);
|
|
||||||
modelBuilder.Entity<Page>().HasIndex(p => p.Permalink);
|
|
||||||
modelBuilder.Entity<Post>().HasIndex(p => p.Permalink);
|
|
||||||
|
|
||||||
// Link "author" to "user"
|
|
||||||
modelBuilder.Entity<Page>().HasOne(p => p.Author).WithMany(wbu => wbu.Pages).HasForeignKey(p => p.AuthorId);
|
|
||||||
modelBuilder.Entity<Post>().HasOne(p => p.Author).WithMany(wbu => wbu.Posts).HasForeignKey(p => p.AuthorId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The details about a web log
|
|
||||||
/// </summary>
|
|
||||||
public class WebLogDetails
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the web log
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A subtitle for the web log
|
|
||||||
/// </summary>
|
|
||||||
public string? Subtitle { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The default page ("posts" or a page Id)
|
|
||||||
/// </summary>
|
|
||||||
public string DefaultPage { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of posts to display on pages of posts
|
|
||||||
/// </summary>
|
|
||||||
public byte PostsPerPage { get; set; } = 10;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The path of the theme (within /views/themes)
|
|
||||||
/// </summary>
|
|
||||||
public string ThemePath { get; set; } = "Default";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The URL base
|
|
||||||
/// </summary>
|
|
||||||
public string UrlBase { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The time zone in which dates/times should be displayed
|
|
||||||
/// </summary>
|
|
||||||
public string TimeZone { get; set; } = "";
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
namespace MyWebLog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A user of the web log
|
|
||||||
/// </summary>
|
|
||||||
public class WebLogUser
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the user
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user name (e-mail address)
|
|
||||||
/// </summary>
|
|
||||||
public string UserName { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's first name
|
|
||||||
/// </summary>
|
|
||||||
public string FirstName { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's last name
|
|
||||||
/// </summary>
|
|
||||||
public string LastName { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's preferred name
|
|
||||||
/// </summary>
|
|
||||||
public string PreferredName { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The hash of the user's password
|
|
||||||
/// </summary>
|
|
||||||
public string PasswordHash { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Salt used to calculate the user's password hash
|
|
||||||
/// </summary>
|
|
||||||
public Guid Salt { get; set; } = Guid.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The URL of the user's personal site
|
|
||||||
/// </summary>
|
|
||||||
public string? Url { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's authorization level
|
|
||||||
/// </summary>
|
|
||||||
public AuthorizationLevel AuthorizationLevel { get; set; } = AuthorizationLevel.User;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pages written by this author
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<Page> Pages { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Posts written by this author
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<Post> Posts { get; set; } = default!;
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
open System
|
open System
|
||||||
|
|
||||||
/// A category under which a post may be identfied
|
/// A category under which a post may be identified
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Category =
|
type Category =
|
||||||
{ /// The ID of the category
|
{ /// The ID of the category
|
||||||
|
@ -120,7 +120,7 @@ type Page =
|
||||||
text : string
|
text : string
|
||||||
|
|
||||||
/// Permalinks at which this page may have been previously served (useful for migrated content)
|
/// Permalinks at which this page may have been previously served (useful for migrated content)
|
||||||
priorPermalinks : string list
|
priorPermalinks : Permalink list
|
||||||
|
|
||||||
/// Revisions of this page
|
/// Revisions of this page
|
||||||
revisions : Revision list
|
revisions : Revision list
|
||||||
|
|
|
@ -61,6 +61,14 @@ type RevisionSource =
|
||||||
/// HTML
|
/// HTML
|
||||||
| Html
|
| Html
|
||||||
|
|
||||||
|
/// Functions to support revision sources
|
||||||
|
module RevisionSource =
|
||||||
|
|
||||||
|
/// Convert a revision source to a string representation
|
||||||
|
let toString = function Markdown -> "Markdown" | Html -> "HTML"
|
||||||
|
|
||||||
|
/// Convert a string to a revision source
|
||||||
|
let ofString = function "Markdown" -> Markdown | "HTML" -> Html | x -> invalidArg "string" x
|
||||||
|
|
||||||
/// A revision of a page or post
|
/// A revision of a page or post
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
|
|
@ -1,5 +1,43 @@
|
||||||
namespace MyWebLog.ViewModels
|
namespace MyWebLog.ViewModels
|
||||||
|
|
||||||
|
open MyWebLog
|
||||||
|
open System
|
||||||
|
|
||||||
|
/// Details about a page used to display page lists
|
||||||
|
type DisplayPage =
|
||||||
|
{ /// The ID of this page
|
||||||
|
id : string
|
||||||
|
|
||||||
|
/// The title of the page
|
||||||
|
title : string
|
||||||
|
|
||||||
|
/// The link at which this page is displayed
|
||||||
|
permalink : string
|
||||||
|
|
||||||
|
/// When this page was published
|
||||||
|
publishedOn : DateTime
|
||||||
|
|
||||||
|
/// When this page was last updated
|
||||||
|
updatedOn : DateTime
|
||||||
|
|
||||||
|
/// Whether this page shows as part of the web log's navigation
|
||||||
|
showInPageList : bool
|
||||||
|
|
||||||
|
/// Is this the default page?
|
||||||
|
isDefault : bool
|
||||||
|
}
|
||||||
|
/// Create a display page from a database page
|
||||||
|
static member fromPage webLog (page : Page) =
|
||||||
|
let pageId = PageId.toString page.id
|
||||||
|
{ id = pageId
|
||||||
|
title = page.title
|
||||||
|
permalink = Permalink.toString page.permalink
|
||||||
|
publishedOn = page.publishedOn
|
||||||
|
updatedOn = page.updatedOn
|
||||||
|
showInPageList = page.showInPageList
|
||||||
|
isDefault = pageId = webLog.defaultPage
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// The model to use to allow a user to log on
|
/// The model to use to allow a user to log on
|
||||||
[<CLIMutable>]
|
[<CLIMutable>]
|
||||||
|
@ -34,6 +72,42 @@ type DashboardModel =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// View model to edit a page
|
||||||
|
[<CLIMutable>]
|
||||||
|
type EditPageModel =
|
||||||
|
{ /// The ID of the page being edited
|
||||||
|
pageId : string
|
||||||
|
|
||||||
|
/// The title of the page
|
||||||
|
title : string
|
||||||
|
|
||||||
|
/// The permalink for the page
|
||||||
|
permalink : string
|
||||||
|
|
||||||
|
/// Whether this page is shown in the page list
|
||||||
|
isShownInPageList : bool
|
||||||
|
|
||||||
|
/// The source format for the text
|
||||||
|
source : string
|
||||||
|
|
||||||
|
/// The text of the page
|
||||||
|
text : string
|
||||||
|
}
|
||||||
|
/// Create an edit model from an existing page
|
||||||
|
static member fromPage (page : Page) =
|
||||||
|
let latest =
|
||||||
|
match page.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with
|
||||||
|
| Some rev -> rev
|
||||||
|
| None -> Revision.empty
|
||||||
|
{ pageId = PageId.toString page.id
|
||||||
|
title = page.title
|
||||||
|
permalink = Permalink.toString page.permalink
|
||||||
|
isShownInPageList = page.showInPageList
|
||||||
|
source = RevisionSource.toString latest.sourceType
|
||||||
|
text = latest.text
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// View model for editing web log settings
|
/// View model for editing web log settings
|
||||||
[<CLIMutable>]
|
[<CLIMutable>]
|
||||||
type SettingsModel =
|
type SettingsModel =
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
module Program
|
|
||||||
|
|
||||||
open MyWebLog
|
|
||||||
open MyWebLog.Data.RethinkDB
|
|
||||||
open MyWebLog.Entities
|
|
||||||
open Nancy.Cryptography
|
|
||||||
open Newtonsoft.Json
|
|
||||||
open Newtonsoft.Json.Linq
|
|
||||||
open NodaTime
|
|
||||||
open RethinkDb.Driver
|
|
||||||
open System
|
|
||||||
open System.Linq
|
|
||||||
|
|
||||||
let r = RethinkDB.R
|
|
||||||
|
|
||||||
let appCfg = try AppConfig.FromJson (System.IO.File.ReadAllText "config.json")
|
|
||||||
with ex -> raise <| Exception ("Bad config.json file", ex)
|
|
||||||
let cfg = appCfg.DataConfig
|
|
||||||
// DataConfig.Connect
|
|
||||||
// (JsonConvert.DeserializeObject<DataConfig>("""{ "hostname" : "data01", "authKey" : "1d9a76f8-2d85-4033-be15-1f4313a96bb2", "database" : "myWebLog" }"""))
|
|
||||||
let conn = cfg.Conn
|
|
||||||
let toTicks (dt : DateTime) = Instant.FromDateTimeUtc(dt.ToUniversalTime()).ToUnixTimeTicks ()
|
|
||||||
/// Hash the user's password
|
|
||||||
let pbkdf2 (pw : string) =
|
|
||||||
PassphraseKeyGenerator(pw, appCfg.PasswordSalt, 4096).GetBytes 512
|
|
||||||
|> Seq.fold (fun acc byt -> sprintf "%s%s" acc (byt.ToString "x2")) ""
|
|
||||||
|
|
||||||
let migr8 () =
|
|
||||||
SetUp.startUpCheck cfg
|
|
||||||
|
|
||||||
Console.WriteLine "Migrating web logs..."
|
|
||||||
|
|
||||||
r.Db("MyWebLog").Table(Table.WebLog)
|
|
||||||
.RunCursor<JObject>(conn)
|
|
||||||
|> Seq.iter (fun x ->
|
|
||||||
r.Db("myWebLog").Table(Table.WebLog)
|
|
||||||
.Insert({ Id = string x.["id"]
|
|
||||||
Name = string x.["name"]
|
|
||||||
Subtitle = Some <| string x.["subtitle"]
|
|
||||||
DefaultPage = string x.["defaultPage"]
|
|
||||||
ThemePath = string x.["themePath"]
|
|
||||||
TimeZone = string x.["timeZone"]
|
|
||||||
UrlBase = string x.["urlBase"]
|
|
||||||
PageList = []
|
|
||||||
})
|
|
||||||
.RunResult(conn)
|
|
||||||
|> ignore)
|
|
||||||
|
|
||||||
Console.WriteLine "Migrating users..."
|
|
||||||
|
|
||||||
r.Db("MyWebLog").Table(Table.User)
|
|
||||||
.RunCursor<JObject>(conn)
|
|
||||||
|> Seq.iter (fun x ->
|
|
||||||
r.Db("myWebLog").Table(Table.User)
|
|
||||||
.Insert({ Id = string x.["id"]
|
|
||||||
UserName = string x.["userName"]
|
|
||||||
FirstName = string x.["firstName"]
|
|
||||||
LastName = string x.["lastName"]
|
|
||||||
PreferredName = string x.["preferredName"]
|
|
||||||
PasswordHash = string x.["passwordHash"]
|
|
||||||
Url = Some <| string x.["url"]
|
|
||||||
Authorizations = x.["authorizations"] :?> JArray
|
|
||||||
|> Seq.map (fun y -> { WebLogId = string y.["webLogId"]
|
|
||||||
Level = string y.["level"] })
|
|
||||||
|> Seq.toList
|
|
||||||
})
|
|
||||||
.RunResult(conn)
|
|
||||||
|> ignore)
|
|
||||||
|
|
||||||
Console.WriteLine "Migrating categories..."
|
|
||||||
|
|
||||||
r.Db("MyWebLog").Table(Table.Category)
|
|
||||||
.RunCursor<JObject>(conn)
|
|
||||||
|> Seq.iter (fun x ->
|
|
||||||
r.Db("myWebLog").Table(Table.Category)
|
|
||||||
.Insert({ Id = string x.["id"]
|
|
||||||
WebLogId = string x.["webLogId"]
|
|
||||||
Name = string x.["name"]
|
|
||||||
Slug = string x.["slug"]
|
|
||||||
Description = match String.IsNullOrEmpty(string x.["description"]) with
|
|
||||||
| true -> None
|
|
||||||
| _ -> Some <| string x.["description"]
|
|
||||||
ParentId = match String.IsNullOrEmpty(string x.["parentId"]) with
|
|
||||||
| true -> None
|
|
||||||
| _ -> Some <| string x.["parentId"]
|
|
||||||
Children = x.["children"] :?> JArray
|
|
||||||
|> Seq.map (fun y -> string y)
|
|
||||||
|> Seq.toList
|
|
||||||
})
|
|
||||||
.RunResult(conn)
|
|
||||||
|> ignore)
|
|
||||||
|
|
||||||
Console.WriteLine "Migrating comments..."
|
|
||||||
|
|
||||||
r.Db("MyWebLog").Table(Table.Comment)
|
|
||||||
.RunCursor<JObject>(conn)
|
|
||||||
|> Seq.iter (fun x ->
|
|
||||||
r.Db("myWebLog").Table(Table.Comment)
|
|
||||||
.Insert({ Id = string x.["id"]
|
|
||||||
PostId = string x.["postId"]
|
|
||||||
InReplyToId = match String.IsNullOrEmpty(string x.["inReplyToId"]) with
|
|
||||||
| true -> None
|
|
||||||
| _ -> Some <| string x.["inReplyToId"]
|
|
||||||
Name = string x.["name"]
|
|
||||||
Email = string x.["email"]
|
|
||||||
Url = match String.IsNullOrEmpty(string x.["url"]) with
|
|
||||||
| true -> None
|
|
||||||
| _ -> Some <| string x.["url"]
|
|
||||||
Status = string x.["status"]
|
|
||||||
PostedOn = x.["postedDate"].ToObject<DateTime>() |> toTicks
|
|
||||||
Text = string x.["text"]
|
|
||||||
})
|
|
||||||
.RunResult(conn)
|
|
||||||
|> ignore)
|
|
||||||
|
|
||||||
Console.WriteLine "Migrating pages..."
|
|
||||||
|
|
||||||
r.Db("MyWebLog").Table(Table.Page)
|
|
||||||
.RunCursor<JObject>(conn)
|
|
||||||
|> Seq.iter (fun x ->
|
|
||||||
r.Db("myWebLog").Table(Table.Page)
|
|
||||||
.Insert({ Id = string x.["id"]
|
|
||||||
WebLogId = string x.["webLogId"]
|
|
||||||
AuthorId = string x.["authorId"]
|
|
||||||
Title = string x.["title"]
|
|
||||||
Permalink = string x.["permalink"]
|
|
||||||
PublishedOn = x.["publishedDate"].ToObject<DateTime> () |> toTicks
|
|
||||||
UpdatedOn = x.["lastUpdatedDate"].ToObject<DateTime> () |> toTicks
|
|
||||||
ShowInPageList = x.["showInPageList"].ToObject<bool>()
|
|
||||||
Text = string x.["text"]
|
|
||||||
Revisions = [{ AsOf = x.["lastUpdatedDate"].ToObject<DateTime> () |> toTicks
|
|
||||||
SourceType = RevisionSource.HTML
|
|
||||||
Text = string x.["text"]
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
.RunResult(conn)
|
|
||||||
|> ignore)
|
|
||||||
|
|
||||||
Console.WriteLine "Migrating posts..."
|
|
||||||
|
|
||||||
r.Db("MyWebLog").Table(Table.Post)
|
|
||||||
.RunCursor<JObject>(conn)
|
|
||||||
|> Seq.iter (fun x ->
|
|
||||||
r.Db("myWebLog").Table(Table.Post)
|
|
||||||
.Insert({ Id = string x.["id"]
|
|
||||||
WebLogId = string x.["webLogId"]
|
|
||||||
AuthorId = "9b491a0f-48df-4b7b-8c10-120b5cd02895"
|
|
||||||
Status = string x.["status"]
|
|
||||||
Title = string x.["title"]
|
|
||||||
Permalink = string x.["permalink"]
|
|
||||||
PublishedOn = match x.["publishedDate"] with
|
|
||||||
| null -> int64 0
|
|
||||||
| dt -> dt.ToObject<DateTime> () |> toTicks
|
|
||||||
UpdatedOn = x.["lastUpdatedDate"].ToObject<DateTime> () |> toTicks
|
|
||||||
Revisions = [{ AsOf = x.["lastUpdatedDate"].ToObject<DateTime> ()
|
|
||||||
|> toTicks
|
|
||||||
SourceType = RevisionSource.HTML
|
|
||||||
Text = string x.["text"]
|
|
||||||
}]
|
|
||||||
Text = string x.["text"]
|
|
||||||
Tags = x.["tag"] :?> JArray
|
|
||||||
|> Seq.map (fun y -> string y)
|
|
||||||
|> Seq.toList
|
|
||||||
CategoryIds = x.["category"] :?> JArray
|
|
||||||
|> Seq.map (fun y -> string y)
|
|
||||||
|> Seq.toList
|
|
||||||
PriorPermalinks = []
|
|
||||||
Categories = []
|
|
||||||
Comments = []
|
|
||||||
})
|
|
||||||
.RunResult(conn)
|
|
||||||
|> ignore)
|
|
||||||
|
|
||||||
|
|
||||||
[<EntryPoint>]
|
|
||||||
let main argv =
|
|
||||||
migr8 ()
|
|
||||||
0 // return an integer exit code
|
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"version": "1.0.0-*",
|
|
||||||
"buildOptions": {
|
|
||||||
"debugType": "portable",
|
|
||||||
"emitEntryPoint": true,
|
|
||||||
"compilerName": "fsc",
|
|
||||||
"compile": {
|
|
||||||
"includeFiles": [
|
|
||||||
"Program.fs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"MyWebLog.App": "0.9.2",
|
|
||||||
"MyWebLog.Data.RethinkDB": "0.9.2",
|
|
||||||
"MyWebLog.Entities": "0.9.2",
|
|
||||||
"NodaTime": "2.0.0-alpha20160729"
|
|
||||||
},
|
|
||||||
"tools": {
|
|
||||||
"dotnet-compile-fsc":"1.0.0-preview2-*"
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"netcoreapp1.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.App": {
|
|
||||||
"type": "platform",
|
|
||||||
"version": "1.0.1"
|
|
||||||
},
|
|
||||||
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,12 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.Themes.BitBadger",
|
||||||
EndProject
|
EndProject
|
||||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}"
|
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.DataCS", "MyWebLog.DataCS\MyWebLog.DataCS.csproj", "{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}"
|
|
||||||
EndProject
|
|
||||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.fsproj", "{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}"
|
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.fsproj", "{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.CS", "MyWebLog.CS\MyWebLog.CS.csproj", "{B23A8093-28B1-4CB5-93F1-B4659516B74F}"
|
|
||||||
EndProject
|
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyWebLog", "MyWebLog\MyWebLog.fsproj", "{5655B63D-429F-4CCD-A14C-FBD74D987ECB}"
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyWebLog", "MyWebLog\MyWebLog.fsproj", "{5655B63D-429F-4CCD-A14C-FBD74D987ECB}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
@ -29,18 +25,10 @@ Global
|
||||||
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{B23A8093-28B1-4CB5-93F1-B4659516B74F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B23A8093-28B1-4CB5-93F1-B4659516B74F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B23A8093-28B1-4CB5-93F1-B4659516B74F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B23A8093-28B1-4CB5-93F1-B4659516B74F}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
|
74
src/MyWebLog/Caches.fs
Normal file
74
src/MyWebLog/Caches.fs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
namespace MyWebLog
|
||||||
|
|
||||||
|
open Microsoft.AspNetCore.Http
|
||||||
|
|
||||||
|
/// Helper functions for caches
|
||||||
|
module Cache =
|
||||||
|
|
||||||
|
/// Create the cache key for the web log for the current request
|
||||||
|
let makeKey (ctx : HttpContext) = ctx.Request.Host.ToUriComponent ()
|
||||||
|
|
||||||
|
|
||||||
|
open System.Collections.Concurrent
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-memory cache of web log details
|
||||||
|
/// </summary>
|
||||||
|
/// <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>
|
||||||
|
module WebLogCache =
|
||||||
|
|
||||||
|
/// The cache of web log details
|
||||||
|
let private _cache = ConcurrentDictionary<string, WebLog> ()
|
||||||
|
|
||||||
|
/// Does a host exist in the cache?
|
||||||
|
let exists ctx = _cache.ContainsKey (Cache.makeKey ctx)
|
||||||
|
|
||||||
|
/// Get the web log for the current request
|
||||||
|
let get ctx = _cache[Cache.makeKey ctx]
|
||||||
|
|
||||||
|
/// Cache the web log for a particular host
|
||||||
|
let set ctx webLog = _cache[Cache.makeKey ctx] <- webLog
|
||||||
|
|
||||||
|
|
||||||
|
/// A cache of page information needed to display the page list in templates
|
||||||
|
module PageListCache =
|
||||||
|
|
||||||
|
open Microsoft.Extensions.DependencyInjection
|
||||||
|
open MyWebLog.ViewModels
|
||||||
|
open RethinkDb.Driver.Net
|
||||||
|
|
||||||
|
/// Cache of displayed pages
|
||||||
|
let private _cache = ConcurrentDictionary<string, DisplayPage[]> ()
|
||||||
|
|
||||||
|
/// Get the pages for the web log for this request
|
||||||
|
let get ctx = _cache[Cache.makeKey ctx]
|
||||||
|
|
||||||
|
/// Update the pages for the current web log
|
||||||
|
let update ctx = task {
|
||||||
|
let webLog = WebLogCache.get ctx
|
||||||
|
let conn = ctx.RequestServices.GetRequiredService<IConnection> ()
|
||||||
|
let! pages = Data.Page.findListed webLog.id conn
|
||||||
|
_cache[Cache.makeKey ctx] <- pages |> List.map (DisplayPage.fromPage webLog) |> Array.ofList
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cache for parsed templates
|
||||||
|
module TemplateCache =
|
||||||
|
|
||||||
|
open DotLiquid
|
||||||
|
open System.IO
|
||||||
|
|
||||||
|
/// Cache of parsed templates
|
||||||
|
let private _cache = ConcurrentDictionary<string, Template> ()
|
||||||
|
|
||||||
|
/// Get a template for the given theme and template nate
|
||||||
|
let get (theme : string) (templateName : string) = task {
|
||||||
|
let templatePath = $"themes/{theme}/{templateName}"
|
||||||
|
match _cache.ContainsKey templatePath with
|
||||||
|
| true -> ()
|
||||||
|
| false ->
|
||||||
|
let! file = File.ReadAllTextAsync $"{templatePath}.liquid"
|
||||||
|
_cache[templatePath] <- Template.Parse (file, SyntaxCompatibility.DotLiquid22)
|
||||||
|
return _cache[templatePath]
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module MyWebLog.Handlers
|
module MyWebLog.Handlers
|
||||||
|
|
||||||
open System.Collections.Generic
|
|
||||||
open DotLiquid
|
open DotLiquid
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
|
@ -26,8 +25,7 @@ module Error =
|
||||||
>=> text ex.Message *)
|
>=> text ex.Message *)
|
||||||
|
|
||||||
/// 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 =
|
let notAuthorized : HttpHandler = fun next ctx ->
|
||||||
fun next ctx ->
|
|
||||||
(next, ctx)
|
(next, ctx)
|
||||||
||> match ctx.Request.Method with
|
||> match ctx.Request.Method with
|
||||||
| "GET" -> redirectTo false $"/user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}"
|
| "GET" -> redirectTo false $"/user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}"
|
||||||
|
@ -41,42 +39,27 @@ module Error =
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module private Helpers =
|
module private Helpers =
|
||||||
|
|
||||||
|
open Markdig
|
||||||
open Microsoft.AspNetCore.Antiforgery
|
open Microsoft.AspNetCore.Antiforgery
|
||||||
open Microsoft.Extensions.DependencyInjection
|
open Microsoft.Extensions.DependencyInjection
|
||||||
open System.Collections.Concurrent
|
open System.Security.Claims
|
||||||
open System.IO
|
|
||||||
|
|
||||||
/// Cache for parsed templates
|
|
||||||
module private TemplateCache =
|
|
||||||
|
|
||||||
/// Cache of parsed templates
|
|
||||||
let private views = ConcurrentDictionary<string, Template> ()
|
|
||||||
|
|
||||||
/// Get a template for the given web log
|
|
||||||
let get (theme : string) (templateName : string) = task {
|
|
||||||
let templatePath = $"themes/{theme}/{templateName}"
|
|
||||||
match views.ContainsKey templatePath with
|
|
||||||
| true -> ()
|
|
||||||
| false ->
|
|
||||||
let! file = File.ReadAllTextAsync $"{templatePath}.liquid"
|
|
||||||
views[templatePath] <- Template.Parse (file, SyntaxCompatibility.DotLiquid22)
|
|
||||||
return views[templatePath]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 deriveWebLogFromHash (hash : Hash) ctx =
|
let private deriveWebLogFromHash (hash : Hash) ctx =
|
||||||
match hash.ContainsKey "web_log" with
|
match hash.ContainsKey "web_log" with
|
||||||
| true -> hash["web_log"] :?> WebLog
|
| true -> hash["web_log"] :?> WebLog
|
||||||
| false ->
|
| false ->
|
||||||
let wl = WebLogCache.getByCtx ctx
|
let wl = WebLogCache.get ctx
|
||||||
hash.Add ("web_log", wl)
|
hash.Add ("web_log", wl)
|
||||||
wl
|
wl
|
||||||
|
|
||||||
/// Render a view for the specified theme, using the specified template, layout, and hash
|
/// Render a view for the specified theme, using the specified template, layout, and hash
|
||||||
let viewForTheme theme template layout next ctx = fun (hash : Hash) -> task {
|
let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||||
// Don't need the web log, but this adds it to the hash if the function is called directly
|
// Don't need the web log, but this adds it to the hash if the function is called directly
|
||||||
let _ = deriveWebLogFromHash hash ctx
|
let _ = deriveWebLogFromHash hash ctx
|
||||||
hash.Add ("logged_on", ctx.User.Identity.IsAuthenticated)
|
hash.Add ("logged_on", ctx.User.Identity.IsAuthenticated)
|
||||||
|
hash.Add ("page_list", PageListCache.get ctx)
|
||||||
|
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
||||||
|
|
||||||
// NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a two-pass
|
// NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a two-pass
|
||||||
// render; the net effect is a "layout" capability similar to Razor or Pug
|
// render; the net effect is a "layout" capability similar to Razor or Pug
|
||||||
|
@ -86,20 +69,26 @@ module private Helpers =
|
||||||
hash.Add ("content", contentTemplate.Render hash)
|
hash.Add ("content", contentTemplate.Render hash)
|
||||||
|
|
||||||
// ...then render that content with its layout
|
// ...then render that content with its layout
|
||||||
let! layoutTemplate = TemplateCache.get theme (defaultArg layout "layout")
|
let! layoutTemplate = TemplateCache.get theme "layout"
|
||||||
return! htmlString (layoutTemplate.Render hash) next ctx
|
return! htmlString (layoutTemplate.Render hash) next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a view for the web log's default theme
|
/// Return a view for the web log's default theme
|
||||||
let themedView template layout next ctx = fun (hash : Hash) -> task {
|
let themedView template next ctx = fun (hash : Hash) -> task {
|
||||||
return! viewForTheme (deriveWebLogFromHash hash ctx).themePath template layout next ctx hash
|
return! viewForTheme (deriveWebLogFromHash hash ctx).themePath template next ctx hash
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The web log ID for the current request
|
/// Get the web log ID for the current request
|
||||||
let webLogId ctx = (WebLogCache.getByCtx ctx).id
|
let webLogId ctx = (WebLogCache.get ctx).id
|
||||||
|
|
||||||
|
/// Get the user ID for the current request
|
||||||
|
let userId (ctx : HttpContext) =
|
||||||
|
WebLogUserId (ctx.User.Claims |> Seq.find (fun c -> c.Type = ClaimTypes.NameIdentifier)).Value
|
||||||
|
|
||||||
|
/// Get the RethinkDB connection
|
||||||
let conn (ctx : HttpContext) = ctx.RequestServices.GetRequiredService<IConnection> ()
|
let conn (ctx : HttpContext) = ctx.RequestServices.GetRequiredService<IConnection> ()
|
||||||
|
|
||||||
|
/// Get the Anti-CSRF service
|
||||||
let private antiForgery (ctx : HttpContext) = ctx.RequestServices.GetRequiredService<IAntiforgery> ()
|
let private antiForgery (ctx : HttpContext) = ctx.RequestServices.GetRequiredService<IAntiforgery> ()
|
||||||
|
|
||||||
/// Get the cross-site request forgery token set
|
/// Get the cross-site request forgery token set
|
||||||
|
@ -116,15 +105,25 @@ module private Helpers =
|
||||||
/// Require a user to be logged on
|
/// Require a user to be logged on
|
||||||
let requireUser = requiresAuthentication Error.notAuthorized
|
let requireUser = requiresAuthentication Error.notAuthorized
|
||||||
|
|
||||||
|
/// Pipeline with most extensions enabled
|
||||||
|
let mdPipeline =
|
||||||
|
MarkdownPipelineBuilder().UseSmartyPants().UseAdvancedExtensions().Build ()
|
||||||
|
|
||||||
|
/// Get the HTML representation of the text of a revision
|
||||||
|
let revisionToHtml (rev : Revision) =
|
||||||
|
match rev.sourceType with Html -> rev.text | Markdown -> Markdown.ToHtml (rev.text, mdPipeline)
|
||||||
|
|
||||||
|
|
||||||
|
open System.Collections.Generic
|
||||||
|
|
||||||
/// Handlers to manipulate admin functions
|
/// Handlers to manipulate admin functions
|
||||||
module Admin =
|
module Admin =
|
||||||
|
|
||||||
// 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 webLogId = webLogId 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 webLogId 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
|
||||||
|
@ -143,12 +142,12 @@ module Admin =
|
||||||
topLevelCategories = topCats
|
topLevelCategories = topCats
|
||||||
}
|
}
|
||||||
|}
|
|}
|
||||||
|> viewForTheme "admin" "dashboard" None next ctx
|
|> viewForTheme "admin" "dashboard" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.getByCtx ctx
|
let webLog = WebLogCache.get 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
|
||||||
|
@ -170,14 +169,14 @@ module Admin =
|
||||||
web_log = webLog
|
web_log = webLog
|
||||||
page_title = "Web Log Settings"
|
page_title = "Web Log Settings"
|
||||||
|}
|
|}
|
||||||
|> viewForTheme "admin" "settings" None next ctx
|
|> viewForTheme "admin" "settings" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 conn = conn ctx
|
||||||
let! model = ctx.BindFormAsync<SettingsModel> ()
|
let! model = ctx.BindFormAsync<SettingsModel> ()
|
||||||
match! Data.WebLog.findByHost (WebLogCache.getByCtx ctx).urlBase conn' with
|
match! Data.WebLog.findById (WebLogCache.get ctx).id conn with
|
||||||
| Some webLog ->
|
| Some webLog ->
|
||||||
let updated =
|
let updated =
|
||||||
{ webLog with
|
{ webLog with
|
||||||
|
@ -187,14 +186,102 @@ module Admin =
|
||||||
postsPerPage = model.postsPerPage
|
postsPerPage = model.postsPerPage
|
||||||
timeZone = model.timeZone
|
timeZone = model.timeZone
|
||||||
}
|
}
|
||||||
do! Data.WebLog.updateSettings updated conn'
|
do! Data.WebLog.updateSettings updated conn
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
WebLogCache.set updated.urlBase updated
|
WebLogCache.set ctx updated
|
||||||
|
|
||||||
// TODO: confirmation message
|
// TODO: confirmation message
|
||||||
|
|
||||||
return! redirectTo false "/admin/" next ctx
|
return! redirectTo false "/admin" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Handlers to manipulate pages
|
||||||
|
module Page =
|
||||||
|
|
||||||
|
// GET /pages
|
||||||
|
// GET /pages/page/{pageNbr}
|
||||||
|
let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
|
let webLog = WebLogCache.get ctx
|
||||||
|
let! pages = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx)
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject
|
||||||
|
{| pages = pages |> List.map (DisplayPage.fromPage webLog)
|
||||||
|
page_title = "Pages"
|
||||||
|
|}
|
||||||
|
|> viewForTheme "admin" "page-list" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /page/{id}/edit
|
||||||
|
let edit pgId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
|
let! hash = task {
|
||||||
|
match pgId with
|
||||||
|
| "new" ->
|
||||||
|
return
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
csrf = csrfToken ctx
|
||||||
|
model = EditPageModel.fromPage { Page.empty with id = PageId "new" }
|
||||||
|
page_title = "Add a New Page"
|
||||||
|
|} |> Some
|
||||||
|
| _ ->
|
||||||
|
match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with
|
||||||
|
| Some page ->
|
||||||
|
return
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
csrf = csrfToken ctx
|
||||||
|
model = EditPageModel.fromPage page
|
||||||
|
page_title = "Edit Page"
|
||||||
|
|} |> Some
|
||||||
|
| None -> return None
|
||||||
|
}
|
||||||
|
match hash with
|
||||||
|
| Some h -> return! viewForTheme "admin" "page-edit" next ctx h
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /page/{id}/edit
|
||||||
|
let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
||||||
|
let! model = ctx.BindFormAsync<EditPageModel> ()
|
||||||
|
let webLogId = webLogId ctx
|
||||||
|
let conn = conn ctx
|
||||||
|
let now = DateTime.UtcNow
|
||||||
|
let! pg = task {
|
||||||
|
match model.pageId with
|
||||||
|
| "new" ->
|
||||||
|
return Some
|
||||||
|
{ Page.empty with
|
||||||
|
id = PageId.create ()
|
||||||
|
webLogId = webLogId
|
||||||
|
authorId = userId ctx
|
||||||
|
publishedOn = now
|
||||||
|
}
|
||||||
|
| pgId -> return! Data.Page.findByFullId (PageId pgId) webLogId conn
|
||||||
|
}
|
||||||
|
match pg with
|
||||||
|
| Some page ->
|
||||||
|
let updateList = page.showInPageList <> model.isShownInPageList
|
||||||
|
let revision = { asOf = now; sourceType = RevisionSource.ofString model.source; text = model.text }
|
||||||
|
// Detect a permalink change, and add the prior one to the prior list
|
||||||
|
let page =
|
||||||
|
match Permalink.toString page.permalink with
|
||||||
|
| "" -> page
|
||||||
|
| link when link = model.permalink -> page
|
||||||
|
| _ -> { page with priorPermalinks = page.permalink :: page.priorPermalinks }
|
||||||
|
let page =
|
||||||
|
{ page with
|
||||||
|
title = model.title
|
||||||
|
permalink = Permalink model.permalink
|
||||||
|
updatedOn = now
|
||||||
|
showInPageList = model.isShownInPageList
|
||||||
|
text = revisionToHtml revision
|
||||||
|
revisions = revision :: page.revisions
|
||||||
|
}
|
||||||
|
do! (match model.pageId with "new" -> Data.Page.add | _ -> Data.Page.update) page conn
|
||||||
|
if updateList then do! PageListCache.update ctx
|
||||||
|
// TODO: confirmation
|
||||||
|
return! redirectTo false $"/page/{PageId.toString page.id}/edit" next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +291,7 @@ module Post =
|
||||||
|
|
||||||
// GET /page/{pageNbr}
|
// GET /page/{pageNbr}
|
||||||
let pageOfPosts (pageNbr : int) : HttpHandler = fun next ctx -> task {
|
let pageOfPosts (pageNbr : int) : HttpHandler = fun next ctx -> task {
|
||||||
let webLog = WebLogCache.getByCtx ctx
|
let webLog = WebLogCache.get ctx
|
||||||
let! posts = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage (conn ctx)
|
let! posts = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage (conn ctx)
|
||||||
let hash = Hash.FromAnonymousObject {| posts = posts |}
|
let hash = Hash.FromAnonymousObject {| posts = posts |}
|
||||||
let title =
|
let title =
|
||||||
|
@ -213,12 +300,12 @@ module Post =
|
||||||
| _, "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 -> ()
|
||||||
return! themedView "index" None next ctx hash
|
return! themedView "index" next ctx hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /
|
// GET /
|
||||||
let home : HttpHandler = fun next ctx -> task {
|
let home : HttpHandler = fun next ctx -> task {
|
||||||
let webLog = WebLogCache.getByCtx ctx
|
let webLog = WebLogCache.get ctx
|
||||||
match webLog.defaultPage with
|
match webLog.defaultPage with
|
||||||
| "posts" -> return! pageOfPosts 1 next ctx
|
| "posts" -> return! pageOfPosts 1 next ctx
|
||||||
| pageId ->
|
| pageId ->
|
||||||
|
@ -226,29 +313,36 @@ module Post =
|
||||||
| Some page ->
|
| Some page ->
|
||||||
return!
|
return!
|
||||||
Hash.FromAnonymousObject {| page = page; page_title = page.title |}
|
Hash.FromAnonymousObject {| page = page; page_title = page.title |}
|
||||||
|> themedView "single-page" page.template next ctx
|
|> themedView (defaultArg page.template "single-page") next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET *
|
// GET {**link}
|
||||||
let catchAll (link : string) : HttpHandler = fun next ctx -> task {
|
let catchAll : HttpHandler = fun next ctx -> task {
|
||||||
let webLog = WebLogCache.getByCtx ctx
|
let webLog = WebLogCache.get ctx
|
||||||
let conn' = conn ctx
|
let conn = conn ctx
|
||||||
let permalink = Permalink link
|
let permalink = (string >> Permalink) ctx.Request.RouteValues["link"]
|
||||||
match! Data.Post.findByPermalink permalink webLog.id conn' with
|
// Current post
|
||||||
| Some post -> return! Error.notFound next ctx
|
match! Data.Post.findByPermalink permalink webLog.id conn with
|
||||||
|
| Some _ -> return! Error.notFound next ctx
|
||||||
// TODO: return via single-post action
|
// TODO: return via single-post action
|
||||||
| None ->
|
| None ->
|
||||||
match! Data.Page.findByPermalink permalink webLog.id conn' with
|
// Current page
|
||||||
|
match! Data.Page.findByPermalink permalink webLog.id conn with
|
||||||
| Some page ->
|
| Some page ->
|
||||||
return!
|
return!
|
||||||
Hash.FromAnonymousObject {| page = page; page_title = page.title |}
|
Hash.FromAnonymousObject {| page = page; page_title = page.title |}
|
||||||
|> themedView "single-page" page.template next ctx
|
|> themedView (defaultArg page.template "single-page") next ctx
|
||||||
| None ->
|
| None ->
|
||||||
|
// Prior post
|
||||||
// TOOD: search prior permalinks for posts and pages
|
match! Data.Post.findCurrentPermalink permalink webLog.id conn with
|
||||||
|
| Some link -> return! redirectTo true $"/{Permalink.toString link}" next ctx
|
||||||
// We tried, we really tried...
|
| None ->
|
||||||
|
// Prior page
|
||||||
|
match! Data.Page.findCurrentPermalink permalink webLog.id conn with
|
||||||
|
| Some link -> return! redirectTo true $"/{Permalink.toString link}" next ctx
|
||||||
|
| None ->
|
||||||
|
// We tried, we really did...
|
||||||
Console.Write($"Returning 404 for permalink |{permalink}|");
|
Console.Write($"Returning 404 for permalink |{permalink}|");
|
||||||
return! Error.notFound next ctx
|
return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
@ -265,15 +359,15 @@ module User =
|
||||||
|
|
||||||
/// Hash a password for a given user
|
/// Hash a password for a given user
|
||||||
let hashedPassword (plainText : string) (email : string) (salt : Guid) =
|
let hashedPassword (plainText : string) (email : string) (salt : Guid) =
|
||||||
let allSalt = Array.concat [ salt.ToByteArray(); (Encoding.UTF8.GetBytes email) ]
|
let allSalt = Array.concat [ salt.ToByteArray (); Encoding.UTF8.GetBytes email ]
|
||||||
use alg = new Rfc2898DeriveBytes (plainText, allSalt, 2_048)
|
use alg = new Rfc2898DeriveBytes (plainText, allSalt, 2_048)
|
||||||
Convert.ToBase64String(alg.GetBytes(64))
|
Convert.ToBase64String (alg.GetBytes 64)
|
||||||
|
|
||||||
// GET /user/log-on
|
// GET /user/log-on
|
||||||
let logOn : HttpHandler = fun next ctx -> task {
|
let logOn : HttpHandler = fun next ctx -> task {
|
||||||
return!
|
return!
|
||||||
Hash.FromAnonymousObject {| page_title = "Log On"; csrf = (csrfToken ctx) |}
|
Hash.FromAnonymousObject {| page_title = "Log On"; csrf = (csrfToken ctx) |}
|
||||||
|> viewForTheme "admin" "log-on" None next ctx
|
|> viewForTheme "admin" "log-on" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /user/log-on
|
// POST /user/log-on
|
||||||
|
@ -294,7 +388,7 @@ module User =
|
||||||
|
|
||||||
// TODO: confirmation message
|
// TODO: confirmation message
|
||||||
|
|
||||||
return! redirectTo false "/admin/" next ctx
|
return! redirectTo false "/admin" next ctx
|
||||||
| _ ->
|
| _ ->
|
||||||
// TODO: make error, not 404
|
// TODO: make error, not 404
|
||||||
return! Error.notFound next ctx
|
return! Error.notFound next ctx
|
||||||
|
@ -318,7 +412,7 @@ let endpoints = [
|
||||||
]
|
]
|
||||||
subRoute "/admin" [
|
subRoute "/admin" [
|
||||||
GET [
|
GET [
|
||||||
route "/" Admin.dashboard
|
route "" Admin.dashboard
|
||||||
route "/settings" Admin.settings
|
route "/settings" Admin.settings
|
||||||
]
|
]
|
||||||
POST [
|
POST [
|
||||||
|
@ -328,6 +422,12 @@ let endpoints = [
|
||||||
subRoute "/page" [
|
subRoute "/page" [
|
||||||
GET [
|
GET [
|
||||||
routef "/%d" Post.pageOfPosts
|
routef "/%d" Post.pageOfPosts
|
||||||
|
routef "/%s/edit" Page.edit
|
||||||
|
route "s" (Page.all 1)
|
||||||
|
routef "s/page/%d" Page.all
|
||||||
|
]
|
||||||
|
POST [
|
||||||
|
route "/save" Page.save
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
subRoute "/user" [
|
subRoute "/user" [
|
||||||
|
@ -339,4 +439,5 @@ let endpoints = [
|
||||||
route "/log-on" User.doLogOn
|
route "/log-on" User.doLogOn
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
route "{**link}" Post.catchAll
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<NoWarn>3391</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="appsettings.json" CopyToOutputDirectory="Always" />
|
<Content Include="appsettings.json" CopyToOutputDirectory="Always" />
|
||||||
<Compile Include="WebLogCache.fs" />
|
<Compile Include="Caches.fs" />
|
||||||
<Compile Include="Handlers.fs" />
|
<Compile Include="Handlers.fs" />
|
||||||
<Compile Include="Program.fs" />
|
<Compile Include="Program.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DotLiquid" Version="2.2.610" />
|
<PackageReference Include="DotLiquid" Version="2.2.610" />
|
||||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Markdig" Version="0.28.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -9,19 +9,57 @@ open System
|
||||||
type WebLogMiddleware (next : RequestDelegate) =
|
type WebLogMiddleware (next : RequestDelegate) =
|
||||||
|
|
||||||
member this.InvokeAsync (ctx : HttpContext) = task {
|
member this.InvokeAsync (ctx : HttpContext) = task {
|
||||||
let host = ctx.Request.Host.ToUriComponent ()
|
match WebLogCache.exists ctx with
|
||||||
match WebLogCache.exists host with
|
|
||||||
| true -> return! next.Invoke ctx
|
| true -> return! next.Invoke ctx
|
||||||
| false ->
|
| false ->
|
||||||
let conn = ctx.RequestServices.GetRequiredService<IConnection> ()
|
let conn = ctx.RequestServices.GetRequiredService<IConnection> ()
|
||||||
match! Data.WebLog.findByHost host conn with
|
match! Data.WebLog.findByHost (Cache.makeKey ctx) conn with
|
||||||
| Some webLog ->
|
| Some webLog ->
|
||||||
WebLogCache.set host webLog
|
WebLogCache.set ctx webLog
|
||||||
|
do! PageListCache.update ctx
|
||||||
return! next.Invoke ctx
|
return! next.Invoke ctx
|
||||||
| None -> ctx.Response.StatusCode <- 404
|
| None -> ctx.Response.StatusCode <- 404
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// DotLiquid filters
|
||||||
|
module DotLiquidBespoke =
|
||||||
|
|
||||||
|
open DotLiquid
|
||||||
|
open System.IO
|
||||||
|
|
||||||
|
/// 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 (+) ""
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
|
||||||
|
|
||||||
/// Initialize a new database
|
/// Initialize a new database
|
||||||
let initDbValidated (args : string[]) (sp : IServiceProvider) = task {
|
let initDbValidated (args : string[]) (sp : IServiceProvider) = task {
|
||||||
|
|
||||||
|
@ -140,14 +178,20 @@ let main args =
|
||||||
let _ = builder.Services.AddSingleton<IConnection> conn
|
let _ = builder.Services.AddSingleton<IConnection> conn
|
||||||
|
|
||||||
// Set up DotLiquid
|
// Set up DotLiquid
|
||||||
|
Template.RegisterFilter typeof<DotLiquidBespoke.NavLinkFilter>
|
||||||
|
Template.RegisterTag<DotLiquidBespoke.UserLinksTag> "user_links"
|
||||||
|
|
||||||
let all = [| "*" |]
|
let all = [| "*" |]
|
||||||
Template.RegisterSafeType (typeof<Page>, all)
|
Template.RegisterSafeType (typeof<Page>, all)
|
||||||
Template.RegisterSafeType (typeof<WebLog>, all)
|
Template.RegisterSafeType (typeof<WebLog>, all)
|
||||||
|
|
||||||
Template.RegisterSafeType (typeof<DashboardModel>, all)
|
Template.RegisterSafeType (typeof<DashboardModel>, all)
|
||||||
|
Template.RegisterSafeType (typeof<DisplayPage>, all)
|
||||||
Template.RegisterSafeType (typeof<SettingsModel>, all)
|
Template.RegisterSafeType (typeof<SettingsModel>, all)
|
||||||
|
Template.RegisterSafeType (typeof<EditPageModel>, all)
|
||||||
|
|
||||||
Template.RegisterSafeType (typeof<AntiforgeryTokenSet>, all)
|
Template.RegisterSafeType (typeof<AntiforgeryTokenSet>, all)
|
||||||
Template.RegisterSafeType (typeof<Option<_>>, all) // doesn't quite get the job done....
|
Template.RegisterSafeType (typeof<string option>, all)
|
||||||
Template.RegisterSafeType (typeof<KeyValuePair>, all)
|
Template.RegisterSafeType (typeof<KeyValuePair>, all)
|
||||||
|
|
||||||
let app = builder.Build ()
|
let app = builder.Build ()
|
||||||
|
@ -160,7 +204,7 @@ let main args =
|
||||||
let _ = app.UseAuthentication ()
|
let _ = app.UseAuthentication ()
|
||||||
let _ = app.UseStaticFiles ()
|
let _ = app.UseStaticFiles ()
|
||||||
let _ = app.UseRouting ()
|
let _ = app.UseRouting ()
|
||||||
let _ = app.UseEndpoints (fun endpoints -> endpoints.MapGiraffeEndpoints Handlers.endpoints)
|
let _ = app.UseGiraffe Handlers.endpoints
|
||||||
|
|
||||||
app.Run()
|
app.Run()
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
/// <summary>
|
|
||||||
/// In-memory cache of web log details
|
|
||||||
/// </summary>
|
|
||||||
/// <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>
|
|
||||||
module MyWebLog.WebLogCache
|
|
||||||
|
|
||||||
open Microsoft.AspNetCore.Http
|
|
||||||
open System.Collections.Concurrent
|
|
||||||
|
|
||||||
/// The cache of web log details
|
|
||||||
let private _cache = ConcurrentDictionary<string, WebLog> ()
|
|
||||||
|
|
||||||
/// Does a host exist in the cache?
|
|
||||||
let exists host = _cache.ContainsKey host
|
|
||||||
|
|
||||||
/// Get the details for a web log via its host
|
|
||||||
let get host = _cache[host]
|
|
||||||
|
|
||||||
/// Get the details for a web log via its host
|
|
||||||
let getByCtx (ctx : HttpContext) = _cache[ctx.Request.Host.ToUriComponent ()]
|
|
||||||
|
|
||||||
/// Set the details for a particular host
|
|
||||||
let set host details = _cache[host] <- details
|
|
|
@ -1,4 +1,5 @@
|
||||||
<article class="container pt-3">
|
<h2 class="py-3">{{ web_log.name }} • Dashboard</h2>
|
||||||
|
<article class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<section class="col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3">
|
<section class="col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -21,7 +22,7 @@
|
||||||
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="/pages/list" class="btn btn-secondary me-2">View All</a>
|
<a href="/pages" class="btn btn-secondary me-2">View All</a>
|
||||||
<a href="/page/new/edit" class="btn btn-primary">Create a New Page</a>
|
<a href="/page/new/edit" class="btn btn-primary">Create a New Page</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,20 +17,26 @@
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarText">
|
<div class="collapse navbar-collapse" id="navbarText">
|
||||||
<span class="navbar-text">{{ page_title }}</span>
|
{% if logged_on -%}
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{{ "admin" | nav_link: "Dashboard" }}
|
||||||
|
{{ "pages" | nav_link: "Pages" }}
|
||||||
|
{{ "posts" | nav_link: "Posts" }}
|
||||||
|
{{ "categories" | nav_link: "Categories" }}
|
||||||
|
</ul>
|
||||||
|
{%- endif %}
|
||||||
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
||||||
{% if logged_on -%}
|
{% if logged_on -%}
|
||||||
<li class="nav-item"><a class="nav-link" href="/admin/">Dashboard</a></li>
|
{{ "user/log-off" | nav_link: "Log Off" }}
|
||||||
<li class="nav-item"><a class="nav-link" href="/user/log-off">Log Off</a></li>
|
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
<li class="nav-item"><a class="nav-link" href="/user/log-on">Log On</a></li>
|
{{ "user/log-on" | nav_link: "Log On" }}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main class="mx-3">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<h2 class="p-3 ">Log On to {{ web_log.name }}</h2>
|
<h2 class="py-3">Log On to {{ web_log.name }}</h2>
|
||||||
<article class="pb-3">
|
<article class="pb-3">
|
||||||
<form action="/user/log-on" method="post">
|
<form action="/user/log-on" 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 }}">
|
||||||
|
|
55
src/MyWebLog/themes/admin/page-edit.liquid
Normal file
55
src/MyWebLog/themes/admin/page-edit.liquid
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<h2 class="py-3">{{ page_title }}</h2>
|
||||||
|
<article>
|
||||||
|
<form action="/page/save" method="post">
|
||||||
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
|
<input type="hidden" name="pageId" value="{{ model.page_id }}">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" name="title" id="title" class="form-control" autofocus required
|
||||||
|
value="{{ model.title }}">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-9">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" name="permalink" id="permalink" class="form-control" required
|
||||||
|
value="{{ model.permalink }}">
|
||||||
|
<label for="permalink">Permalink</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 align-self-center">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" name="isShownInPageList" id="showList" class="form-check-input" value="true"
|
||||||
|
{%- if model.is_shown_in_page_list %} checked="checked"{% endif %}>
|
||||||
|
<label for="showList" class="form-check-label">Show in Page List</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col">
|
||||||
|
<label for="text">Text</label>
|
||||||
|
<input type="radio" name="source" id="source_html" class="btn-check" value="HTML"
|
||||||
|
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
|
||||||
|
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
|
||||||
|
<input type="radio" name="source" id="source_md" class="btn-check" value="Markdown"
|
||||||
|
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
|
||||||
|
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<textarea name="Text" id="text" class="form-control" rows="10">{{ model.text }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</article>
|
27
src/MyWebLog/themes/admin/page-list.liquid
Normal file
27
src/MyWebLog/themes/admin/page-list.liquid
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<h2 class="py-3">{{ page_title }}</h2>
|
||||||
|
<article class="container">
|
||||||
|
<a href="/page/new/edit" class="btn btn-primary btn-sm my-3">Create a New Page</a>
|
||||||
|
<table class="table table-sm table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
|
<th scope="col">Title</th>
|
||||||
|
<th scope="col">In List?</th>
|
||||||
|
<th scope="col">Last Updated</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for pg in pages -%}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/page/{{ pg.id }}/edit">Edit</a></td>
|
||||||
|
<td>
|
||||||
|
{{ pg.title }}
|
||||||
|
{%- if pg.is_default %} <span class="badge bg-success">HOME PAGE</span>{% endif -%}
|
||||||
|
</td>
|
||||||
|
<td>{% if pg.show_in_page_list %} Yes {% else %} No {% endif %}</td>
|
||||||
|
<td>{{ pg.updated_on | date: "MMMM d, yyyy" }}</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
|
@ -1,4 +1,5 @@
|
||||||
<article class="pt-3">
|
<h2 class="py-3">{{ web_log.name }} Settings</h2>
|
||||||
|
<article>
|
||||||
<form action="/admin/settings" method="post">
|
<form action="/admin/settings" 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">
|
||||||
|
|
|
@ -20,21 +20,21 @@
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarText">
|
<div class="collapse navbar-collapse" id="navbarText">
|
||||||
{% if web_log.subtitle -%}
|
{% if web_log.subtitle -%}
|
||||||
<span class="navbar-text">{{ web_log.subtitle | escape }}</span>
|
<span class="navbar-text">{{ web_log.subtitle.value | escape }}</span>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
{% if page_list -%}
|
||||||
{% if logged_on %}
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item"><a class="nav-link" href="/admin/">Dashboard</a></li>
|
{% for pg in page_list -%}
|
||||||
<li class="nav-item"><a class="nav-link" href="/user/log-off">Log Off</a></li>
|
{{ pg.permalink | nav_link: pg.title }}
|
||||||
{% else %}
|
{%- endfor %}
|
||||||
<li class="nav-item"><a class="nav-link" href="/user/log-on">Log On</a></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
|
{%- endif %}
|
||||||
|
{% user_links %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main class="mx-3">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<h2>{{ page.title }}</h2>
|
<h2 class="py-3">{{ page.title }}</h2>
|
||||||
<article>
|
<article>
|
||||||
{{ page.text }}
|
{{ page.text }}
|
||||||
</article>
|
</article>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user