Skip to content

Commit 62d2356

Browse files
committed
Add project
1 parent 257064a commit 62d2356

34 files changed

Lines changed: 1410 additions & 0 deletions
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
namespace Neolution.OrchardCoreModules.PageViewStats.Controllers
2+
{
3+
using System;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Dapper;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Neolution.OrchardCoreModules.PageViewStats.Models;
9+
using Neolution.OrchardCoreModules.PageViewStats.Services;
10+
using OrchardCore.Data;
11+
using OrchardCore.Entities;
12+
using OrchardCore.Settings;
13+
using YesSql;
14+
15+
public class CountPageViewController : Controller
16+
{
17+
/// <summary>
18+
/// The table for storing the page views
19+
/// </summary>
20+
private string table;
21+
22+
/// <summary>
23+
/// The SQL dialect
24+
/// </summary>
25+
private ISqlDialect dialect;
26+
27+
private readonly ISession session;
28+
private readonly IDbConnectionAccessor dbConnectionAccessor;
29+
private readonly ISiteService siteService;
30+
private readonly IBotDetector botDetector;
31+
32+
public CountPageViewController(ISession session, IDbConnectionAccessor dbConnectionAccessor, ISiteService siteService, IBotDetector botDetector)
33+
{
34+
this.session = session;
35+
this.dbConnectionAccessor = dbConnectionAccessor;
36+
this.siteService = siteService;
37+
this.botDetector = botDetector;
38+
39+
SetupDatabaseFields();
40+
}
41+
42+
[HttpPost]
43+
[ActionName(nameof(Index))]
44+
public async Task<ActionResult> IndexPost(string contentItemId)
45+
{
46+
var settings = (await siteService.GetSiteSettingsAsync()).As<PageViewStatsSettings>();
47+
48+
if (!settings.IsEnabled)
49+
{
50+
// Ignore requests when page view statistics are disabled
51+
return Ok();
52+
}
53+
54+
var pageView = new PageView
55+
{
56+
ContentItemId = contentItemId,
57+
CreatedUtc = DateTimeOffset.UtcNow,
58+
};
59+
60+
if (settings.CollectUserIp)
61+
{
62+
pageView.RequestIpAddress = this.HttpContext.Connection.RemoteIpAddress?.ToString();
63+
}
64+
65+
if (settings.CollectUserAgentString)
66+
{
67+
pageView.RequestUserAgentString = this.Request.Headers.UserAgent.ToString();
68+
pageView.RequestUserAgentIsRobot = this.botDetector.CheckUserAgentString(pageView.RequestUserAgentString);
69+
}
70+
71+
await using var connection = this.dbConnectionAccessor.CreateConnection();
72+
73+
var insertCmd =
74+
$"INSERT INTO {dialect.QuoteForTableName(table)} (" +
75+
$" {dialect.QuoteForColumnName(nameof(PageView.Id))}, " +
76+
$" {dialect.QuoteForColumnName(nameof(PageView.CreatedUtc))}, " +
77+
$" {dialect.QuoteForColumnName(nameof(PageView.ContentItemId))}, " +
78+
$" {dialect.QuoteForColumnName(nameof(PageView.RequestIpAddress))}, " +
79+
$" {dialect.QuoteForColumnName(nameof(PageView.RequestUserAgentString))}, " +
80+
$" {dialect.QuoteForColumnName(nameof(PageView.RequestUserAgentIsRobot))}) " +
81+
$"VALUES (" +
82+
$" @{nameof(PageView.Id)}, " +
83+
$" @{nameof(PageView.CreatedUtc)}, " +
84+
$" @{nameof(PageView.ContentItemId)}, " +
85+
$" @{nameof(PageView.RequestIpAddress)}, " +
86+
$" @{nameof(PageView.RequestUserAgentString)}, " +
87+
$" @{nameof(PageView.RequestUserAgentIsRobot)});";
88+
89+
await connection.ExecuteAsync(insertCmd, pageView);
90+
91+
return Ok();
92+
}
93+
94+
private void SetupDatabaseFields()
95+
{
96+
dialect = session.Store.Configuration.SqlDialect;
97+
98+
var tablePrefix = session.Store.Configuration.TablePrefix;
99+
if (!string.IsNullOrEmpty(tablePrefix))
100+
{
101+
tablePrefix += '_';
102+
}
103+
104+
table = $"{tablePrefix}{PageView.TableName}";
105+
}
106+
}
107+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
namespace Neolution.OrchardCoreModules.PageViewStats.Controllers;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Dapper;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Mvc;
10+
using Neolution.OrchardCoreModules.PageViewStats.Models;
11+
using Neolution.OrchardCoreModules.PageViewStats.ViewModels;
12+
using OrchardCore.Admin;
13+
using OrchardCore.ContentManagement;
14+
using OrchardCore.Data;
15+
using YesSql;
16+
17+
[Admin]
18+
public class PageViewStatsController : Controller
19+
{
20+
private readonly ISession session;
21+
private readonly IDbConnectionAccessor dbConnectionAccessor;
22+
private readonly IAuthorizationService authorizationService;
23+
private readonly IContentManager contentManager;
24+
25+
private ISqlDialect dialect;
26+
private string table;
27+
28+
public PageViewStatsController(ISession session, IDbConnectionAccessor dbConnectionAccessor, IAuthorizationService authorizationService, IContentManager contentManager)
29+
{
30+
this.session = session;
31+
this.dbConnectionAccessor = dbConnectionAccessor;
32+
this.authorizationService = authorizationService;
33+
this.contentManager = contentManager;
34+
35+
SetupDatabaseFields();
36+
}
37+
38+
[HttpGet]
39+
public async Task<ActionResult> Index(int history)
40+
{
41+
if (!await this.authorizationService.AuthorizeAsync(User, PageViewStatsPermissions.ViewPageViewStats))
42+
{
43+
return Forbid();
44+
}
45+
46+
if (history > 30 || history <= 0)
47+
{
48+
// Limit history to maximum number of 30 days
49+
history = 30;
50+
}
51+
52+
await using var connection = this.dbConnectionAccessor.CreateConnection();
53+
54+
/*var insertCmd =
55+
$"INSERT INTO {dialect.QuoteForTableName(table)} ({dialect.QuoteForColumnName(nameof(Abstractions.PageView.CreatedUtc))}, {dialect.QuoteForColumnName(nameof(Abstractions.PageView.ContentItemId))}) " +
56+
$"VALUES (@{nameof(Abstractions.PageView.CreatedUtc)}, @{nameof(Abstractions.PageView.ContentItemId)});";*/
57+
58+
var viewModel = new PageViewStatsViewModel { History = history };
59+
60+
/*var selectCmd = $"SELECT COUNT(*) AS {dialect.QuoteForColumnName(nameof(GroupedPageView.Amount))}, {dialect.QuoteForColumnName(nameof(GroupedPageView.ContentItemId))} " +
61+
$"FROM {dialect.QuoteForTableName(table)} " +
62+
$"GROUP BY {dialect.QuoteForColumnName(nameof(GroupedPageView.ContentItemId))} " +
63+
$"ORDER BY {dialect.QuoteForColumnName(nameof(GroupedPageView.Amount))} DESC";*/
64+
65+
var selectCmd = $"SELECT " +
66+
$"[t2].[value2] AS {dialect.QuoteForColumnName(nameof(DateGroupedPageView.Date))}, " +
67+
$"[t2].[value] AS {dialect.QuoteForColumnName(nameof(DateGroupedPageView.Views))}, " +
68+
$"(SELECT COUNT(*) FROM {PageView.TableName} AS [t3] WHERE ([t3].{nameof(PageView.RequestUserAgentIsRobot)} = 1) AND ((([t2].[value2] IS NULL) AND (CONVERT(DATE, [t3].{nameof(PageView.CreatedUtc)}) IS NULL)) OR (([t2].[value2] IS NOT NULL) AND (CONVERT(DATE, [t3].{nameof(PageView.CreatedUtc)}) IS NOT NULL) AND ((([t2].[value2] IS NULL) AND (CONVERT(DATE, [t3].{nameof(PageView.CreatedUtc)}) IS NULL)) OR (([t2].[value2] IS NOT NULL) AND (CONVERT(DATE, [t3].{nameof(PageView.CreatedUtc)}) IS NOT NULL) AND ([t2].[value2] = CONVERT(DATE, [t3].{nameof(PageView.CreatedUtc)}))))))) AS {dialect.QuoteForColumnName(nameof(DateGroupedPageView.BotViews))}" +
69+
$"FROM (" +
70+
$"SELECT COUNT(*) AS [value], [t1].[value] AS [value2] " +
71+
$"FROM (SELECT CONVERT(DATE, [t0].{nameof(PageView.CreatedUtc)}) AS [value] FROM {PageView.TableName} AS [t0]) AS [t1] " +
72+
$"GROUP BY [t1].[value] " +
73+
$") AS [t2]";
74+
75+
var model = (await connection.QueryAsync<DateGroupedPageView>(selectCmd)).ToList();
76+
77+
var selectUniqueVisitors = $"SELECT DISTINCT " +
78+
$"[t1].{nameof(PageView.RequestIpAddress)}, [t1].{nameof(PageView.RequestUserAgentString)}, [t1].[value] AS {nameof(UniqueVisitorPageView.Date)} " +
79+
$"FROM (" +
80+
$"SELECT [t0].{nameof(PageView.RequestIpAddress)}, [t0].{nameof(PageView.RequestUserAgentString)}, CONVERT(DATE, [t0].{nameof(PageView.CreatedUtc)}) AS[value], [t0].{nameof(PageView.RequestUserAgentIsRobot)} " +
81+
$"FROM {PageView.TableName} AS[t0]) AS[t1] " +
82+
$"WHERE NOT([t1].{nameof(PageView.RequestUserAgentIsRobot)} = 1)";
83+
84+
var uniqueVisitors = (await connection.QueryAsync<UniqueVisitorPageView>(selectUniqueVisitors)).ToList();
85+
86+
viewModel.DateGroupedPageViews = new List<PageViewsPerDate>();
87+
foreach (var groupedPageViews in model)
88+
{
89+
var date = DateOnly.FromDateTime(groupedPageViews.Date.UtcDateTime);
90+
var uniqueVisitorsCount = uniqueVisitors.Count(x => DateOnly.FromDateTime(x.Date.UtcDateTime) == date);
91+
92+
var item = new PageViewsPerDate
93+
{
94+
Date = date,
95+
Views = groupedPageViews.Views,
96+
BotViews = groupedPageViews.BotViews,
97+
UniqueVisitors = uniqueVisitorsCount
98+
};
99+
100+
viewModel.DateGroupedPageViews.Add(item);
101+
}
102+
103+
// Filter entries
104+
var latestDate = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(history * -1);
105+
viewModel.DateGroupedPageViews = viewModel.DateGroupedPageViews.Where(x => x.Date >= latestDate).OrderByDescending(x => x.Date).ToList();
106+
107+
return View(viewModel);
108+
}
109+
110+
private void SetupDatabaseFields()
111+
{
112+
dialect = session.Store.Configuration.SqlDialect;
113+
114+
var tablePrefix = session.Store.Configuration.TablePrefix;
115+
if (!string.IsNullOrEmpty(tablePrefix))
116+
{
117+
tablePrefix += '_';
118+
}
119+
120+
table = $"{tablePrefix}{PageView.TableName}";
121+
}
122+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
namespace Neolution.OrchardCoreModules.PageViewStats.Drivers
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Http;
10+
using Neolution.OrchardCoreModules.PageViewStats.Models;
11+
using Neolution.OrchardCoreModules.PageViewStats.ViewModels;
12+
using OrchardCore.DisplayManagement.Entities;
13+
using OrchardCore.DisplayManagement.Handlers;
14+
using OrchardCore.DisplayManagement.Views;
15+
using OrchardCore.Settings;
16+
17+
public class PageViewStatsSiteSettingsDisplayDriver : SectionDisplayDriver<ISite, PageViewStatsSettings>
18+
{
19+
public const string GroupId = "pageViewStats";
20+
private readonly IHttpContextAccessor _httpContextAccessor;
21+
private readonly IAuthorizationService _authorizationService;
22+
23+
public PageViewStatsSiteSettingsDisplayDriver(IHttpContextAccessor httpContextAccessor, IAuthorizationService authorizationService)
24+
{
25+
_httpContextAccessor = httpContextAccessor;
26+
_authorizationService = authorizationService;
27+
}
28+
29+
public override async Task<IDisplayResult> EditAsync(PageViewStatsSettings settings, BuildEditorContext context)
30+
{
31+
var user = _httpContextAccessor.HttpContext?.User;
32+
33+
if (!await _authorizationService.AuthorizeAsync(user, PageViewStatsPermissions.ManagePageViewStats))
34+
{
35+
return null;
36+
}
37+
38+
return Initialize<PageViewStatsSettingsViewModel>("PageViewStatsSettings_Edit", m =>
39+
{
40+
m.IsEnabled = settings.IsEnabled;
41+
m.CollectUserIp = settings.CollectUserIp;
42+
m.CollectUserAgentString = settings.CollectUserAgentString;
43+
})
44+
.Location("Content:1").OnGroup(GroupId);
45+
}
46+
47+
public override async Task<IDisplayResult> UpdateAsync(PageViewStatsSettings settings, BuildEditorContext context)
48+
{
49+
var user = _httpContextAccessor.HttpContext?.User;
50+
51+
if (!await _authorizationService.AuthorizeAsync(user, PageViewStatsPermissions.ManagePageViewStats))
52+
{
53+
return null;
54+
}
55+
56+
if (context.GroupId == GroupId)
57+
{
58+
var model = new PageViewStatsSettingsViewModel();
59+
60+
await context.Updater.TryUpdateModelAsync(model, Prefix);
61+
62+
settings.IsEnabled = model.IsEnabled;
63+
settings.CollectUserIp = model.CollectUserIp;
64+
settings.CollectUserAgentString = model.CollectUserAgentString;
65+
}
66+
67+
return await EditAsync(settings, context);
68+
}
69+
}
70+
}

GitVersion.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
mode: ContinuousDelivery
2+
branches:
3+
feature:
4+
mode: ContinuousDeployment
5+
ignore:
6+
sha: []
7+
merge-message-formats: {}

Manifest.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using OrchardCore.Modules.Manifest;
2+
3+
[assembly: Module(
4+
Name = "PageViewStats",
5+
Author = "Neolution AG",
6+
Website = "https://www.neolution.ch/",
7+
Version = "0.0.1",
8+
Description = "A simple module that counts page views",
9+
Category = "Analytics"
10+
)]

Migrations.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Neolution.OrchardCoreModules.PageViewStats
2+
{
3+
using System;
4+
using Neolution.OrchardCoreModules.PageViewStats.Models;
5+
using OrchardCore.Data.Migration;
6+
7+
public class Migrations : DataMigration
8+
{
9+
public int Create()
10+
{
11+
SchemaBuilder.CreateTable(PageView.TableName, table => table
12+
.Column<Guid>(nameof(PageView.Id), col => col.PrimaryKey())
13+
.Column<string>(nameof(PageView.ContentItemId), c => c.WithLength(26))
14+
.Column<string>(nameof(PageView.RequestIpAddress), c => c.WithLength(45))
15+
.Column<string>(nameof(PageView.RequestUserAgentString))
16+
.Column<bool?>(nameof(PageView.RequestUserAgentIsRobot))
17+
.Column<DateTimeOffset>(nameof(PageView.CreatedUtc), col => col.NotNull())
18+
);
19+
20+
SchemaBuilder.AlterTable(PageView.TableName, table => table
21+
.CreateIndex($"IDX_{PageView.TableName}_{nameof(PageView.ContentItemId)}", nameof(PageView.ContentItemId))
22+
);
23+
24+
return 1;
25+
}
26+
}
27+
}

Models/DailyArchive.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Neolution.OrchardCoreModules.PageViewStats.Models;
2+
3+
using System;
4+
5+
public class DailyArchive
6+
{
7+
public DateOnly Date { get; set; }
8+
public int Views { get; set; }
9+
public int BotViews { get; set; }
10+
public int UniqueVisitors { get; set; }
11+
}

Models/MonthlyArchive.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Neolution.OrchardCoreModules.PageViewStats.Models;
2+
3+
using System.Collections.Generic;
4+
using OrchardCore.Data.Documents;
5+
6+
public class MonthlyArchive : Document
7+
{
8+
public int Month { get; set; }
9+
10+
public int Year { get; set; }
11+
12+
public ICollection<DailyArchive> Summaries { get; set; }
13+
}

0 commit comments

Comments
 (0)