Skip to content

Commit 9ee1359

Browse files
WIP (move controller logic to import manager)
1 parent d97efd3 commit 9ee1359

17 files changed

Lines changed: 346 additions & 6 deletions

File tree

Data/Models/Athlete.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ namespace FLRC.Leaderboards.Data.Models;
22

33
public sealed record Athlete
44
{
5-
public Guid ID { get; init; }
6-
public string Name { get; init; } = null!;
7-
public char Category { get; init; }
8-
public DateOnly DateOfBirth { get; init; }
9-
public bool IsPrivate { get; init; }
5+
public Guid ID { get; set; }
6+
public string Name { get; set; } = null!;
7+
public char Category { get; set; }
8+
public DateOnly DateOfBirth { get; set; }
9+
public bool IsPrivate { get; set; }
1010

1111
public ICollection<Result> Results { get; init; } = [];
1212
public ICollection<LinkedAccount> LinkedAccounts { get; init; } = [];

Importer/App.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.IO.Abstractions;
2+
using System.Text.Json;
3+
using Microsoft.Extensions.Hosting;
4+
5+
namespace FLRC.Leaderboards.Importer;
6+
7+
public sealed class App(IFileSystem fs, Importer importer, Action<string> log) : IHostedService
8+
{
9+
public async Task StartAsync(CancellationToken cancellationToken)
10+
{
11+
log("Starting...");
12+
var data = await fs.File.ReadAllTextAsync("Challenge.json", cancellationToken);
13+
var courses = JsonSerializer.Deserialize<Course[]>(data)!;
14+
await importer.Run(courses);
15+
log("Done!");
16+
}
17+
18+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
19+
}

Importer/Challenge.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
[
2+
{
3+
"Name": "Sweet 1600",
4+
"Type": "WebScorer",
5+
"ExternalID": 386605
6+
},
7+
{
8+
"Name": "Mulholland Waterfalls",
9+
"Type": "WebScorer",
10+
"ExternalID": 386607
11+
},
12+
{
13+
"Name": "Lakefront Loops 5K",
14+
"Type": "WebScorer",
15+
"ExternalID": 386606
16+
},
17+
{
18+
"Name": "Cayuga Cliffs",
19+
"Type": "WebScorer",
20+
"ExternalID": 386608
21+
},
22+
{
23+
"Name": "Fall Creek Trails",
24+
"Type": "WebScorer",
25+
"ExternalID": 386609
26+
},
27+
{
28+
"Name": "Town & Gown Up & Down",
29+
"Type": "WebScorer",
30+
"ExternalID": 386610
31+
},
32+
{
33+
"Name": "Abbott Ascent",
34+
"Type": "WebScorer",
35+
"ExternalID": 386611
36+
},
37+
{
38+
"Name": "Black Diamond Cass to Gorge",
39+
"Type": "WebScorer",
40+
"ExternalID": 386612
41+
},
42+
{
43+
"Name": "Triple Hump",
44+
"Type": "WebScorer",
45+
"ExternalID": 386613
46+
},
47+
{
48+
"Name": "North Country Half",
49+
"Type": "WebScorer",
50+
"ExternalID": 386614
51+
}
52+
]

Importer/Course.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace FLRC.Leaderboards.Importer;
2+
3+
public sealed record Course
4+
{
5+
public required string Name { get; init; }
6+
public required string Type { get; init; }
7+
public required uint ExternalID { get; init; }
8+
}

Importer/Importer.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using FLRC.Leaderboards.Web.Areas.Admin.Controllers;
2+
using FLRC.Leaderboards.Web.Areas.Admin.Services;
3+
4+
namespace FLRC.Leaderboards.Importer;
5+
6+
public sealed class Importer(ResultsController resultsController, IRaceService raceService, Action<string> log)
7+
{
8+
public async Task Run(Course[] definitions)
9+
{
10+
log("Getting all courses...");
11+
var races = await raceService.GetAllRaces();
12+
var courses = races.SelectMany(r => r.Courses).ToArray();
13+
log($"{courses.Length} courses found!");
14+
15+
foreach (var definition in definitions)
16+
{
17+
var course = courses.FirstOrDefault(c => c.Race.Name == definition.Name);
18+
if (course is null)
19+
{
20+
log($"No course found for \"{definition.Name}\"!");
21+
continue;
22+
}
23+
24+
await resultsController.Import(course.ID, definition.Type, definition.ExternalID);
25+
log($"Imported \"{definition.Name}\"!");
26+
}
27+
}
28+
}

Importer/Importer.csproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<RootNamespace>FLRC.Leaderboards.Importer</RootNamespace>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<None Include="Challenge.json" CopyToOutputDirectory="PreserveNewest"/>
13+
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0"/>
14+
<ProjectReference Include="..\Web\Web.csproj"/>
15+
</ItemGroup>
16+
17+
</Project>

Importer/Program.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using FLRC.Leaderboards.Importer;
2+
using FLRC.Leaderboards.Web.Areas.Admin.Controllers;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
6+
var builder = Host.CreateDefaultBuilder(args);
7+
builder.ConfigureServices((_, services) =>
8+
{
9+
FLRC.Leaderboards.Web.App.ConfigureServices(services);
10+
services.AddSingleton<Action<string>>(Console.WriteLine);
11+
services.AddSingleton<ResultsController>();
12+
services.AddSingleton<Importer>();
13+
services.AddHostedService<App>();
14+
}
15+
);
16+
17+
var app = builder.Build();
18+
await app.StartAsync();
19+
await app.StopAsync();
20+
await app.WaitForShutdownAsync();

Leaderboards.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Track", "Track\Track.csproj
1919
EndProject
2020
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj", "{1E6AD31A-96CF-4F58-B320-7B0D12E1D447}"
2121
EndProject
22+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Importer", "Importer\Importer.csproj", "{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}"
23+
EndProject
2224
Global
2325
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2426
Debug|Any CPU = Debug|Any CPU
@@ -125,6 +127,18 @@ Global
125127
{1E6AD31A-96CF-4F58-B320-7B0D12E1D447}.Release|x64.Build.0 = Release|Any CPU
126128
{1E6AD31A-96CF-4F58-B320-7B0D12E1D447}.Release|x86.ActiveCfg = Release|Any CPU
127129
{1E6AD31A-96CF-4F58-B320-7B0D12E1D447}.Release|x86.Build.0 = Release|Any CPU
130+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
131+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Debug|Any CPU.Build.0 = Debug|Any CPU
132+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Debug|x64.ActiveCfg = Debug|Any CPU
133+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Debug|x64.Build.0 = Debug|Any CPU
134+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Debug|x86.ActiveCfg = Debug|Any CPU
135+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Debug|x86.Build.0 = Debug|Any CPU
136+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Release|Any CPU.ActiveCfg = Release|Any CPU
137+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Release|Any CPU.Build.0 = Release|Any CPU
138+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Release|x64.ActiveCfg = Release|Any CPU
139+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Release|x64.Build.0 = Release|Any CPU
140+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Release|x86.ActiveCfg = Release|Any CPU
141+
{FD4898F2-B895-49D0-BE2E-30D82BDCDD52}.Release|x86.Build.0 = Release|Any CPU
128142
EndGlobalSection
129143
GlobalSection(SolutionProperties) = preSolution
130144
HideSolutionNode = FALSE

Web/App.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,11 @@ public static void ConfigureServices(IServiceCollection services)
6565
{ nameof(WebScorer), s.GetRequiredService<ResultsAPI<WebScorer>>() }
6666
});
6767

