Skip to content

Commit fea9ba8

Browse files
authored
Merge pull request #38 from linkdotnet/feature/refactor-visit-page-count
Feature/refactor visit page count
2 parents 1812448 + 674eddd commit fea9ba8

File tree

11 files changed

+160
-123
lines changed

11 files changed

+160
-123
lines changed

LinkDotNet.Blog.IntegrationTests/SqlDatabaseTestBase.cs

-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ async Task IAsyncLifetime.DisposeAsync()
3737

3838
public async ValueTask DisposeAsync()
3939
{
40-
await DbContext.Database.EnsureDeletedAsync();
4140
await DbContext.DisposeAsync();
4241
}
4342

LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/Dashboard/DashboardServiceTests.cs

-34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Linq;
32
using System.Threading.Tasks;
43
using FluentAssertions;
54
using LinkDotNet.Blog.Domain;
@@ -97,38 +96,5 @@ public async Task ShouldGetAboutMeClicks()
9796
data.TotalAboutMeClicks.Should().Be(2);
9897
data.AboutMeClicksLast30Days.Should().Be(1);
9998
}
100-
101-
[Fact]
102-
public async Task ShouldGetBlogPostClicks()
103-
{
104-
var record1 = new UserRecord
105-
{
106-
UrlClicked = "blogPost/1",
107-
};
108-
var record2 = new UserRecord
109-
{
110-
UrlClicked = "blogPost/2",
111-
};
112-
var record3 = new UserRecord
113-
{
114-
UrlClicked = "blogPost/1",
115-
};
116-
var record4 = new UserRecord
117-
{
118-
UrlClicked = "unrelated",
119-
};
120-
await Repository.StoreAsync(record1);
121-
await Repository.StoreAsync(record2);
122-
await Repository.StoreAsync(record3);
123-
await Repository.StoreAsync(record4);
124-
125-
var data = (await sut.GetDashboardDataAsync()).BlogPostVisitCount.ToList();
126-
127-
data.Count.Should().Be(2);
128-
data[0].Key.Should().Be("blogPost/1");
129-
data[0].Value.Should().Be(2);
130-
data[1].Key.Should().Be("blogPost/2");
131-
data[1].Value.Should().Be(1);
132-
}
13399
}
134100
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
using System.Collections.Generic;
1+
using System;
22
using System.Linq;
33
using System.Threading.Tasks;
44
using AngleSharp.Html.Dom;
55
using Bunit;
66
using FluentAssertions;
77
using LinkDotNet.Blog.Domain;
8-
using LinkDotNet.Blog.Infrastructure.Persistence;
98
using LinkDotNet.Blog.TestUtilities;
109
using LinkDotNet.Blog.Web.Shared.Admin.Dashboard;
1110
using Microsoft.Extensions.DependencyInjection;
@@ -21,12 +20,10 @@ public async Task ShouldShowCounts()
2120
var blogPost = new BlogPostBuilder().WithTitle("I was clicked").WithLikes(2).Build();
2221
await Repository.StoreAsync(blogPost);
2322
using var ctx = new TestContext();
24-
ctx.Services.AddScoped<IRepository<BlogPost>>(_ => Repository);
25-
var visits = new List<KeyValuePair<string, int>> { new($"blogPost/{blogPost.Id}", 5) };
26-
var pageVisitCounts = visits.OrderByDescending(s => s.Value);
23+
ctx.Services.AddScoped(_ => DbContext);
24+
await SaveBlogPostArticleClicked(blogPost.Id, 10);
2725

28-
var cut = ctx.RenderComponent<VisitCountPerPage>(p => p.Add(
29-
s => s.PageVisitCount, pageVisitCounts));
26+
var cut = ctx.RenderComponent<VisitCountPerPage>();
3027

3128
cut.WaitForState(() => cut.FindAll("td").Any());
3229
var elements = cut.FindAll("td").ToList();
@@ -35,36 +32,54 @@ public async Task ShouldShowCounts()
3532
titleData.Should().NotBeNull();
3633
titleData.InnerHtml.Should().Be(blogPost.Title);
3734
titleData.Href.Should().Contain($"blogPost/{blogPost.Id}");
38-
elements[1].InnerHtml.Should().Be("5");
35+
elements[1].InnerHtml.Should().Be("10");
3936
elements[2].InnerHtml.Should().Be("2");
4037
}
4138

