diff --git a/samples/Opw.PineBlog.Sample/DatabaseSeed.cs b/samples/Opw.PineBlog.Sample/DatabaseSeed.cs new file mode 100644 index 0000000..8b9b048 --- /dev/null +++ b/samples/Opw.PineBlog.Sample/DatabaseSeed.cs @@ -0,0 +1,85 @@ +using Opw.PineBlog.Entities; +using Opw.PineBlog.EntityFrameworkCore; +using System; +using System.Linq; +using WaffleGenerator; + +namespace Opw.PineBlog.Sample +{ + internal class DatabaseSeed + { + private readonly BlogEntityDbContext _dbContext; + + public DatabaseSeed(BlogEntityDbContext context) + { + _dbContext = context; + } + + public void Run() + { + if (DateTime.UtcNow.Day % 2 == 0) + CreateAuthor("John Smith", "images/avatar-male.png"); + else + CreateAuthor("Mary Smith", "images/avatar-female.png"); + + CreateBlogPosts(); + } + + void CreateAuthor(string name, string imagePath) + { + if (_dbContext.Authors.Count() > 0) return; + + var email = "pineblog@example.com"; + if (_dbContext.Authors.Count(a => a.UserName.Equals(email)) > 0) return; + + _dbContext.Authors.Add(new Author + { + UserName = email, + Email = email, + DisplayName = name, + Avatar = imagePath, + Bio = WaffleEngine.Text(1, false), + }); + + _dbContext.SaveChanges(); + } + + void CreateBlogPosts() + { + if (_dbContext.Posts.Count() > 0) return; + + var author = _dbContext.Authors.Single(); + + for(int i = 0; i < 5; i++) + { + var title = WaffleEngine.Title(); + var post = new Post + { + AuthorId = author.Id, + Title = title, + Slug = title.ToSlug(), + Description = WaffleEngine.Text(1, false), + Published = DateTime.UtcNow.AddDays(-i * 10) + }; + + if (i % 2 == 0) + { + post.CoverUrl = "/images/woods.gif"; + post.CoverCaption = "Battle background for the Misty Woods in the game Shadows of Adam by Tim Wendorf"; + post.CoverLink = "http://pixeljoint.com/pixelart/94359.htm"; + post.Content = $"## {WaffleEngine.Text(1, true)} {WaffleEngine.Text(1, false)} \n``` csharp\npublic class {{\n var myVar = \"Some value\";\n}}\n```\n ## {WaffleEngine.Text(1, true)} {WaffleEngine.Text(2, false)}"; + post.Categories = "csharp,waffle,random"; + } + else + { + post.Content = $"## {WaffleEngine.Text(1, true)} {WaffleEngine.Text(1, false)} ### {WaffleEngine.Text(1, true)} \n``` yaml\nYAML: YAML Ain't Markup Language\n```\n ## {WaffleEngine.Text(1, true)} {WaffleEngine.Text(2, false)}"; + post.Categories = "yaml,waffle,random"; + } + + _dbContext.Posts.Add(post); + } + + _dbContext.SaveChanges(); + } + } +} diff --git a/samples/Opw.PineBlog.Sample/Opw.PineBlog.Sample.csproj b/samples/Opw.PineBlog.Sample/Opw.PineBlog.Sample.csproj index 4318056..4e656b5 100644 --- a/samples/Opw.PineBlog.Sample/Opw.PineBlog.Sample.csproj +++ b/samples/Opw.PineBlog.Sample/Opw.PineBlog.Sample.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.2 @@ -8,6 +8,7 @@ + diff --git a/src/Opw.PineBlog.EntityFrameworkCore/ServiceProviderExtensions.cs b/samples/Opw.PineBlog.Sample/ServiceProviderExtensions.cs similarity index 91% rename from src/Opw.PineBlog.EntityFrameworkCore/ServiceProviderExtensions.cs rename to samples/Opw.PineBlog.Sample/ServiceProviderExtensions.cs index d2e5af1..4d20c68 100644 --- a/src/Opw.PineBlog.EntityFrameworkCore/ServiceProviderExtensions.cs +++ b/samples/Opw.PineBlog.Sample/ServiceProviderExtensions.cs @@ -2,8 +2,9 @@ using System; using Microsoft.Extensions.DependencyInjection; using Opw.EntityFrameworkCore; +using Opw.PineBlog.EntityFrameworkCore; -namespace Opw.PineBlog.EntityFrameworkCore +namespace Opw.PineBlog.Sample { public static class ServiceProviderExtensions { diff --git a/samples/Opw.PineBlog.Sample/appsettings.json b/samples/Opw.PineBlog.Sample/appsettings.json index 55148fe..d822d27 100644 --- a/samples/Opw.PineBlog.Sample/appsettings.json +++ b/samples/Opw.PineBlog.Sample/appsettings.json @@ -5,14 +5,14 @@ }, "PineBlogOptions": { "Title": "PineBlog", - "CoverUrl": "images/woods.gif", + "CoverUrl": "/images/woods.gif", "CoverCaption": "Battle background for the Misty Woods in the game Shadows of Adam by Tim Wendorf", "CoverLink": "http://pixeljoint.com/pixelart/94359.htm", "ItemsPerPage": 2, "CreateAndSeedDatabases": true, "AzureStorageConnectionString": "UseDevelopmentStorage=true", "AzureStorageBlobContainerName": "pineblog", - "CdnUrl": "http://127.0.0.1:10000/devstoreaccount1", + "FileBaseUrl": "http://127.0.0.1:10000/devstoreaccount1", "CoverImagesPath": "covers" }, "Logging": { diff --git a/src/Opw.PineBlog.Abstractions/PineBlogOptions.cs b/src/Opw.PineBlog.Abstractions/PineBlogOptions.cs index 89fc87c..e191416 100644 --- a/src/Opw.PineBlog.Abstractions/PineBlogOptions.cs +++ b/src/Opw.PineBlog.Abstractions/PineBlogOptions.cs @@ -28,10 +28,10 @@ public class PineBlogOptions public string CoverLink { get; set; } /// - /// The URL of the CDN where the images and other files are accessible. - /// Can also be the web host or a local host name if no CDN is used. + /// The URL of the location where the images and other files are stored. + /// Can be the web host, a CDN or a local host. /// - public string CdnUrl { get; set; } + public string FileBaseUrl { get; set; } /// /// The path for the folder where the cover images are stored. diff --git a/src/Opw.PineBlog.Core/Files/FileUrlHelper.cs b/src/Opw.PineBlog.Core/Files/FileUrlHelper.cs index 311ef52..7baddc7 100644 --- a/src/Opw.PineBlog.Core/Files/FileUrlHelper.cs +++ b/src/Opw.PineBlog.Core/Files/FileUrlHelper.cs @@ -43,7 +43,7 @@ public string ReplaceUrlFormatWithBaseUrl(string s) /// public string GetBaseUrl() { - var url = _blogOptions.Value.CdnUrl; + var url = _blogOptions.Value.FileBaseUrl; if (!string.IsNullOrWhiteSpace(_blogOptions.Value.AzureStorageBlobContainerName)) url += "/" + _blogOptions.Value.AzureStorageBlobContainerName; diff --git a/src/Opw.PineBlog.Core/Posts/AddPostCommand.cs b/src/Opw.PineBlog.Core/Posts/AddPostCommand.cs index 9f19d17..d69aa8f 100644 --- a/src/Opw.PineBlog.Core/Posts/AddPostCommand.cs +++ b/src/Opw.PineBlog.Core/Posts/AddPostCommand.cs @@ -23,11 +23,6 @@ public class AddPostCommand : IRequest>, IEditPostCommand /// public string Title { get; set; } - /// - /// The slug for this post, until the post is published a temporary slug will be used. - /// - public string Slug { get; set; } = DateTime.UtcNow.Ticks.ToString(); - /// /// A short description for the post. /// @@ -97,7 +92,7 @@ public async Task> Handle(AddPostCommand request, CancellationToken { AuthorId = author.Id, Title = request.Title, - Slug = request.Slug, + Slug = request.Title.ToSlug(), Description = request.Description, Content = request.Content, Categories = request.Categories, diff --git a/src/Opw.PineBlog.Core/Posts/EditPostCommandValidator.cs b/src/Opw.PineBlog.Core/Posts/EditPostCommandValidator.cs index 357e07e..7814585 100644 --- a/src/Opw.PineBlog.Core/Posts/EditPostCommandValidator.cs +++ b/src/Opw.PineBlog.Core/Posts/EditPostCommandValidator.cs @@ -1,5 +1,4 @@ using FluentValidation; -using Opw.FluentValidation; namespace Opw.PineBlog.Posts { @@ -15,7 +14,6 @@ public class EditPostCommandValidator : AbstractValidator public EditPostCommandValidator() { RuleFor(c => c.Title).MaximumLength(160).NotEmpty(); - RuleFor(c => c.Slug).IsSlug(); RuleFor(c => c.Description).MaximumLength(450); RuleFor(c => c.Categories).MaximumLength(2000); RuleFor(c => c.Content).NotEmpty(); diff --git a/src/Opw.PineBlog.Core/Posts/IEditPostCommand.cs b/src/Opw.PineBlog.Core/Posts/IEditPostCommand.cs index 1129491..8a03f37 100644 --- a/src/Opw.PineBlog.Core/Posts/IEditPostCommand.cs +++ b/src/Opw.PineBlog.Core/Posts/IEditPostCommand.cs @@ -12,11 +12,6 @@ public interface IEditPostCommand /// string Title { get; set; } - /// - /// The slug for this post. - /// - string Slug { get; set; } - /// /// A short description for the post. /// diff --git a/src/Opw.PineBlog.Core/Posts/PublishPostCommand.cs b/src/Opw.PineBlog.Core/Posts/PublishPostCommand.cs new file mode 100644 index 0000000..ac06a63 --- /dev/null +++ b/src/Opw.PineBlog.Core/Posts/PublishPostCommand.cs @@ -0,0 +1,59 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Opw.HttpExceptions; +using Opw.PineBlog.Entities; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Opw.PineBlog.Posts +{ + /// + /// Command that publishes a post. + /// + public class PublishPostCommand : IRequest> + { + /// + /// The post id. + /// + public Guid Id { get; set; } + + /// + /// Handler for the PublishPostCommand. + /// + public class Handler : IRequestHandler> + { + private readonly IBlogEntityDbContext _context; + + /// + /// Implementation of PublishPostCommand.Handler. + /// + /// The blog entity context. + public Handler(IBlogEntityDbContext context) + { + _context = context; + } + + /// + /// Handle the PublishPostCommand request. + /// + /// The PublishPostCommand request. + /// A cancellation token. + public async Task> Handle(PublishPostCommand request, CancellationToken cancellationToken) + { + var entity = await _context.Posts.SingleOrDefaultAsync(e => e.Id.Equals(request.Id)); + if (entity == null) + return Result.Fail(new NotFoundException($"Could not find post, id: \"{request.Id}\"")); + + entity.Published = DateTime.UtcNow; + + _context.Posts.Update(entity); + var result = await _context.SaveChangesAsync(true, cancellationToken); + if (!result.IsSuccess) + return Result.Fail(result.Exception); + + return Result.Success(entity); + } + } + } +} diff --git a/src/Opw.PineBlog.Core/Posts/PublishPostCommandValidator.cs b/src/Opw.PineBlog.Core/Posts/PublishPostCommandValidator.cs new file mode 100644 index 0000000..692a149 --- /dev/null +++ b/src/Opw.PineBlog.Core/Posts/PublishPostCommandValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using Opw.FluentValidation; + +namespace Opw.PineBlog.Posts +{ + /// + /// Validator for the PublishPostCommand request. + /// + public class PublishPostCommandValidator : AbstractValidator + { + /// + /// Implementation of PublishPostCommandValidator. + /// + public PublishPostCommandValidator() + { + RuleFor(c => c.Id).IsRequiredGuid(); + } + } +} diff --git a/src/Opw.PineBlog.Core/Posts/UnpublishPostCommand.cs b/src/Opw.PineBlog.Core/Posts/UnpublishPostCommand.cs new file mode 100644 index 0000000..676f36e --- /dev/null +++ b/src/Opw.PineBlog.Core/Posts/UnpublishPostCommand.cs @@ -0,0 +1,59 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Opw.HttpExceptions; +using Opw.PineBlog.Entities; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Opw.PineBlog.Posts +{ + /// + /// Command that unpublishes a post. + /// + public class UnpublishPostCommand : IRequest> + { + /// + /// The post id. + /// + public Guid Id { get; set; } + + /// + /// Handler for the UnpublishPostCommand. + /// + public class Handler : IRequestHandler> + { + private readonly IBlogEntityDbContext _context; + + /// + /// Implementation of UnpublishPostCommand.Handler. + /// + /// The blog entity context. + public Handler(IBlogEntityDbContext context) + { + _context = context; + } + + /// + /// Handle the UnpublishPostCommand request. + /// + /// The UnpublishPostCommand request. + /// A cancellation token. + public async Task> Handle(UnpublishPostCommand request, CancellationToken cancellationToken) + { + var entity = await _context.Posts.SingleOrDefaultAsync(e => e.Id.Equals(request.Id)); + if (entity == null) + return Result.Fail(new NotFoundException($"Could not find post, id: \"{request.Id}\"")); + + entity.Published = null; + + _context.Posts.Update(entity); + var result = await _context.SaveChangesAsync(true, cancellationToken); + if (!result.IsSuccess) + return Result.Fail(result.Exception); + + return Result.Success(entity); + } + } + } +} diff --git a/src/Opw.PineBlog.Core/Posts/UnpublishPostCommandValidator.cs b/src/Opw.PineBlog.Core/Posts/UnpublishPostCommandValidator.cs new file mode 100644 index 0000000..fccf574 --- /dev/null +++ b/src/Opw.PineBlog.Core/Posts/UnpublishPostCommandValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using Opw.FluentValidation; + +namespace Opw.PineBlog.Posts +{ + /// + /// Validator for the UnpublishPostCommand request. + /// + public class UnpublishPostCommandValidator : AbstractValidator + { + /// + /// Implementation of UnpublishPostCommandValidator. + /// + public UnpublishPostCommandValidator() + { + RuleFor(c => c.Id).IsRequiredGuid(); + } + } +} diff --git a/src/Opw.PineBlog.Core/Posts/UpdatePostCommand.cs b/src/Opw.PineBlog.Core/Posts/UpdatePostCommand.cs index 1cedd57..09e3ac7 100644 --- a/src/Opw.PineBlog.Core/Posts/UpdatePostCommand.cs +++ b/src/Opw.PineBlog.Core/Posts/UpdatePostCommand.cs @@ -24,11 +24,6 @@ public class UpdatePostCommand : IRequest>, IEditPostCommand /// public string Title { get; set; } - /// - /// The slug for this post, until the post is published a temporary slug will be used. - /// - public string Slug { get; set; } - /// /// A short description for the post. /// @@ -94,8 +89,10 @@ public async Task> Handle(UpdatePostCommand request, CancellationTo if (entity == null) return Result.Fail(new NotFoundException($"Could not find post, id: \"{request.Id}\"")); + var oldSlug = entity.Slug; + entity.Title = request.Title; - entity.Slug = request.Slug; + entity.Slug = request.Title.ToSlug(); entity.Description = request.Description; entity.Content = request.Content; entity.Categories = request.Categories; @@ -111,6 +108,11 @@ public async Task> Handle(UpdatePostCommand request, CancellationTo if (!result.IsSuccess) return Result.Fail(result.Exception); + if (!oldSlug.Equals(entity.Slug)) + { + // TODO: update folders + } + return Result.Success(entity); } } diff --git a/src/Opw.PineBlog.Core/ServiceCollectionExtensions.cs b/src/Opw.PineBlog.Core/ServiceCollectionExtensions.cs index 3973384..aab8c11 100644 --- a/src/Opw.PineBlog.Core/ServiceCollectionExtensions.cs +++ b/src/Opw.PineBlog.Core/ServiceCollectionExtensions.cs @@ -35,11 +35,16 @@ public static IServiceCollection AddPineBlogCore(this IServiceCollection service ServiceRegistrar.AddMediatRClasses(services, new[] { typeof(AddPostCommand).Assembly }); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>)); + services.AddTransient, EditPostCommandValidator>(); services.AddTransient, AddPostCommandValidator>(); services.AddTransient, UpdatePostCommandValidator>(); + services.AddTransient, PublishPostCommandValidator>(); + services.AddTransient, UnpublishPostCommandValidator>(); + services.AddTransient, GetPostQueryValidator>(); services.AddTransient, GetPostByIdQueryValidator>(); + services.AddTransient, UploadFileCommandValidator>(); services.AddTransient(); diff --git a/src/Opw.PineBlog.EntityFrameworkCore/DatabaseSeed.cs b/src/Opw.PineBlog.EntityFrameworkCore/DatabaseSeed.cs deleted file mode 100644 index 828eee9..0000000 --- a/src/Opw.PineBlog.EntityFrameworkCore/DatabaseSeed.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Opw.PineBlog.Entities; -using System; -using System.Linq; - -namespace Opw.PineBlog.EntityFrameworkCore -{ - internal class DatabaseSeed - { - private readonly BlogEntityDbContext _dbContext; - - public DatabaseSeed(BlogEntityDbContext context) - { - _dbContext = context; - } - - public void Run() - { - CreateAuthors(); - CreateBlogPosts(); - } - - void CreateAuthors() - { - if (_dbContext.Authors.Count() > 0) return; - - var email = "pineblog@example.com"; - if (_dbContext.Authors.Count(a => a.UserName.Equals(email)) > 0) return; - - _dbContext.Authors.Add(new Author - { - UserName = email, - Email = email, - DisplayName = "John Smith", - Avatar = "images/avatar-male.png", - Bio = "It is common knowledge that the consolidation of the mindset cannot be shown to be relevant.This is in contrast to The Affectability Of Determinant Empathy", - }); - _dbContext.Authors.Add(new Author - { - UserName = "mary.smith@example.com", - Email = email, - DisplayName = "Mary Smith", - Avatar = "images/avatar-female.png", - Bio = "It is common knowledge that the consolidation of the mindset cannot be shown to be relevant.This is in contrast to The Affectability Of Determinant Empathy", - }); - - _dbContext.SaveChanges(); - } - - void CreateBlogPosts() - { - if (_dbContext.Posts.Count() > 0) return; - - var index = 0; - foreach (var author in _dbContext.Authors) - { - index++; - if (_dbContext.Posts.Count(p => p.AuthorId.Equals(author.Id)) > 0) continue; - - _dbContext.Posts.Add(new Post - { - AuthorId = author.Id, - Title = "The Affectability Of Determinant Empathy", - Slug = $"The Affectability Of Determinant Empathy {index}".ToSlug(), - Description = "To be perfectly frank, the target population for any formalization of the proactive dynamic teleology provides a balanced perspective to the strategic organic auto-interruption. We need to be able to rationalize the hierarchical immediate vibrancy. We need to be able to rationalize the marginalised empirical support. One must therefore dedicate resources to the total paralyptic correspondence immediately.", - Content = @"So far, the consolidation of the benchmark de-stabilizes any discrete or conceptual configuration mode. - -In a strictly mechanistic sense, efforts are already underway in the development of the global business practice. On the other hand, the ball-park figures for the basic definitive rationalization indicates the importance of other systems and the necessity for an elemental change in the adequate timing control.", - Categories = "wafflegen", - CoverUrl = "images/woods.gif", - Published = DateTime.UtcNow.AddDays(-(index * 20) - index) - }); - - _dbContext.Posts.Add(new Post - { - AuthorId = author.Id, - Title = "The Element Of Sub-Logical Phenomenon", - Slug = $"The Element Of Sub-Logical Phenomenon {index}".ToSlug(), - Description = "Without doubt, the assessment of any significant weaknesses in the value added vibrant concept embodies The Element Of Sub-Logical Phenomenon.", - Content = @"Whilst it may be true that a proportion of the skill set makes little difference to the philosophy of commonality and standardization. Everything should be done to expedite the two-phase empirical parameter. Everything should be done to expedite the universe of object, one must not lose sight of the fact that a primary interrelationship between system and/or subsystem technologies uniquely legitimises the significance of what should be termed the non-viable expressive program.", - Categories = "wafflegen", - Published = DateTime.UtcNow.AddDays(-(index * 10) - index) - }); - - _dbContext.Posts.Add(new Post - { - AuthorId = author.Id, - Title = "The Disposition Of Non-Referent Discord", - Slug = $"The Disposition Of Non-Referent Discord {index}".ToSlug(), - Description = "It can be forcibly emphasized that an anticipation of the effects of any homogeneous partnership capitalises on the strengths of the overall game-plan.", - Content = @"Without a doubt, any significant enhancements in the purchaser - provider may mean a wide diffusion of the mechanism-independent governing support into the temperamental symbolism. One must therefore dedicate resources to the psychic factor immediately.. So, where to from here? Presumably, The core drivers is generally compatible with the doctrine of the integrated item. Everything should be done to expedite the evolution of precise absorption over a given time limit.", - Categories = "wafflegen", - Published = DateTime.UtcNow.AddDays(-index - index) - }); - } - - _dbContext.SaveChanges(); - } - } -} diff --git a/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/AddPost.cshtml b/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/AddPost.cshtml index 675d507..1e50ee3 100644 --- a/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/AddPost.cshtml +++ b/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/AddPost.cshtml @@ -13,14 +13,13 @@ var simplemde = new SimpleMDE({ element: document.getElementById('Content'), toolbar: [ - 'bold', 'italic', 'heading', 'heading-2', '|', + 'bold', 'italic', 'heading-2', 'heading-3', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'code', 'link', { name: 'insertImage', action: function customFunction(editor) { - _editor = editor; - fileManager.open(insertImageCallback, '@Model.Post.Slug', 'image'); + // TODO: alert that the post needs to be saved first }, className: 'fa fa-folder-open', title: 'Insert image' diff --git a/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/Shared/_EditPost.cshtml b/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/Shared/_EditPost.cshtml index 4cb9c01..1c85d40 100644 --- a/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/Shared/_EditPost.cshtml +++ b/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/Shared/_EditPost.cshtml @@ -1,32 +1,49 @@ @model Opw.PineBlog.Posts.IEditPostCommand - +@{ + var isEdit = ViewData["Id"] != null;// !string.IsNullOrWhiteSpace(Model.Title); +}
- -
- - + }
- - + @if (isEdit) + { + View + } + Cancel -
- - -
+ @if (isEdit) + { +
+ + +
+ }
@@ -34,6 +51,7 @@
+
@@ -62,11 +80,6 @@
-
- - - -

@@ -78,6 +91,15 @@
+ @if (Model.Published.HasValue) + { +
+
+ + + +
+ }
diff --git a/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/UpdatePost.cshtml b/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/UpdatePost.cshtml index b17406f..81415d0 100644 --- a/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/UpdatePost.cshtml +++ b/src/Opw.PineBlog.RazorPages/Areas/Admin/Pages/UpdatePost.cshtml @@ -1,5 +1,9 @@ @page @model UpdatePostModel +@{ + ViewData["Id"] = Model.Post.Id; + ViewData["Slug"] = Model.Post.Title.ToSlug(); +} @section scripts {