68+
services.AddScoped<IAthleteService, AthleteService>();
69+
services.AddScoped<ICourseService, CourseService>();
6870
services.AddScoped<IIterationService, IterationService>();
6971
services.AddScoped<IRaceService, RaceService>();
72+
services.AddScoped<IResultService, ResultService>();
7073
services.AddScoped<ISeriesService, SeriesService>();
7174
}
7275

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using FLRC.Leaderboards.Core.Data;
2+
using FLRC.Leaderboards.Data.Models;
3+
using FLRC.Leaderboards.Web.Areas.Admin.Services;
4+
using FLRC.Leaderboards.Web.Areas.Admin.ViewModels;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace FLRC.Leaderboards.Web.Areas.Admin.Controllers;
8+
9+
[Area("Admin")]
10+
public sealed class ResultsController(IAthleteService athleteService, ICourseService courseService, IRaceService raceService, IResultService resultService, IDictionary<string, IResultsAPI> resultsAPI) : Controller
11+
{
12+
private static readonly DateOnly UnknownDOB = new();
13+
private const char UnknownCategory = ' ';
14+
15+
public async Task<ViewResult> Index(Guid id)
16+
{
17+
var results = await resultService.Find(id);
18+
var vm = new ViewModel<Result[]>("Course Results", results);
19+
return View(vm);
20+
}
21+
22+
[HttpGet]
23+
public async Task<ViewResult> Import()
24+
{
25+
var races = await raceService.GetAllRaces();
26+
var form = new ResultImportForm
27+
{
28+
Races = races,
29+
Sources = resultsAPI.Keys.ToArray()
30+
};
31+
var vm = new ViewModel<ResultImportForm>("Results Importer", form);
32+
return View(vm);
33+
}
34+
35+
[HttpPost]
36+
public async Task<RedirectToActionResult> Import(Guid courseID, string source, uint externalID)
37+
{
38+
var importer = resultsAPI[source];
39+
var response = importer.GetResults(externalID);
40+
41+
var legacyResults = importer.Source.ParseCourse(null, await response, null);
42+
var results = await ConvertLegacyResults(courseID, source, legacyResults);
43+
44+
await resultService.Import(results);
45+
return RedirectToAction(nameof(Index), courseID);
46+
}
47+
48+
private async Task<Result[]> ConvertLegacyResults(Guid courseID, string source, Core.Results.Result[] legacyResults)
49+
{
50+
var results = new List<Result>();
51+
foreach (var legacyResult in legacyResults.Where(r => r.Duration is not null))
52+
{
53+
var result = await ConvertLegacyResult(courseID, source, legacyResult);
54+
results.Add(result);
55+
}
56+
57+
return results.ToArray();
58+
}
59+
60+
private async Task<Result> ConvertLegacyResult(Guid courseID, string source, Core.Results.Result result)
61+
{
62+
var athlete = await athleteService.Find(source, result.Athlete.ID.ToString());
63+
if (athlete is null && result.Athlete.DateOfBirth.HasValue)
64+
athlete = await athleteService.Find(result.Athlete.Name, DateOnly.FromDateTime(result.Athlete.DateOfBirth.Value));
65+
66+
var newAthlete = ConvertLegacyAthlete(source, result.Athlete);
67+
if (athlete is null)
68+
{
69+
athlete = newAthlete;
70+
await athleteService.AddAthlete(athlete);
71+
}
72+
73+
if ((athlete.DateOfBirth == UnknownDOB && newAthlete.DateOfBirth != UnknownDOB)
74+
|| (athlete.Category == UnknownCategory && newAthlete.Category != UnknownCategory))
75+
await athleteService.UpdateAthlete(athlete, newAthlete);
76+
77+
return new Result
78+
{
79+
ID = Guid.NewGuid(),
80+
AthleteID = athlete.ID,
81+
CourseID = courseID,
82+
StartTime = result.StartTime.Value.ToUniversalTime(),
83+
Duration = result.Duration.Value
84+
};
85+
}
86+
87+
private static Athlete ConvertLegacyAthlete(string source, Core.Athletes.Athlete athlete)
88+
=> new()
89+
{
90+
ID = Guid.NewGuid(),
91+
Name = athlete.Name,
92+
DateOfBirth = athlete.DateOfBirth.HasValue ? DateOnly.FromDateTime(athlete.DateOfBirth.Value) : UnknownDOB,
93+
Category = athlete.Category?.Display[0] ?? UnknownCategory,
94+
IsPrivate = athlete.Private,
95+
LinkedAccounts = [new LinkedAccount { ID = Guid.NewGuid(), Type = source, Value = athlete.ID.ToString() }]
96+
};
97+
}

0 commit comments

Comments
 (0)