4239
[Fact]
43-
public void ShouldIgnoreNullForBlogPostVisits()
40+
public async Task ShouldFilterStartDate()
4441
{
42+
var blogPost1 = new BlogPostBuilder().WithTitle("1").WithLikes(2).Build();
43+
var blogPost2 = new BlogPostBuilder().WithTitle("2").WithLikes(2).Build();
44+
await Repository.StoreAsync(blogPost1);
45+
await Repository.StoreAsync(blogPost2);
46+
var urlClicked1New = new UserRecord
47+
{ UrlClicked = $"blogPost/{blogPost1.Id}", DateTimeUtcClicked = DateTime.UtcNow };
48+
var urlClicked1Old = new UserRecord
49+
{ UrlClicked = $"blogPost/{blogPost1.Id}", DateTimeUtcClicked = DateTime.MinValue };
50+
var urlClicked2 = new UserRecord
51+
{ UrlClicked = $"blogPost/{blogPost2.Id}", DateTimeUtcClicked = DateTime.MinValue };
52+
await DbContext.UserRecords.AddRangeAsync(new[] { urlClicked1New, urlClicked1Old, urlClicked2 });
53+
await DbContext.SaveChangesAsync();
4554
using var ctx = new TestContext();
46-
ctx.Services.AddScoped<IRepository<BlogPost>>(_ => Repository);
55+
ctx.Services.AddScoped(_ => DbContext);
56+
var cut = ctx.RenderComponent<VisitCountPerPage>();
4757

48-
var cut = ctx.RenderComponent<VisitCountPerPage>(p => p.Add(
49-
s => s.PageVisitCount, null));
58+
cut.FindComponent<DateRangeSelector>().Find("select").Change(DateTime.UtcNow.Date);
5059

60+
cut.WaitForState(() => cut.FindAll("td").Any());
5161
var elements = cut.FindAll("td").ToList();
52-
elements.Should().BeEmpty();
62+
elements.Count.Should().Be(3);
63+
var titleData = elements[0].ChildNodes.Single() as IHtmlAnchorElement;
64+
titleData.Should().NotBeNull();
65+
titleData.InnerHtml.Should().Be(blogPost1.Title);
66+
titleData.Href.Should().Contain($"blogPost/{blogPost1.Id}");
67+
elements[1].InnerHtml.Should().Be("1");
5368
}
5469

55-
[Fact]
56-
public void ShouldIgnoreNotBlogPosts()
70+
private async Task SaveBlogPostArticleClicked(string blogPostId, int count)
5771
{
58-
using var ctx = new TestContext();
59-
ctx.Services.AddScoped<IRepository<BlogPost>>(_ => Repository);
60-
var visits = new List<KeyValuePair<string, int>> { new("notablogpost", 5) };
61-
var pageVisitCounts = visits.OrderByDescending(s => s.Value);
72+
var urlClicked = $"blogPost/{blogPostId}";
73+
for (var i = 0; i < count; i++)
74+
{
75+
var data = new UserRecord
76+
{
77+
UrlClicked = urlClicked,
78+
};
79+
await DbContext.UserRecords.AddAsync(data);
80+
}
6281

63-
var cut = ctx.RenderComponent<VisitCountPerPage>(p => p.Add(
64-
s => s.PageVisitCount, pageVisitCounts));
65-
66-
var elements = cut.FindAll("td").ToList();
67-
elements.Should().BeEmpty();
82+
await DbContext.SaveChangesAsync();
6883
}
6984
}
7085
}

LinkDotNet.Blog.UnitTests/Web/Pages/Admin/DashboardTests.cs

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
using System.Linq;
1+
using System.Data.Common;
2+
using System.Linq;
23
using Bunit;
34
using Bunit.TestDoubles;
45
using FluentAssertions;
56
using LinkDotNet.Blog.Domain;
67
using LinkDotNet.Blog.Infrastructure.Persistence;
8+
using LinkDotNet.Blog.Infrastructure.Persistence.Sql;
79
using LinkDotNet.Blog.Web;
810
using LinkDotNet.Blog.Web.Pages.Admin;
911
using LinkDotNet.Blog.Web.Shared.Admin.Dashboard;
12+
using Microsoft.Data.Sqlite;
13+
using Microsoft.EntityFrameworkCore;
1014
using Microsoft.Extensions.DependencyInjection;
1115
using Moq;
1216
using Xunit;
@@ -18,11 +22,15 @@ public class DashboardTests : TestContext
1822
[Fact]
1923
public void ShouldNotShowAboutMeStatisticsWhenDisabled()
2024
{
25+
var options = new DbContextOptionsBuilder()
26+
.UseSqlite(CreateInMemoryConnection())
27+
.Options;
2128
var dashboardService = new Mock<IDashboardService>();
2229
this.AddTestAuthorization().SetAuthorized("test");
2330
Services.AddScoped(_ => CreateAppConfiguration(false));
2431
Services.AddScoped(_ => dashboardService.Object);
2532
Services.AddScoped(_ => new Mock<IRepository<BlogPost>>().Object);
33+
Services.AddScoped(_ => new BlogDbContext(options));
2634
dashboardService.Setup(d => d.GetDashboardDataAsync())
2735
.ReturnsAsync(new DashboardData());
2836

@@ -41,5 +49,14 @@ private static AppConfiguration CreateAppConfiguration(bool aboutMeEnabled)
4149
ProfileInformation = aboutMeEnabled ? new ProfileInformation() : null,
4250
};
4351
}
52+
53+
private static DbConnection CreateInMemoryConnection()
54+
{
55+
var connection = new SqliteConnection("Filename=:memory:");
56+
57+
connection.Open();
58+
59+
return connection;
60+
}
4461
}
4562
}

