Skip to content

Commit 222012e

Browse files
committed
Added streaming for contest import
1 parent 3fda0f7 commit 222012e

File tree

3 files changed

+77
-32
lines changed

3 files changed

+77
-32
lines changed

Servers/Administration/OJS.Servers.Administration/Extensions/WebApplicationExtensions.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public static WebApplication ConfigureWebApplication(this WebApplication app, IC
1919
app.UseCorsPolicy();
2020
app.UseDefaults();
2121

22+
// Enable serving static files from wwwroot folder
23+
app.UseStaticFiles();
24+
2225
app.UseMiddleware<AdministrationExceptionMiddleware>();
2326
app.MigrateDatabase<OjsDbContext>(configuration);
2427

@@ -30,20 +33,21 @@ public static WebApplication ConfigureWebApplication(this WebApplication app, IC
3033
.MapHealthChecksUI()
3134
.RequireAuthorization(auth => auth.RequireRole(Administrator));
3235

36+
// API endpoint for importing contests
3337
app.MapGet("/api/temp/ImportContestsFromCategory", async (
3438
IContestsImportBusinessService contestsImportBusinessService,
39+
HttpContext httpContext,
3540
int sourceContestCategoryId,
3641
int destinationContestCategoryId,
3742
bool dryRun = true) =>
3843
{
39-
var result = await contestsImportBusinessService.ImportContestsFromCategory(
44+
await contestsImportBusinessService.StreamImportContestsFromCategory(
4045
sourceContestCategoryId,
4146
destinationContestCategoryId,
47+
httpContext.Response,
4248
dryRun);
4349

44-
return result.IsError
45-
? Results.BadRequest(result.Error)
46-
: Results.Content(result.Data, GlobalConstants.MimeTypes.TextHtml);
50+
return Results.Empty;
4751
})
4852
.RequireAuthorization(auth => auth.RequireRole(Administrator))
4953
.WithRequestTimeout(TimeSpan.FromMinutes(10));

Services/Administration/OJS.Services.Administration.Business/Contests/ContestsImportBusinessService.cs

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
namespace OJS.Services.Administration.Business.Contests;
22

33
using FluentExtensions.Extensions;
4+
using Microsoft.AspNetCore.Http;
45
using Microsoft.EntityFrameworkCore;
56
using Microsoft.Extensions.Options;
7+
using OJS.Common;
68
using OJS.Common.Enumerations;
79
using OJS.Data.Models;
810
using OJS.Data.Models.Checkers;
@@ -14,7 +16,6 @@ namespace OJS.Services.Administration.Business.Contests;
1416
using OJS.Services.Administration.Business.Problems;
1517
using OJS.Services.Administration.Data;
1618
using OJS.Services.Administration.Models.Contests;
17-
using OJS.Services.Common.Models;
1819
using OJS.Services.Infrastructure.Configurations;
1920
using System;
2021
using System.Collections.Generic;
@@ -34,53 +35,76 @@ public class ContestsImportBusinessService(
3435
ISubmissionTypesDataService submissionTypesData,
3536
ITestRunsDataService testRunsData,
3637
IProblemsBusinessService problemsBusiness,
37-
IProblemGroupsBusinessService problemGroupsBusiness) : IContestsImportBusinessService
38+
IProblemGroupsBusinessService problemGroupsBusiness)
39+
: IContestsImportBusinessService
3840
{
3941
private readonly HttpClient httpClient = httpClientFactory.CreateClient();
4042
private readonly ApplicationUrlsConfig urls = urlsConfig.Value;
4143

42-
public async Task<ServiceResult<string>> ImportContestsFromCategory(int sourceContestCategoryId, int destinationContestCategoryId,
43-
bool dryRun = true)
44+
public async Task StreamImportContestsFromCategory(int sourceContestCategoryId, int destinationContestCategoryId, HttpResponse response, bool dryRun = true)
4445
{
46+
response.ContentType = GlobalConstants.MimeTypes.TextHtml;
47+
4548
if (sourceContestCategoryId == 0 || destinationContestCategoryId == 0)
4649
{
47-
return new ServiceResult<string>("Invalid contest category ids.");
50+
await response.WriteAsync("<p style='color:red'>Invalid contest category ids.</p>");
51+
return;
4852
}
4953

54+
await response.WriteAsync("<div id='import-results' style='font-family: Arial, sans-serif;'>");
55+
await response.WriteAsync("<h2>Import Process Started</h2>");
56+
await response.Body.FlushAsync();
57+
5058
var contestIds = await this.httpClient.GetFromJsonAsync<int[]>($"{this.urls.LegacyJudgeUrl}/api/Contests/GetExistingIdsForCategory?contestCategoryId={sourceContestCategoryId}&apiKey={this.urls.LegacyJudgeApiKey}");
5159

5260
if (contestIds == null)
5361
{
54-
return new ServiceResult<string>("Failed to get contest IDs.");
62+
await response.WriteAsync("<p style='color:red'>Failed to get contest IDs.</p>");
63+
await response.WriteAsync("</div>");
64+
return;
5565
}
5666

5767
var destinationContestCategory = await contestCategoriesData.OneById(destinationContestCategoryId);
5868

5969
if (destinationContestCategory == null)
6070
{
61-
return new ServiceResult<string>($"Destination contest category with id {destinationContestCategoryId} does not exist.");
71+
await response.WriteAsync($"<p style='color:red'>Destination contest category with id {destinationContestCategoryId} does not exist.</p>");
72+
await response.WriteAsync("</div>");
73+
return;
6274
}
6375

64-
var result = new StringBuilder();
65-
result.AppendLine(CultureInfo.InvariantCulture, $"<p>Importing contests from category #{sourceContestCategoryId} into category \"{destinationContestCategory.Name}\" #{destinationContestCategoryId}</p>");
66-
result.AppendLine(CultureInfo.InvariantCulture, $"<p>{contestIds.Length} contests will be imported. These are the source contest ids: <b>{string.Join(", ", contestIds)}</b></p>");
67-
result.AppendLine("<hr>");
76+
await response.WriteAsync($"<p>Importing contests from category #{sourceContestCategoryId} into category \"{destinationContestCategory.Name}\" #{destinationContestCategoryId}</p>");
77+
await response.WriteAsync($"<p>{contestIds.Length} contests will be imported. These are the source contest ids: <b>{string.Join(", ", contestIds)}</b></p>");
78+
await response.WriteAsync("<hr>");
79+
await response.Body.FlushAsync();
6880

6981
if (dryRun)
7082
{
71-
result.AppendLine("<b>Dry run is enabled. This will not import any contests.</b>");
72-
result.AppendLine("<hr>");
83+
await response.WriteAsync("<p><b style='color:blue'>Dry run is enabled. This will not import any contests.</b></p>");
84+
await response.WriteAsync("<hr>");
85+
await response.Body.FlushAsync();
7386
}
7487

7588
var checkers = await checkersData.All().ToListAsync();
7689
var submissionTypes = await submissionTypesData.All().ToListAsync();
7790

91+
// Add a progress counter
92+
var processedCount = 0;
93+
94+
await response.WriteAsync("<div id='progress-bar' style='margin: 10px 0; background-color: #f0f0f0; border-radius: 4px; padding: 2px;'>");
95+
await response.WriteAsync($"<div id='progress' style='background-color: #4CAF50; height: 20px; border-radius: 4px; width: 0%;'></div>");
96+
await response.WriteAsync("</div>");
97+
await response.WriteAsync($"<p id='progress-text'>Processed: 0 of {contestIds.Length}</p>");
98+
await response.WriteAsync("<div id='contest-results'>");
99+
await response.Body.FlushAsync();
100+
78101
foreach (var contestId in contestIds)
79102
{
80103
var contest = await this.httpClient.GetFromJsonAsync<ContestLegacyExportServiceModel>($"{this.urls.LegacyJudgeUrl}/api/Contests/Export/{contestId}?apiKey={this.urls.LegacyJudgeApiKey}");
81104
if (contest == null)
82105
{
83-
result.AppendLine(CultureInfo.InvariantCulture, $"<p><b>Skip:</b> Failed to get source contest <b>#{contestId}</b>. Skipping it...</p>");
106+
await response.WriteAsync($"<p><b style='color:orange'>Skip:</b> Failed to get source contest <b>#{contestId}</b>. Skipping it...</p>");
107+
await response.Body.FlushAsync();
84108
continue;
85109
}
86110

@@ -102,30 +126,47 @@ public async Task<ServiceResult<string>> ImportContestsFromCategory(int sourceCo
102126
.ThenInclude(sp => sp.SubmissionType)
103127
.FirstOrDefaultAsync(c => c.CategoryId == destinationContestCategoryId && c.Name == contest.Name);
104128

129+
// Create a StringBuilder for this contest's results
130+
var contestResult = new StringBuilder();
131+
105132
if (dryRun)
106133
{
107-
result.AppendLine(existingContest == null
108-
? $"<p><b>Import as new:</b> (src <b>#{contestId}</b>) Contest <b>\"{contest.Name}\"</b> will be imported as new contest.</p>"
109-
: $"<p><b>Update:</b> (src <b>#{contestId}</b>) Contest <b>\"{contest.Name}\"</b> already exists and will be updated.</p>");
134+
contestResult.AppendLine(existingContest == null
135+
? $"<p><b style='color:green'>Import as new:</b> (src <b>#{contestId}</b>) Contest <b>\"{contest.Name}\"</b> will be imported as new contest.</p>"
136+
: $"<p><b style='color:blue'>Update:</b> (src <b>#{contestId}</b>) Contest <b>\"{contest.Name}\"</b> already exists and will be updated.</p>");
110137
}
111138

112139
if (existingContest == null)
113140
{
114-
await this.ImportNewContest(destinationContestCategoryId, contest, checkers, submissionTypes, result, dryRun);
141+
await this.ImportNewContest(destinationContestCategoryId, contest, checkers, submissionTypes, contestResult, dryRun);
115142
}
116143
else
117144
{
118-
await this.UpdateContest(existingContest, contest, checkers, submissionTypes, result, dryRun);
145+
await this.UpdateContest(existingContest, contest, checkers, submissionTypes, contestResult, dryRun);
119146
}
120-
}
121147

122-
result.AppendLine("<hr>");
123-
result.AppendLine(dryRun
124-
? "<p>Dry run completed. To import, set dryRun to false.</p>"
125-
: "<p>Import completed.</p>");
126-
result.AppendLine("<hr>");
148+
// Write this contest's results to the response
149+
await response.WriteAsync(contestResult.ToString());
150+
151+
// Update progress
152+
processedCount++;
153+
var percentage = (double)processedCount / contestIds.Length * 100;
154+
155+
// Update progress bar with JavaScript
156+
await response.WriteAsync($"<script>document.getElementById('progress').style.width = '{percentage}%';</script>");
157+
await response.WriteAsync($"<script>document.getElementById('progress-text').innerText = 'Processed: {processedCount} of {contestIds.Length}';</script>");
158+
159+
await response.Body.FlushAsync();
160+
}
127161

128-
return ServiceResult<string>.Success(result.ToString());
162+
await response.WriteAsync("</div>"); // Close contest-results div
163+
await response.WriteAsync("<hr>");
164+
await response.WriteAsync(dryRun
165+
? "<p><b style='color:blue'>Dry run completed. To import, set dryRun to false.</b></p>"
166+
: "<p><b style='color:green'>Import completed successfully!</b></p>");
167+
await response.WriteAsync("<hr>");
168+
await response.WriteAsync("</div>"); // Close import-results div
169+
await response.Body.FlushAsync();
129170
}
130171

131172
private static DateTime? ConvertTimeToUtc(DateTime? dateTime)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
namespace OJS.Services.Administration.Business.Contests;
22

3-
using OJS.Services.Common.Models;
3+
using Microsoft.AspNetCore.Http;
44
using OJS.Services.Infrastructure;
55
using System.Threading.Tasks;
66

77
public interface IContestsImportBusinessService : IService
88
{
9-
Task<ServiceResult<string>> ImportContestsFromCategory(int sourceContestCategoryId, int destinationContestCategoryId, bool dryRun = true);
9+
Task StreamImportContestsFromCategory(int sourceContestCategoryId, int destinationContestCategoryId, HttpResponse response, bool dryRun = true);
1010
}

0 commit comments

Comments
 (0)