Skip to content

Commit

Permalink
Add support for tags (#168)
Browse files Browse the repository at this point in the history
* Rename tag to category in edit page.

* Add support for tags.
  • Loading branch information
marcoskirchner authored Nov 9, 2021
1 parent 6ceeeeb commit 3a49385
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 37 deletions.
2 changes: 2 additions & 0 deletions src/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ namespace Miniblog.Core
public static class Constants
{
public static readonly string AllCats = "AllCats";
public static readonly string AllTags = "AllTags";
public static readonly string categories = "categories";
public static readonly string tags = "tags";
public static readonly string Dash = "-";
public static readonly string Description = "Description";
public static readonly string Head = "Head";
Expand Down
31 changes: 31 additions & 0 deletions src/Controllers/BlogController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ public async Task<IActionResult> Category(string category, int page = 0)
return this.View("~/Views/Blog/Index.cshtml", filteredPosts.AsAsyncEnumerable());
}

[Route("/blog/tag/{tag}/{page:int?}")]
[OutputCache(Profile = "default")]
public async Task<IActionResult> Tag(string tag, int page = 0)
{
// get posts for the selected tag.
var posts = this.blog.GetPostsByTag(tag);

// apply paging filter.
var filteredPosts = posts.Skip(this.settings.Value.PostsPerPage * page).Take(this.settings.Value.PostsPerPage);

// set the view option
this.ViewData["ViewOption"] = this.settings.Value.ListView;

this.ViewData[Constants.TotalPostCount] = await posts.CountAsync().ConfigureAwait(true);
this.ViewData[Constants.Title] = $"{this.manifest.Name} {tag}";
this.ViewData[Constants.Description] = $"Articles posted in the {tag} tag";
this.ViewData[Constants.prev] = $"/blog/tag/{tag}/{page + 1}/";
this.ViewData[Constants.next] = $"/blog/tag/{tag}/{(page <= 1 ? null : page - 1 + "/")}";
return this.View("~/Views/Blog/Index.cshtml", filteredPosts.AsAsyncEnumerable());
}

[Route("/blog/comment/{postId}/{commentId}")]
[Authorize]
public async Task<IActionResult> DeleteComment(string postId, string commentId)
Expand Down Expand Up @@ -135,6 +156,10 @@ public async Task<IActionResult> Edit(string? id)
categories.Sort();
this.ViewData[Constants.AllCats] = categories;

var tags = await this.blog.GetTags().ToListAsync();
tags.Sort();
this.ViewData[Constants.AllTags] = tags;

if (string.IsNullOrEmpty(id))
{
return this.View(new Post());
Expand Down Expand Up @@ -198,12 +223,18 @@ public async Task<IActionResult> UpdatePost(Post post)

var existing = await this.blog.GetPostById(post.ID).ConfigureAwait(false) ?? post;
string categories = this.Request.Form[Constants.categories];
string tags = this.Request.Form[Constants.tags];

existing.Categories.Clear();
categories.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Trim().ToLowerInvariant())
.ToList()
.ForEach(existing.Categories.Add);
existing.Tags.Clear();
tags.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.Trim().ToLowerInvariant())
.ToList()
.ForEach(existing.Tags.Add);
existing.Title = post.Title.Trim();
existing.Slug = !string.IsNullOrWhiteSpace(post.Slug) ? post.Slug.Trim() : Models.Post.CreateSlug(post.Title);
existing.IsPublished = post.IsPublished;
Expand Down
4 changes: 4 additions & 0 deletions src/Controllers/RobotsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ public async Task Rss(string type)
{
item.AddCategory(new SyndicationCategory(category));
}
foreach (var tag in post.Tags)
{
item.AddCategory(new SyndicationCategory(tag));
}

item.AddContributor(new SyndicationPerson("test@example.com", this.settings.Value.Owner));
item.AddLink(new SyndicationLink(new Uri(item.Id)));
Expand Down
4 changes: 2 additions & 2 deletions src/Miniblog.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
Expand All @@ -23,7 +23,7 @@
<PackageReference Include="WebEssentials.AspNetCore.PWA" Version="1.0.59" />
<PackageReference Include="WebEssentials.AspNetCore.StaticFilesWithCache" Version="1.0.1" />
<PackageReference Include="WebMarkupMin.AspNetCore2" Version="2.7.0" />
<PackageReference Include="WilderMinds.MetaWeblog" Version="2.0.1" />
<PackageReference Include="WilderMinds.MetaWeblog" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/Models/Post.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public class Post
{
public IList<string> Categories { get; } = new List<string>();

public IList<string> Tags { get; } = new List<string>();

public IList<Comment> Comments { get; } = new List<Comment>();

[Required]
Expand Down
48 changes: 48 additions & 0 deletions src/Services/FileBlogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ public virtual IAsyncEnumerable<string> GetCategories()
.ToAsyncEnumerable();
}

