Skip to content

Commit 30cccb3

Browse files
authored
Add most seen plates graph (#122)
* most seen * tests * clean up these handlers * usings also
1 parent fbdfb6c commit 30cccb3

12 files changed

Lines changed: 253 additions & 78 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using OpenAlprWebhookProcessor.Data;
3+
using System;
4+
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace OpenAlprWebhookProcessor.LicensePlates.GetMostSeenPlates
9+
{
10+
public class GetMostSeenPlatesHandler
11+
{
12+
private readonly ProcessorContext _processorContext;
13+
14+
public GetMostSeenPlatesHandler(ProcessorContext processorContext)
15+
{
16+
_processorContext = processorContext;
17+
}
18+
19+
public async Task<GetMostSeenPlatesResponse> HandleAsync(
20+
GetMostSeenPlatesRequest request,
21+
CancellationToken cancellationToken)
22+
{
23+
var aWeekAgo = DateTimeOffset.UtcNow.AddDays(-7).ToUnixTimeMilliseconds();
24+
25+
var results = await _processorContext.PlateGroups
26+
.AsNoTracking()
27+
.Where(x => x.ReceivedOnEpoch > aWeekAgo)
28+
.GroupBy(x => x.BestNumber)
29+
.Select(x => new MostSeenCount
30+
{
31+
PlateNumber = x.Key,
32+
Count = x.Count(),
33+
})
34+
.OrderByDescending(x => x.Count)
35+
.Take(10)
36+
.ToListAsync(cancellationToken);
37+
38+
return new GetMostSeenPlatesResponse()
39+
{
40+
Counts = results,
41+
};
42+
}
43+
}
44+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System;
2+
3+
namespace OpenAlprWebhookProcessor.LicensePlates.GetMostSeenPlates
4+
{
5+
public class GetMostSeenPlatesRequest
6+
{
7+
}
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Collections.Generic;
2+
3+
namespace OpenAlprWebhookProcessor.LicensePlates.GetMostSeenPlates
4+
{
5+
public class GetMostSeenPlatesResponse
6+
{
7+
public List<MostSeenCount> Counts { get; set; }
8+
}
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace OpenAlprWebhookProcessor.LicensePlates.GetMostSeenPlates
4+
{
5+
public class MostSeenCount
6+
{
7+
public string PlateNumber { get; set; }
8+
9+
public int Count { get; set; }
10+
}
11+
}

OpenAlprWebhookProcessor.Server/LicensePlates/LicensePlatesController.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using OpenAlprWebhookProcessor.LicensePlates.DeletePlate;
44
using OpenAlprWebhookProcessor.LicensePlates.Enricher;
55
using OpenAlprWebhookProcessor.LicensePlates.GetLicensePlateCounts;
6+
using OpenAlprWebhookProcessor.LicensePlates.GetMostSeenPlates;
67
using OpenAlprWebhookProcessor.LicensePlates.GetPlate;
78
using OpenAlprWebhookProcessor.LicensePlates.GetPlateFilters;
89
using OpenAlprWebhookProcessor.LicensePlates.GetStatistics;
@@ -23,6 +24,8 @@ public class LicensePlatesController : ControllerBase
2324

2425
private readonly GetLicensePlateCountsHandler _getLicensePlateCountsHandler;
2526

27+
private readonly GetMostSeenPlatesHandler _getMostSeenPlatesHandler;
28+
2629
private readonly DeleteLicensePlateGroupRequestHandler _deleteLicensePlateGroupHandler;
2730

2831
private readonly GetLicensePlateFiltersHandler _getLicensePlateFiltersHandler;
@@ -38,6 +41,7 @@ public class LicensePlatesController : ControllerBase
3841
public LicensePlatesController(
3942
SearchLicensePlateHandler searchLicensePlateHandler,
4043
GetLicensePlateCountsHandler getLicensePlateCountsHandler,
44+
GetMostSeenPlatesHandler getMostSeenPlatesHandler,
4145
DeleteLicensePlateGroupRequestHandler deleteLicensePlateGroupHandler,
4246
GetLicensePlateFiltersHandler getLicensePlateFiltersHandler,
4347
GetStatisticsHandler getStatisticsHandler,
@@ -47,6 +51,7 @@ public LicensePlatesController(
4751
{
4852
_searchLicensePlateHandler = searchLicensePlateHandler;
4953
_getLicensePlateCountsHandler = getLicensePlateCountsHandler;
54+
_getMostSeenPlatesHandler = getMostSeenPlatesHandler;
5055
_deleteLicensePlateGroupHandler = deleteLicensePlateGroupHandler;
5156
_getLicensePlateFiltersHandler = getLicensePlateFiltersHandler;
5257
_getStatisticsHandler = getStatisticsHandler;
@@ -103,6 +108,16 @@ public async Task<GetLicensePlateCountsResponse> GetLicensePlateCounts(Cancellat
103108
cancellationToken);
104109
}
105110

111+
[HttpGet("mostseen")]
112+
public async Task<GetMostSeenPlatesResponse> GetMostSeenPlates(CancellationToken cancellationToken)
113+
{
114+
var request = new GetMostSeenPlatesRequest();
115+
116+
return await _getMostSeenPlatesHandler.HandleAsync(
117+
request,
118+
cancellationToken);
119+
}
120+
106121
[HttpGet("filters")]
107122
public async Task<GetLicensePlateFiltersResponse> GetLicensePlateFilters(CancellationToken cancellationToken)
108123
{

OpenAlprWebhookProcessor.Server/Startup.cs

Lines changed: 20 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,36 @@
11
using AutoMapper;
22
using Hangfire;
3+
using Lib.Net.Http.WebPush;
34
using Microsoft.AspNetCore.Authentication.JwtBearer;
45
using Microsoft.AspNetCore.Builder;
5-
using Microsoft.AspNetCore.Hosting;
66
using Microsoft.AspNetCore.SignalR;
77
using Microsoft.EntityFrameworkCore;
88
using Microsoft.Extensions.Configuration;
99
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.DependencyInjection.Extensions;
1011
using Microsoft.Extensions.Hosting;
1112
using Microsoft.IdentityModel.Tokens;
1213
using OpenAlprWebhookProcessor.Alerts;
13-
using OpenAlprWebhookProcessor.Cameras;
14+
using OpenAlprWebhookProcessor.Alerts.Pushover;
1415
using OpenAlprWebhookProcessor.Data;
1516
using OpenAlprWebhookProcessor.Hydrator;
16-
using OpenAlprWebhookProcessor.ImageRelay;
17-
using OpenAlprWebhookProcessor.LicensePlates.DeletePlate;
18-
using OpenAlprWebhookProcessor.LicensePlates.GetLicensePlateCounts;
19-
using OpenAlprWebhookProcessor.LicensePlates.SearchLicensePlates;
20-
using OpenAlprWebhookProcessor.SystemLogs;
17+
using OpenAlprWebhookProcessor.LicensePlates.Enricher;
18+
using OpenAlprWebhookProcessor.LicensePlates.Enricher.LicensePlateData;
2119
using OpenAlprWebhookProcessor.ProcessorHub;
22-
using OpenAlprWebhookProcessor.Settings;
23-
using OpenAlprWebhookProcessor.Settings.GetIgnores;
24-
using OpenAlprWebhookProcessor.Settings.UpdatedCameras;
25-
using OpenAlprWebhookProcessor.Settings.UpsertWebhookForwards;
20+
using OpenAlprWebhookProcessor.SystemLogs;
2621
using OpenAlprWebhookProcessor.Users;
2722
using OpenAlprWebhookProcessor.Users.Data;
2823
using OpenAlprWebhookProcessor.Users.Register;
2924
using OpenAlprWebhookProcessor.WebhookProcessor;
25+
using OpenAlprWebhookProcessor.WebhookProcessor.OpenAlprAgentScraper;
26+
using OpenAlprWebhookProcessor.WebhookProcessor.OpenAlprWebsocket;
27+
using OpenAlprWebhookProcessor.WebPushSubscriptions;
3028
using Serilog;
3129
using System;
30+
using System.IO;
3231
using System.Linq;
32+
using System.Reflection;
3333
using System.Threading.Tasks;
34-
using OpenAlprWebhookProcessor.Cameras.ZoomAndFocus;
35-
using System.IO;
36-
using OpenAlprWebhookProcessor.WebhookProcessor.OpenAlprAgentScraper;
37-
using OpenAlprWebhookProcessor.LicensePlates.GetPlateFilters;
38-
using OpenAlprWebhookProcessor.Settings.AgentHydration;
39-
using OpenAlprWebhookProcessor.LicensePlates.GetStatistics;
40-
using OpenAlprWebhookProcessor.LicensePlates.UpsertPlate;
41-
using OpenAlprWebhookProcessor.Alerts.Pushover;
42-
using OpenAlprWebhookProcessor.Settings.Enrichers;
43-
using OpenAlprWebhookProcessor.LicensePlates.Enricher;
44-
using OpenAlprWebhookProcessor.LicensePlates.Enricher.LicensePlateData;
45-
using OpenAlprWebhookProcessor.Settings.GetDebugPlateGroups;
46-
using OpenAlprWebhookProcessor.Settings.GetDebubPlateGroups;
47-
using OpenAlprWebhookProcessor.WebPushSubscriptions;
48-
using Lib.Net.Http.WebPush;
49-
using OpenAlprWebhookProcessor.Alerts.WebPush;
50-
using OpenAlprWebhookProcessor.WebhookProcessor.OpenAlprWebsocket;
51-
using OpenAlprWebhookProcessor.Cameras.GetPlateCaptures;
52-
using OpenAlprWebhookProcessor.LicensePlates.GetPlate;
5334

5435
namespace OpenAlprWebhookProcessor
5536
{
@@ -148,52 +129,16 @@ public void ConfigureServices(IServiceCollection services)
148129
services.AddDbContext<UsersContext>(options =>
149130
options.UseSqlite(UsersContextConnectionString));
150131

151-
services.AddScoped<GroupWebhookHandler>();
152-
services.AddScoped<SinglePlateWebhookHandler>();
153-
services.AddScoped<GetAgentRequestHandler>();
154-
services.AddScoped<GetAgentStatusRequestHandler>();
155-
services.AddScoped<GetCameraRequestHandler>();
156-
services.AddScoped<SetZoomAndFocusHandler>();
157-
services.AddScoped<GetZoomAndFocusHandler>();
158-
services.AddScoped<DeleteCameraHandler>();
159-
services.AddScoped<UpsertIgnoresRequestHandler>();
160-
services.AddScoped<TestCameraHandler>();
161-
services.AddScoped<UpsertAgentRequestHandler>();
162-
services.AddScoped<GetAlertsRequestHandler>();
163-
services.AddScoped<GetIgnoresRequestHandler>();
164-
services.AddScoped<UpsertCameraHandler>();
165-
services.AddScoped<SearchLicensePlateHandler>();
166-
services.AddScoped<UpsertAlertsRequestHandler>();
167-
services.AddScoped<GetSnapshotHandler>();
168-
services.AddScoped<GetLicensePlateCountsHandler>();
169-
services.AddScoped<DeleteLicensePlateGroupRequestHandler>();
170-
services.AddScoped<GetWebhookForwardsRequestHandler>();
171-
services.AddScoped<UpsertWebhookForwardsRequestHandler>();
132+
var handlerTypes = Assembly.GetExecutingAssembly()
133+
.GetTypes()
134+
.Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("Handler"));
135+
136+
foreach (var handlerType in handlerTypes)
137+
{
138+
services.TryAddScoped(handlerType);
139+
}
140+
172141
services.AddScoped<OpenAlprAgentScraper>();
173-
services.AddScoped<GetLicensePlateFiltersHandler>();
174-
services.AddScoped<AgentScrapeRequestHandler>();
175-
services.AddScoped<GetStatisticsHandler>();
176-
services.AddScoped<UpsertPlateRequestHandler>();
177-
services.AddScoped<UpsertPushoverClientRequestHandler>();
178-
services.AddScoped<GetPushoverClientRequestHandler>();
179-
services.AddScoped<TestPushoverClientRequestHandler>();
180-
services.AddScoped<GetEnrichersRequestHandler>();
181-
services.AddScoped<UpsertEnricherRequestHandler>();
182-
services.AddScoped<TestEnricherRequestHandler>();
183-
services.AddScoped<EnrichLicensePlateRequestHandler>();
184-
services.AddScoped<GetDebugPlateGroupRequestHandler>();
185-
services.AddScoped<DeleteDebugPlateGroupRequestHandler>();
186-
services.AddScoped<TriggerAutofocusHandler>();
187-
services.AddScoped<UpsertCameraMaskHandler>();
188-
services.AddScoped<GetCameraMaskHandler>();
189-
services.AddScoped<DisableAgentRequestHandler>();
190-
services.AddScoped<EnableAgentRequestHandler>();
191-
services.AddScoped<GetPlateCapturesHandler>();
192-
services.AddScoped<GetPlateHandler>();
193-
194-
services.AddScoped<UpsertWebPushClientRequestHandler>();
195-
services.AddScoped<GetWebPushClientRequestHandler>();
196-
services.AddScoped<TestWebPushClientRequestHandler>();
197142

198143
services.AddScoped<ILicensePlateEnricherClient, LicensePlateDataClient>();
199144

OpenAlprWebhookProcessor.Server/WebhookProcessor/OpenAlprAgentScraper/OpenAlprAgentScraper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ public async Task ScrapeAgentAsync(CancellationToken cancellationToken)
7070
agent.EndpointUrl
7171
+ scrapeUrl
7272
.Replace("{0}", agent.LastSuccessfulScrapeEpoch.ToString())
73-
.Replace("{1}", agent.LastSuccessfulScrapeEpoch + millisecondsToScrape.ToString()),
74-
cancellationToken);
73+
.Replace("{1}", (agent.LastSuccessfulScrapeEpoch + millisecondsToScrape).ToString()),
74+
cancellationToken);
7575

7676
timer.Stop();
7777
_logger.LogInformation("Scraping took {seconds} seconds", timer.Elapsed.Seconds);

openalprwebhookprocessor.client/src/app/home/home.component.html

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,27 @@
2222
</div>
2323
</mat-card-content>
2424
</mat-card>
25+
<mat-card>
26+
<mat-card-header>
27+
<mat-card-title>Most Seen</mat-card-title>
28+
<mat-card-subtitle>Last 7 days</mat-card-subtitle>
29+
</mat-card-header>
30+
<mat-card-content>
31+
<div>
32+
<ngx-charts-bar-vertical
33+
[view]="view"
34+
[results]="mostSeenCounts"
35+
[xAxis]="true"
36+
[yAxis]="true"
37+
[showXAxisLabel]="true"
38+
[showYAxisLabel]="true"
39+
[xAxisLabel]="xAxisLabel"
40+
[yAxisLabel]="yAxisLabel"
41+
[showGridLines]="true"
42+
[showDataLabel]="true"
43+
[legend]="false"
44+
[tooltipDisabled]="true"></ngx-charts-bar-vertical>
45+
</div>
46+
</mat-card-content>
47+
</mat-card>
2548
</div>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing'
2+
import { HomeComponent } from './home.component'
3+
import { HomeService } from './home.service'
4+
import { of } from 'rxjs'
5+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
6+
import { MatCardModule } from '@angular/material/card'
7+
import { BarChartModule } from '@swimlane/ngx-charts'
8+
import { AccountService } from 'app/_services'
9+
import { User } from 'app/_models'
10+
import { DayCounts } from './plateCountResponse'
11+
import { MostSeenCounts } from './mostSeenResponse'
12+
13+
describe('HomeComponent', () => {
14+
let component: HomeComponent
15+
let fixture: ComponentFixture<HomeComponent>
16+
let mockHomeService: jasmine.SpyObj<HomeService>
17+
let mockAccountService: Partial<AccountService>
18+
19+
const testPlateData: DayCounts = {
20+
counts: [
21+
{ date: new Date('2025-06-20'), count: 5 },
22+
{ date: new Date('2025-06-21'), count: 8 },
23+
],
24+
weeklyUniqueCounts: [
25+
{ date: new Date('2025-06-20'), count: 5 },
26+
{ date: new Date('2025-06-21'), count: 8 },
27+
],
28+
}
29+
30+
const testMostSeenData: MostSeenCounts = {
31+
counts: [
32+
{ plateNumber: 'ABC123', count: 12 },
33+
{ plateNumber: 'XYZ789', count: 7 },
34+
],
35+
}
36+
37+
beforeEach(async () => {
38+
mockHomeService = jasmine.createSpyObj('HomeService', ['getPlatesCount', 'getMostSeenPlates'])
39+
mockHomeService.getPlatesCount.and.returnValue(of(testPlateData))
40+
mockHomeService.getMostSeenPlates.and.returnValue(of(testMostSeenData))
41+
42+
mockAccountService = {
43+
userValue: {
44+
username: 'testuser',
45+
} as User,
46+
}
47+
48+
await TestBed.configureTestingModule({
49+
imports: [
50+
HomeComponent,
51+
MatCardModule,
52+
BarChartModule,
53+
BrowserAnimationsModule,
54+
],
55+
providers: [
56+
{ provide: HomeService, useValue: mockHomeService },
57+
{ provide: AccountService, useValue: mockAccountService },
58+
],
59+
}).compileComponents()
60+
})
61+
62+
beforeEach(() => {
63+
fixture = TestBed.createComponent(HomeComponent)
64+
component = fixture.componentInstance
65+
fixture.detectChanges()
66+
})
67+
68+
it('should create', () => {
69+
expect(component).toBeTruthy()
70+
})
71+
72+
it('should load plateCounts on init', () => {
73+
expect(component.plateCounts.length).toBe(2)
74+
expect(component.plateCounts[0]).toEqual({
75+
name: new Date('2025-06-20'),
76+
value: 5,
77+
})
78+
})
79+
80+
it('should load mostSeenCounts on init', () => {
81+
expect(component.mostSeenCounts.length).toBe(2)
82+
expect(component.mostSeenCounts[0]).toEqual({
83+
name: 'ABC123',
84+
value: 12,
85+
})
86+
})
87+
88+
it('should call homeService methods once each', () => {
89+
expect(mockHomeService.getPlatesCount).toHaveBeenCalledTimes(1)
90+
expect(mockHomeService.getMostSeenPlates).toHaveBeenCalledTimes(1)
91+
})
92+
})

0 commit comments

Comments
 (0)