LinkDotNet.Blog.Web/Pages/Admin/Dashboard.razor

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
}
2323
</div>
2424
<div class="row mt-3">
25-
<div class="col-auto">
26-
<VisitCountPerPage PageVisitCount="@data.BlogPostVisitCount"></VisitCountPerPage>
27-
</div>
28-
</div>
25+
<div class="col-12">
26+
<VisitCountPerPage></VisitCountPerPage>
27+
</div>
28+
</div>
2929
</div>
3030
</div>
3131

LinkDotNet.Blog.Web/Pages/Admin/DashboardData.cs

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
using System.Collections.Generic;
2-
using System.Linq;
3-
4-
namespace LinkDotNet.Blog.Web.Pages.Admin
1+
namespace LinkDotNet.Blog.Web.Pages.Admin
52
{
63
public class DashboardData
74
{
@@ -13,8 +10,6 @@ public class DashboardData
1310

1411
public int PageClicksLast30Days { get; set; }
1512

16-
public IOrderedEnumerable<KeyValuePair<string, int>> BlogPostVisitCount { get; set; }
17-
1813
public int TotalAboutMeClicks { get; set; }
1914

2015
public int AboutMeClicksLast30Days { get; set; }

LinkDotNet.Blog.Web/Pages/Admin/DashboardService.cs

-12
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ public async Task<DashboardData> GetDashboardDataAsync()
3636
var aboutMeClicks = records.Count(r => r.UrlClicked.Contains("AboutMe"));
3737
var aboutMeClicksLast30Days = records.Count(r => r.UrlClicked.Contains("AboutMe") && r.DateTimeUtcClicked >= DateTime.UtcNow.AddDays(-30));
3838

39-
var visitCount = GetPageVisitCount(records);
40-
4139
return new DashboardData
4240
{
4341
TotalAmountOfUsers = users,
@@ -46,17 +44,7 @@ public async Task<DashboardData> GetDashboardDataAsync()
4644
PageClicksLast30Days = clicks30Days,
4745
TotalAboutMeClicks = aboutMeClicks,
4846
AboutMeClicksLast30Days = aboutMeClicksLast30Days,
49-
BlogPostVisitCount = visitCount,
5047
};
5148
}
52-
53-
private static IOrderedEnumerable<KeyValuePair<string, int>> GetPageVisitCount(IEnumerable<UserRecord> records)
54-
{
55-
return records
56-
.Where(u => u.UrlClicked.StartsWith("blogPost/"))
57-
.GroupBy(u => u.UrlClicked)
58-
.ToDictionary(k => k.Key, v => v.Count())
59-
.OrderByDescending(d => d.Value);
60-
}
6149
}
6250
}

LinkDotNet.Blog.Web/Shared/AccessControl.razor

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<li><h6 class="dropdown-header">Others</h6></li>
1717
<li><a class="dropdown-item" href="Sitemap">Sitemap</a></li>
1818
<li><hr class="dropdown-divider"></li>
19-
<li><a class="dropdown-item" href="https://github.com/linkdotnet/Blog/releases">Version 2.2</a></li>
19+
<li><a class="dropdown-item" href="https://github.com/linkdotnet/Blog/releases">Version 2.3</a></li>
2020
</ul>
2121
</li>
2222
<li class="nav-item"><a class="nav-link" href="logout">Log out</a></li>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<div class="col-1">
2+
<p>Since: </p>
3+
</div>
4+
<div class="col-2">
5+
<select @onchange="RaiseDateTimeSpanChanged">
6+
@foreach (var (title, time) in options)
7+
{
8+
<option value="@time">@title</option>
9+
}
10+
</select>
11+
</div>
12+
@code {
13+
[Parameter]
14+
public EventCallback<DateTime> DateTimeSpanChanged { get; set; }
15+
16+
private readonly Dictionary<string, DateTime> options = new()
17+
{
18+
{ "Beginning of time", DateTime.MinValue },
19+
{ "Last 90 Days", DateTime.UtcNow.AddDays(-90) },
20+
{ "Last 30 Days", DateTime.UtcNow.AddDays(-30) },
21+
{ "Last 7 Days", DateTime.UtcNow.AddDays(-7) },
22+
{ "Since Today", DateTime.UtcNow.Date },
23+
};
24+
25+
private async Task RaiseDateTimeSpanChanged(ChangeEventArgs args)
26+
{
27+
await DateTimeSpanChanged.InvokeAsync(DateTime.Parse(args.Value!.ToString()));
28+
}
29+
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace LinkDotNet.Blog.Web.Shared.Admin.Dashboard
2+
{
3+
public record VisitCountPageData
4+
{
5+
public string Id { get; init; }
6+
7+
public string Title { get; init; }
8+
9+
public int Likes { get; init; }
10+
11+
public int ClickCount { get; init; }
12+
}
13+
}

0 commit comments

Comments
 (0)