[SuppressMessage(
"Globalization",
"CA1308:Normalize strings to uppercase",
Justification = "Consumer preference.")]
public virtual IAsyncEnumerable<string> GetTags()
{
var isAdmin = this.IsAdmin();

return this.cache
.Where(p => p.IsPublished || isAdmin)
.SelectMany(post => post.Tags)
.Select(tag => tag.ToLowerInvariant())
.Distinct()
.ToAsyncEnumerable();
}

public virtual Task<Post?> GetPostById(string id)
{
var isAdmin = this.IsAdmin();
Expand Down Expand Up @@ -143,6 +159,18 @@ where p.Categories.Contains(category, StringComparer.OrdinalIgnoreCase)
return posts.ToAsyncEnumerable();
}

public IAsyncEnumerable<Post> GetPostsByTag(string tag)
{
var isAdmin = this.IsAdmin();

var posts = from p in this.cache
where p.PubDate <= DateTime.UtcNow && (p.IsPublished || isAdmin)
where p.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase)
select p;

return posts.ToAsyncEnumerable();
}

[SuppressMessage(
"Usage",
"SecurityIntelliSenseCS:MS Security rules violation",
Expand Down Expand Up @@ -193,6 +221,7 @@ public async Task SavePost(Post post)
new XElement("content", post.Content),
new XElement("ispublished", post.IsPublished),
new XElement("categories", string.Empty),
new XElement("tags", string.Empty),
new XElement("comments", string.Empty)
));

Expand All @@ -202,6 +231,12 @@ public async Task SavePost(Post post)
categories.Add(new XElement("category", category));
}

var tags = doc.XPathSelectElement("post/tags");
foreach (var tag in post.Tags)
{
tags.Add(new XElement("tag", tag));
}

var comments = doc.XPathSelectElement("post/comments");
foreach (var comment in post.Comments)
{
Expand Down Expand Up @@ -263,6 +298,18 @@ private static void LoadCategories(Post post, XElement doc)
categories.Elements("category").Select(node => node.Value).ToList().ForEach(post.Categories.Add);
}

private static void LoadTags(Post post, XElement doc)
{
var tags = doc.Element("tags");
if (tags is null)
{
return;
}

post.Tags.Clear();
tags.Elements("tag").Select(node => node.Value).ToList().ForEach(post.Tags.Add);
}

private static void LoadComments(Post post, XElement doc)
{
var comments = doc.Element("comments");
Expand Down Expand Up @@ -342,6 +389,7 @@ private void LoadPosts()
};

LoadCategories(post, doc);
LoadTags(post, doc);
LoadComments(post, doc);
this.cache.Add(post);
}
Expand Down
4 changes: 4 additions & 0 deletions src/Services/IBlogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface IBlogService

IAsyncEnumerable<string> GetCategories();

IAsyncEnumerable<string> GetTags();

Task<Post?> GetPostById(string id);

Task<Post?> GetPostBySlug(string slug);
Expand All @@ -21,6 +23,8 @@ public interface IBlogService

IAsyncEnumerable<Post> GetPostsByCategory(string category);

IAsyncEnumerable<Post> GetPostsByTag(string tag);

Task<string> SaveFile(byte[] bytes, string fileName, string? suffix = null);

Task SavePost(Post post);
Expand Down
30 changes: 30 additions & 0 deletions src/Services/InMemoryBlogServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ public virtual IAsyncEnumerable<string> GetCategories()
return categories;
}

[SuppressMessage(
"Globalization",
"CA1308:Normalize strings to uppercase",
Justification = "Consumer preference.")]
public virtual IAsyncEnumerable<string> GetTags()
{
var isAdmin = this.IsAdmin();

var tags = this.Cache
.Where(p => p.IsPublished || isAdmin)
.SelectMany(post => post.Tags)
.Select(tag => tag.ToLowerInvariant())
.Distinct()
.ToAsyncEnumerable();

return tags;
}

public virtual Task<Post?> GetPostById(string id)
{
var isAdmin = this.IsAdmin();
Expand Down Expand Up @@ -92,6 +110,18 @@ where p.Categories.Contains(category, StringComparer.OrdinalIgnoreCase)
return posts.ToAsyncEnumerable();
}

