Use new API for blog

This commit is contained in:
Robert Marshall 2020-04-11 13:37:14 +01:00
parent e389b2404a
commit 25c320bf6b
17 changed files with 356 additions and 314 deletions

View file

@ -23,6 +23,8 @@ steps:
from_secret: GitDomain from_secret: GitDomain
GitToken: GitToken:
from_secret: GitToken from_secret: GitToken
BlogEndpoint:
from_secret: BlogEndpoint
commands: commands:
- curl -sL https://deb.nodesource.com/setup_12.x | bash - - curl -sL https://deb.nodesource.com/setup_12.x | bash -
- apt-get install -y nodejs - apt-get install -y nodejs
@ -31,6 +33,7 @@ steps:
- sed -i "s/<DatabaseConnectionString>/$ConnectionString/g" output/appsettings.json - sed -i "s/<DatabaseConnectionString>/$ConnectionString/g" output/appsettings.json
- sed -i "s/<GitDomain>/$GitDomain/g" output/appsettings.json - sed -i "s/<GitDomain>/$GitDomain/g" output/appsettings.json
- sed -i "s/<GitToken>/$GitToken/g" output/appsettings.json - sed -i "s/<GitToken>/$GitToken/g" output/appsettings.json
- sed -i "s/<BlogEndpoint>/$BlogEndpoint/g" output/appsettings.json
- cp Infrastructure/website.service output/ - cp Infrastructure/website.service output/
- cp -r ./output/* /output - cp -r ./output/* /output
- name: restart service - name: restart service

View file

@ -0,0 +1,249 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Website.Data;
using Website.Models;
using Xunit;
namespace Website.Tests.Data {
public class BlogApiTests {
[Fact]
public async Task GetPostByUrlAsync_WithUrl_ReturnsBlogPost() {
const string json = @"{""id"":1,""title"":""title"",""content"":""content"",""timestamp"":""2020-04-10T13:00:42"",""draft"":""draft"",""url"":""url"",""userId"":0}";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Get)
.WithUrl("/get/test")
.WithResponse(json)
.Build();
var expectation = new BlogPost {
Id = 1,
Title = "title",
Content = "content",
Draft = "draft",
Timestamp = new DateTime(2020, 04, 10, 13, 00, 42),
Url = "url",
UserId = 0
};
var api = new BlogApi(httpClient);
(await api.GetPostByUrlAsync("test")).Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task GetLatestPostsAsync_WithNoParameters_ReturnsBlogPostCollection() {
const string json = @"[{""id"":1,""title"":""title"",""content"":""content"",""timestamp"":""2020-04-10T13:00:42"",""draft"":""draft"",""url"":""url"",""userId"":0}]";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Get)
.WithUrl("/getlatestposts")
.WithResponse(json)
.Build();
var expectation = new[] {
new BlogPost {
Id = 1,
Title = "title",
Content = "content",
Draft = "draft",
Timestamp = new DateTime(2020, 04, 10, 13, 00, 42),
Url = "url",
UserId = 0
}
};
var api = new BlogApi(httpClient);
(await api.GetLatestPostsAsync()).Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task GetLatestPostsAsync_WithCount_ReturnsBlogPostCollection() {
const string json = @"[{""id"":1,""title"":""title"",""content"":""content"",""timestamp"":""2020-04-10T13:00:42"",""draft"":""draft"",""url"":""url"",""userId"":0}]";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Get)
.WithUrl("/getlatestposts?count=1")
.WithResponse(json)
.Build();
var expectation = new[] {
new BlogPost {
Id = 1,
Title = "title",
Content = "content",
Draft = "draft",
Timestamp = new DateTime(2020, 04, 10, 13, 00, 42),
Url = "url",
UserId = 0
}
};
var api = new BlogApi(httpClient);
(await api.GetLatestPostsAsync(1)).Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task GetLatestPostsAsync_WithCountAndOffset_ReturnsBlogPostCollection() {
const string json = @"[{""id"":1,""title"":""title"",""content"":""content"",""timestamp"":""2020-04-10T13:00:42"",""draft"":""draft"",""url"":""url"",""userId"":0}]";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Get)
.WithUrl("/getlatestposts")
.WithQueryString("count", "1")
.WithQueryString("offset", "1")
.WithResponse(json)
.Build();
var expectation = new[] {
new BlogPost {
Id = 1,
Title = "title",
Content = "content",
Draft = "draft",
Timestamp = new DateTime(2020, 04, 10, 13, 00, 42),
Url = "url",
UserId = 0
}
};
var api = new BlogApi(httpClient);
(await api.GetLatestPostsAsync(1, 1)).Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task GetLatestPostAsync_ReturnsBlogPost() {
const string json = @"{""id"":1,""title"":""title"",""content"":""content"",""timestamp"":""2020-04-10T13:00:42"",""draft"":""draft"",""url"":""url"",""userId"":0}";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Get)
.WithUrl("/getlatestpost")
.WithResponse(json)
.Build();
var expectation = new BlogPost {
Id = 1,
Title = "title",
Content = "content",
Draft = "draft",
Timestamp = new DateTime(2020, 04, 10, 13, 00, 42),
Url = "url",
UserId = 0
};
var api = new BlogApi(httpClient);
(await api.GetLatestPostAsync()).Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task GetCountAsync_ReturnsCount() {
const string json = @"23";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Get)
.WithUrl("/getcount")
.WithResponse(json)
.Build();
var api = new BlogApi(httpClient);
(await api.GetCountAsync()).Should().Be(23);
}
[Fact]
public async Task GetPostByIdAsync_WithUrl_ReturnsBlogPost() {
const string json = @"{""id"":1,""title"":""title"",""content"":""content"",""timestamp"":""2020-04-10T13:00:42"",""draft"":""draft"",""url"":""url"",""userId"":0}";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Get)
.WithUrl("/get/1")
.WithResponse(json)
.Build();
var expectation = new BlogPost {
Id = 1,
Title = "title",
Content = "content",
Draft = "draft",
Timestamp = new DateTime(2020, 04, 10, 13, 00, 42),
Url = "url",
UserId = 0
};
var api = new BlogApi(httpClient);
(await api.GetPostByIdAsync(1)).Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task SavePost_WithPost_ReturnsNothing() {
const string requestJson = @"{""Id"":1,""Title"":""title"",""Content"":""content""}";
const string responseJson = @"{""id"":1,""title"":""title"",""content"":""content"",""timestamp"":""2020-04-10T13:00:42"",""draft"":""draft"",""url"":""url"",""userId"":0}";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Post)
.WithUrl("/savepost")
.WithPostBody(requestJson)
.WithResponse(responseJson)
.Build();
var post = new BlogPostSubmission {Id = 1, Title = "title", Content = "content"};
var expectation = new BlogPost {
Id = 1,
Title = "title",
Content = "content",
Draft = "draft",
Timestamp = new DateTime(2020, 04, 10, 13, 00, 42),
Url = "url",
UserId = 0
};
var api = new BlogApi(httpClient);
(await api.SavePost(post)).Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task GetAllPostsAsync_ReturnsBlogPostCollection() {
const string json =
@"[{""id"":1,""title"":""title"",""content"":""content"",""timestamp"":""2020-04-10T13:00:42"",""draft"":""draft"",""url"":""url"",""userId"":0}]";
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Get)
.WithUrl("/getallposts")
.WithResponse(json)
.Build();
var expectation = new[] {
new BlogPost {
Id = 1,
Title = "title",
Content = "content",
Draft = "draft",
Timestamp = new DateTime(2020, 04, 10, 13, 00, 42),
Url = "url",
UserId = 0
}
};
var api = new BlogApi(httpClient);
(await api.GetAllPostsAsync()).Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task DeletePost_WithId_ReturnsNothing() {
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Post)
.WithUrl("/deletepost")
.WithPostBody("1")
.Build(out var mockHttpMessageHandler);
var api = new BlogApi(httpClient);
await api.DeletePostAsync(1);
mockHttpMessageHandler.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task PublishPost_WithId_ReturnsNothing() {
var httpClient = new HttpClientBuilder()
.WithMethod(HttpMethod.Post)
.WithUrl("/publishpost")
.WithPostBody("1")
.Build(out var mockHttpMessageHandler);
var api = new BlogApi(httpClient);
await api.PublishPostAsync(1);
mockHttpMessageHandler.VerifyNoOutstandingExpectation();
}
}
}

View file

@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using Dapper;
using FluentAssertions;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Website.Data;
using Website.Data.States;
using Website.Models;
using Xunit;
namespace Website.Tests.Data
{
public class BlogRepositoryTests
{
// [Fact]
// public void GetPostAsync_WithValidId_ReturnsBlogPost() {
// var blogPostState = new BlogPostState
// {
// Post_Id = 1,
// Post_Title = "title",
// Post_Content = "content",
// Post_Timestamp = new DateTime(),
// Post_Deleted = false,
// Post_Draft = "draft",
// Post_Url = "url",
// User_Id = 1
// };
// var connection = Substitute.For<IDbConnection>();
// connection.QueryAsync(null, null, (Func<object[], BlogPostState>)null, null, null, false, null, null, null).ReturnsForAnyArgs(new[]{ blogPostState });
// var provider = Substitute.For<IDatabaseProvider>();
// provider.NewConnection().Returns(connection);
// var repo = new BlogRepository(provider);
// var post = repo.GetPostAsync(1);
// var expected = new BlogPost
// {
// Id = 1,
// Title = "title",
// Content = "content",
// Timestamp = new DateTime(),
// Draft = "draft",
// Url = "url",
// UserId = 1
// }; ;
// post.Should().BeEquivalentTo(expected);
// }
//public async Task GetPostAsync_WithInvalidId_ReturnsNull()
//public async Task GetPostAsync_WithDeletedPost_ReturnsNull()
}
}

View file

@ -1,42 +0,0 @@
using FluentAssertions;
using Website.Models;
using Xunit;
namespace Website.Tests.Models {
public class BlogPostTests {
[Fact]
public void UpdateTitle_WithNewTitle_UpdatesTitlePropertyAndRegeneratesUrl() {
var post = new BlogPost();
post.UpdateTitle("new title");
post.Title.Should().Be("new title");
post.Url.Should().Be("new-title");
}
[Theory]
[InlineData("new:title", "new-title")]
[InlineData("new: title", "new-title")]
[InlineData("new$title", "new-title")]
[InlineData("new TITle", "new-title")]
public void UpdateTitle_WithUnsafeCharsInTitle_RegeneratesSafeUrl(string title, string url) {
var post = new BlogPost();
post.UpdateTitle(title);
post.Url.Should().Be(url);
}
[Fact]
public void UpdateDraft_WithContent_UpdatesDraftProperty() {
var post = new BlogPost();
post.UpdateDraft("content");
post.Draft.Should().Be("content");
}
[Fact]
public void Publish_SetsContentToDraft() {
var post = new BlogPost();
post.UpdateDraft("content");
post.Publish();
post.Content.Should().Be("content");
}
}
}

View file

@ -1,5 +1,4 @@
using FluentAssertions; using FluentAssertions;
using Website.Data.States;
using Website.Models; using Website.Models;
using Website.ViewModels; using Website.ViewModels;
using Xunit; using Xunit;
@ -10,20 +9,14 @@ namespace Website.Tests.VIewModels
{ {
[Fact] [Fact]
public void Constructor_WithContentOver1000Characters_LimitsContentTo1000Chars() { public void Constructor_WithContentOver1000Characters_LimitsContentTo1000Chars() {
var state = new BlogPostState { var post = new BlogPost {Content = new string('a', 1001)};
Post_Content = new string('a', 1001)
};
var post = new BlogPost(state);
var vm = new BlogPostSnippetViewModel(post); var vm = new BlogPostSnippetViewModel(post);
vm.Content.Length.Should().Be(1000); vm.Content.Length.Should().Be(1000);
} }
[Fact] [Fact]
public void Constructor_WithContentUnder1000Characters_ContentIsIdenticalLength() { public void Constructor_WithContentUnder1000Characters_ContentIsIdenticalLength() {
var state = new BlogPostState { var post = new BlogPost{Content = new string('a', 900)};
Post_Content = new string('a', 900)
};
var post = new BlogPost(state);
var vm = new BlogPostSnippetViewModel(post); var vm = new BlogPostSnippetViewModel(post);
vm.Content.Length.Should().Be(900); vm.Content.Length.Should().Be(900);
} }

View file

@ -1,53 +1,46 @@
using System; using System;
using FluentAssertions; using FluentAssertions;
using Website.Data.States;
using Website.Models; using Website.Models;
using Website.ViewModels; using Website.ViewModels;
using Xunit; using Xunit;
namespace Website.Tests.VIewModels namespace Website.Tests.VIewModels {
{ public class BlogPostViewModelTests {
public class BlogPostViewModelTests
{
[Fact] [Fact]
public void ContentHtml_WithMarkdownContent_ReturnsHtml() { public void ContentHtml_WithMarkdownContent_ReturnsHtml() {
var state = new BlogPostState { var post = new BlogPost {
Post_Content="# header" Content = "# header"
}; };
var post = new BlogPost(state);
var vm = new BlogPostViewModel(post, false); var vm = new BlogPostViewModel(post, false);
vm.ContentHtml.Should().Be("<h1>header</h1>"); vm.ContentHtml.Should().Be("<h1>header</h1>");
} }
[Fact] [Fact]
public void Timestamp_OnThe1st_IsFriendlyString() { public void Timestamp_OnThe1st_IsFriendlyString() {
var state = new BlogPostState { var post = new BlogPost {
Post_Content = "", Content = "",
Post_Timestamp = new DateTime(2018, 10, 01, 15, 1, 25) Timestamp = new DateTime(2018, 10, 01, 15, 1, 25)
}; };
var post = new BlogPost(state);
var vm = new BlogPostViewModel(post, false); var vm = new BlogPostViewModel(post, false);
vm.Timestamp.Should().Be("Monday the 1st of October 2018"); vm.Timestamp.Should().Be("Monday the 1st of October 2018");
} }
[Fact] [Fact]
public void Timestamp_OnThe2nd_IsFriendlyString() { public void Timestamp_OnThe2nd_IsFriendlyString() {
var state = new BlogPostState { var post = new BlogPost {
Post_Content = "", Content = "",
Post_Timestamp = new DateTime(2018, 10, 02, 15, 1, 25) Timestamp = new DateTime(2018, 10, 02, 15, 1, 25)
}; };
var post = new BlogPost(state);
var vm = new BlogPostViewModel(post, false); var vm = new BlogPostViewModel(post, false);
vm.Timestamp.Should().Be("Tuesday the 2nd of October 2018"); vm.Timestamp.Should().Be("Tuesday the 2nd of October 2018");
} }
[Fact] [Fact]
public void Timestamp_OnThe3rd_IsFriendlyString() { public void Timestamp_OnThe3rd_IsFriendlyString() {
var state = new BlogPostState { var post = new BlogPost {
Post_Content = "", Content = "",
Post_Timestamp = new DateTime(2018, 10, 03, 15, 1, 25) Timestamp = new DateTime(2018, 10, 03, 15, 1, 25)
}; };
var post = new BlogPost(state);
var vm = new BlogPostViewModel(post, false); var vm = new BlogPostViewModel(post, false);
vm.Timestamp.Should().Be("Wednesday the 3rd of October 2018"); vm.Timestamp.Should().Be("Wednesday the 3rd of October 2018");
} }

View file

@ -10,21 +10,21 @@ using Website.ViewModels;
namespace Website.Controllers { namespace Website.Controllers {
public class BlogController:Controller { public class BlogController:Controller {
private const int MaxPostsPerPage = 10; private const int MaxPostsPerPage = 10;
private readonly BlogRepository _repo; private readonly IBlogApi _api;
public BlogController(BlogRepository repo) => _repo = repo; public BlogController(IBlogApi api) => _api = api;
public async Task<IActionResult> Page(int page) { public async Task<IActionResult> Page(int page) {
var offset = (page - 1) * MaxPostsPerPage; var offset = (page - 1) * MaxPostsPerPage;
var posts = (await _repo.GetLatestPostsAsync(MaxPostsPerPage, offset)).Select(p => new BlogPostSnippetViewModel(p)); var posts = (await _api.GetLatestPostsAsync(MaxPostsPerPage, offset)).Select(p => new BlogPostSnippetViewModel(p));
var maxPages = (await _repo.GetCountAsync()) / MaxPostsPerPage; var maxPages = (await _api.GetCountAsync()) / MaxPostsPerPage;
var model = new BlogViewModel(posts, page, maxPages); var model = new BlogViewModel(posts, page, maxPages);
return View(model); return View(model);
} }
public async Task<IActionResult> Entry(string url, bool preview = false) { public async Task<IActionResult> Entry(string url, bool preview = false) {
try { try {
var post = await _repo.GetPostByUrlAsync(url); var post = await _api.GetPostByUrlAsync(url);
if (!preview && string.IsNullOrEmpty(post.Content)) if (!preview && string.IsNullOrEmpty(post.Content))
return NotFound(); return NotFound();
@ -42,7 +42,7 @@ namespace Website.Controllers {
return View(); return View();
try { try {
var post = await _repo.GetPostByIdAsync(id.Value); var post = await _api.GetPostByIdAsync(id.Value);
var model = new BlogPostSubmission { var model = new BlogPostSubmission {
Id = post.Id, Id = post.Id,
Title = post.Title, Title = post.Title,
@ -57,35 +57,28 @@ namespace Website.Controllers {
[Authorize] [Authorize]
[HttpPost] [HttpPost]
public async Task<IActionResult> Save(BlogPostSubmission submission) { public async Task<IActionResult> Save(BlogPostSubmission submission) {
var post = submission.Id.HasValue ? await _repo.GetPostByIdAsync(submission.Id.Value) : new BlogPost(); var savedPost = await _api.SavePost(submission);
post.UpdateTitle(submission.Title);
post.UpdateDraft(submission.Content);
var savedPost = await _repo.SavePost(post);
return RedirectToAction(nameof(Edit), new { savedPost.Id }); return RedirectToAction(nameof(Edit), new { savedPost.Id });
} }
[Authorize] [Authorize]
public async Task<IActionResult> Manage() { public async Task<IActionResult> Manage() {
var posts = await _repo.GetAllPostsAsync(); var posts = await _api.GetAllPostsAsync();
var models = posts.OrderByDescending(post => post.Timestamp).Select(post => new BlogPostViewModel(post, false)); var models = posts.OrderByDescending(post => post.Timestamp).Select(post => new BlogPostViewModel(post, false));
return View(models); return View(models);
} }
[Authorize] [Authorize]
public async Task<IActionResult> Publish(int id) { public async Task<IActionResult> Publish(int id) {
var post = await _repo.GetPostByIdAsync(id); await _api.PublishPostAsync(id);
post.Publish();
await _repo.SavePost(post);
return RedirectToAction(nameof(Manage)); return RedirectToAction(nameof(Manage));
} }
[Authorize] [Authorize]
public async Task<IActionResult> Delete(int id) { public async Task<IActionResult> Delete(int id) {
await _repo.DeletePostAsync(id); await _api.DeletePostAsync(id);
return RedirectToAction(nameof(Manage)); return RedirectToAction(nameof(Manage));
} }

View file

@ -6,16 +6,16 @@ using System.Linq;
namespace Website.Controllers { namespace Website.Controllers {
public class HomeController : Controller { public class HomeController : Controller {
private readonly BlogRepository _blogRepo; private readonly IBlogApi _blogApi;
private readonly GitServerApi _api; private readonly GitServerApi _api;
public HomeController(BlogRepository blogRepo, GitServerApi api) { public HomeController(IBlogApi blogApi, GitServerApi api) {
_api = api; _api = api;
_blogRepo = blogRepo; _blogApi = blogApi;
} }
public async Task<IActionResult> Index() { public async Task<IActionResult> Index() {
var post = await _blogRepo.GetLatestPostAsync(); var post = await _blogApi.GetLatestPostAsync();
var repo = (await _api.GetRepositories()).First(); var repo = (await _api.GetRepositories()).First();
var branch = (await _api.GetBranches(repo.Name)).First(); var branch = (await _api.GetBranches(repo.Name)).First();
var commit = await _api.GetCommit(repo.Name, branch.Commit.Id); var commit = await _api.GetCommit(repo.Name, branch.Commit.Id);

22
Website/Data/BlogApi.cs Normal file
View file

@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Website.Models;
using System.Collections.Generic;
using System.Net.Http;
namespace Website.Data
{
public class BlogApi : ApiClient, IBlogApi {
public BlogApi(HttpClient client) : base(client) {
}
public async Task<BlogPost> GetPostByUrlAsync(string url) => await Get<BlogPost>("/get/" + url);
public async Task<IEnumerable<BlogPost>> GetLatestPostsAsync(int count = 0, int offset = 0) => await Get<IEnumerable<BlogPost>>("/getlatestposts", new{count, offset});
public async Task<BlogPost> GetLatestPostAsync() => await Get<BlogPost>("/getlatestpost");
public async Task<int> GetCountAsync() => await Get<int>("/getcount");
public async Task<BlogPost> GetPostByIdAsync(int id) => await Get<BlogPost>("/get/" + id);
public async Task<BlogPost> SavePost(BlogPostSubmission post) => await Post<BlogPost>("/savepost", post);
public async Task<IEnumerable<BlogPost>> GetAllPostsAsync() => await Get<IEnumerable<BlogPost>>("/getallposts");
public async Task DeletePostAsync(int id) => await Post<object>("/deletepost", id);
public async Task PublishPostAsync(int id) => await Post<object>("/publishpost", id);
}
}

View file

@ -1,87 +0,0 @@
using System.Threading.Tasks;
using System.Linq;
using Dapper;
using Website.Models;
using Website.Data.States;
using System.Collections.Generic;
namespace Website.Data
{
public class BlogRepository
{
private readonly IDatabaseProvider _dbProvider;
public BlogRepository(IDatabaseProvider dbProvider) => _dbProvider = dbProvider;
public async Task<BlogPost> GetPostByUrlAsync(string url) {
const string query = "SELECT * FROM blog_posts WHERE post_url=@url AND post_deleted=0";
using (var connection = _dbProvider.NewConnection()) {
connection.Open();
var result = await connection.QueryAsync<BlogPostState>(query, new{url});
return new BlogPost(result.First());
}
}
public async Task<IEnumerable<BlogPost>> GetLatestPostsAsync(int limit, int offset = 0) {
const string query = "SELECT * FROM blog_posts WHERE post_content<>'' AND post_deleted=0 ORDER BY post_timestamp DESC LIMIT @offset,@limit";
using (var connection = _dbProvider.NewConnection()) {
connection.Open();
var results = await connection.QueryAsync<BlogPostState>(query, new{limit, offset});
return results.Select(result => new BlogPost(result));
}
}
public async Task<BlogPost> GetLatestPostAsync() => (await GetLatestPostsAsync(1)).First();
public async Task<int> GetCountAsync()
{
var query="SELECT COUNT(*) FROM blog_posts WHERE post_content<>'' AND post_deleted=0";
using(var connection = _dbProvider.NewConnection()) {
connection.Open();
var result = await connection.QueryAsync<int>(query);
return result.First();
}
}
public async Task<BlogPost> GetPostByIdAsync(int id) {
const string query = "SELECT * FROM blog_posts WHERE post_id=@id AND post_deleted=0";
using (var connection = _dbProvider.NewConnection()) {
connection.Open();
var result = await connection.QueryAsync<BlogPostState>(query, new {id});
return new BlogPost(result.First());
}
}
public async Task<BlogPost> SavePost(BlogPost post) {
const string newPostQuery = "INSERT INTO blog_posts (post_title, post_content, post_draft, post_url) VALUES (@title, @content, @draft, @url); SELECT CAST(LAST_INSERT_ID() as int)";
const string updatePostQuery = "UPDATE blog_posts SET post_id = @id, post_title = @title, post_content = @content, post_draft = @draft, post_url = @url WHERE post_id = @id ";
var newPost = post.Id == 0;
using (var connection = _dbProvider.NewConnection()) {
connection.Open();
var result = await connection.QueryAsync<int>(newPost ? newPostQuery : updatePostQuery, post);
return newPost ? await GetPostByIdAsync(result.Single()) : post;
}
}
public async Task<IEnumerable<BlogPost>> GetAllPostsAsync() {
const string query = "SELECT * FROM blog_posts WHERE post_deleted=0";
using (var connection = _dbProvider.NewConnection()) {
connection.Open();
var result = await connection.QueryAsync<BlogPostState>(query);
return result.Select(state => new BlogPost(state));
}
}
public async Task DeletePostAsync(int id) {
const string query = "UPDATE blog_posts SET post_deleted=1 WHERE post_id=@id";
using (var connection = _dbProvider.NewConnection()) {
connection.Open();
await connection.ExecuteAsync(query, new {id});
}
}
}
}

17
Website/Data/IBlogApi.cs Normal file
View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Website.Models;
namespace Website.Data {
public interface IBlogApi {
Task<BlogPost> GetPostByUrlAsync(string url);
Task<IEnumerable<BlogPost>> GetLatestPostsAsync(int count = 0, int offset = 0);
Task<BlogPost> GetLatestPostAsync();
Task<int> GetCountAsync();
Task<BlogPost> GetPostByIdAsync(int id);
Task<BlogPost> SavePost(BlogPostSubmission post);
Task<IEnumerable<BlogPost>> GetAllPostsAsync();
Task DeletePostAsync(int id);
Task PublishPostAsync(int id);
}
}

View file

@ -1,19 +0,0 @@
using System;
using Dapper.Contrib.Extensions;
namespace Website.Data.States
{
[Table("blog_posts")]
public class BlogPostState
{
[Key]
public int Post_Id { get; set; }
public string Post_Title { get; set; }
public string Post_Content { get; set; }
public DateTime Post_Timestamp { get; set; }
public string Post_Draft { get; set; }
public string Post_Url { get; set; }
public bool Post_Deleted{ get; set; }
public int User_Id { get; set; }
}
}

View file

@ -1,44 +1,15 @@
using System; using System;
using System.Text.RegularExpressions;
using Website.Data.States;
namespace Website.Models namespace Website.Models
{ {
public class BlogPost public class BlogPost
{ {
public BlogPost() { public int Id { get; set; }
public string Title { get; set; }
} public string Content { get; set; }
public DateTime Timestamp { get; set; }
public BlogPost(BlogPostState state) public string Draft { get; set; }
{ public string Url { get; set; }
Id = state.Post_Id; public int UserId { get; set; }
Title = state.Post_Title;
Content = state.Post_Content;
Timestamp = state.Post_Timestamp;
Draft = state.Post_Draft;
Url = state.Post_Url;
UserId = state.User_Id;
}
public int Id { get; private set; }
public string Title { get; private set; }
public string Content { get; private set; }
public DateTime Timestamp { get; private set; }
public string Draft { get; private set; }
public string Url { get; private set; }
public int UserId { get; private set; }
private void GenerateUrl() {
Url = Regex.Replace(Title, @"[^a-zA-Z0-9\.]+", "-").ToLower();
}
public void UpdateTitle(string title) {
Title = title;
GenerateUrl();
}
public void UpdateDraft(string content) => Draft = content;
public void Publish() => Content = Draft;
} }
} }

View file

@ -1,4 +1,5 @@
using System.Net.Http; using System;
using System.Net.Http;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -43,9 +44,10 @@ namespace Website
private void RegisterRepositories(IServiceCollection services) => private void RegisterRepositories(IServiceCollection services) =>
services.AddSingleton<IDatabaseProvider, MySQLDatabaseProvider>() services.AddSingleton<IDatabaseProvider, MySQLDatabaseProvider>()
.AddSingleton<BlogRepository, BlogRepository>() .AddSingleton<IBlogApi, BlogApi>()
.AddSingleton<UserRepository, UserRepository>() .AddSingleton<UserRepository, UserRepository>()
.AddSingleton(new GitServerApi(new HttpClient(), Configuration["gitDomain"], Configuration["gitToken"])); .AddSingleton(new GitServerApi(new HttpClient(), Configuration["gitDomain"], Configuration["gitToken"]))
.AddHttpClient<IBlogApi, BlogApi>(client => client.BaseAddress = new Uri(Configuration["blogApiEndpoint"]));
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

View file

@ -4,9 +4,9 @@ using Website.Data;
namespace Website.ViewComponents { namespace Website.ViewComponents {
public class BlogNavigationViewComponent:ViewComponent { public class BlogNavigationViewComponent:ViewComponent {
private readonly BlogRepository _repo; private readonly IBlogApi _repo;
public BlogNavigationViewComponent(BlogRepository repo) { public BlogNavigationViewComponent(IBlogApi repo) {
_repo = repo; _repo = repo;
} }

View file

@ -10,5 +10,6 @@
"database": "Server=localhost;User ID=user;Password=pass;Database=db" "database": "Server=localhost;User ID=user;Password=pass;Database=db"
}, },
"gitDomain": "", "gitDomain": "",
"gitToken": "" "gitToken": "",
"blogApiEndpoint": ""
} }

View file

@ -9,6 +9,7 @@
}, },
"gitDomain": "<GitDomain>", "gitDomain": "<GitDomain>",
"gitToken": "<GitToken>", "gitToken": "<GitToken>",
"blogApiEndpoint": "<BlogEndpoint>",
"AllowedHosts": "*", "AllowedHosts": "*",
"Kestrel": { "Kestrel": {
"EndPoints": { "EndPoints": {