public virtual IAsyncEnumerable<Post> GetPostsByTag(string tag)
{
var isAdmin = this.IsAdmin();

var posts = from p in this.Cache
where p.IsVisible() || isAdmin
where p.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase)
select p;

return posts.ToAsyncEnumerable();
}

public abstract Task<string> SaveFile(byte[] bytes, string fileName, string? suffix = null);

public abstract Task SavePost(Post post);
Expand Down
20 changes: 19 additions & 1 deletion src/Services/MetaWeblogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public async Task<string> AddPostAsync(string blogid, string username, string pa
};

post.categories.ToList().ForEach(newPost.Categories.Add);
post.mt_keywords.Split(',').ToList().ForEach(newPost.Tags.Add);

if (post.dateCreated != DateTime.MinValue)
{
Expand Down Expand Up @@ -123,6 +124,8 @@ public async Task<bool> EditPostAsync(string postid, string username, string pas
existing.IsPublished = publish;
existing.Categories.Clear();
post.categories.ToList().ForEach(existing.Categories.Add);
existing.Tags.Clear();
post.mt_keywords.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList().ForEach(existing.Tags.Add);

if (post.dateCreated != DateTime.MinValue)
{
Expand Down Expand Up @@ -176,6 +179,20 @@ public async Task<Post[]> GetRecentPostsAsync(string blogid, string username, st
.ToArrayAsync();
}

public async Task<Tag[]> GetTagsAsync(string blogid, string username, string password)
{
this.ValidateUser(username, password);

return await this.blog.GetTags()
.Select(
tag =>
new Tag
{
name = tag
})
.ToArrayAsync();
}

public Task<UserInfo> GetUserInfoAsync(string key, string username, string password)
{
this.ValidateUser(username, password);
Expand Down Expand Up @@ -231,7 +248,8 @@ private Post ToMetaWebLogPost(Models.Post post)
dateCreated = post.PubDate,
mt_excerpt = post.Excerpt,
description = post.Content,
categories = post.Categories.ToArray()
categories = post.Categories.ToArray(),
mt_keywords = string.Join(',', post.Tags)
};
}

Expand Down
21 changes: 17 additions & 4 deletions src/Views/Blog/Edit.cshtml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
@model Post
@model Post
@{
var isNew = string.IsNullOrEmpty(Model.Title);
ViewData[Constants.Title] = "Edit " + (Model.Title ?? "new post");
var host = Context.Request.Host.ToString();
var allCats = ViewData[Constants.AllCats] as List<string> ?? new List<string>();
var allTags = ViewData[Constants.AllTags] as List<string> ?? new List<string>();
}

@section Head {
Expand All @@ -24,18 +25,30 @@
<span class="desc" id="desc_slug">The part of the URL that identifies this blog post</span>
<br />

<label for="categories" class="label">Tags</label>
<input type="text" name="selecttag" id="selecttag" aria-describedby="desc_categories" list="taglist" placeholder="@string.Join(", ", Model.Categories)" />
<label for="categories" class="label">Categories</label>
<input type="text" name="selectcat" id="selectcat" aria-describedby="desc_categories" list="catlist" placeholder="@string.Join(", ", Model.Categories)" />
<input type="text" name="categories" id="categories" value="@string.Join(", ", Model.Categories)" hidden />
<span class="desc" id="desc_categories">Select, or build a comma separated list of keywords - to remove double the keyword</span>
<datalist id="taglist">
<datalist id="catlist">
@foreach (var cat in allCats)
{
<option value="@cat"/>
}
</datalist>
<br />

<label for="tags" class="label">Tags</label>
<input type="text" name="selecttag" id="selecttag" aria-describedby="desc_tags" list="taglist" placeholder="@string.Join(", ", Model.Tags)" />
<input type="text" name="tags" id="tags" value="@string.Join(", ", Model.Tags)" hidden />
<span class="desc" id="desc_tags">Select, or build a comma separated list of keywords - to remove double the keyword</span>
<datalist id="taglist">
@foreach (var tag in allTags)
{
<option value="@tag"/>
}
</datalist>
<br />

<label asp-for="@Model.Excerpt" class="label">Excerpt</label>
<textarea asp-for="@Model.Excerpt" rows="3" placeholder="Short description of blog post" aria-describedby="desc_excerpt">@Model.Excerpt</textarea>
<span class="desc" id="desc_excerpt">A brief description of the content of the post</span>
Expand Down
Loading

0 comments on commit 3a49385

Please sign in to comment.