1
1
namespace OJS . Services . Administration . Business . Contests ;
2
2
3
3
using FluentExtensions . Extensions ;
4
+ using Microsoft . AspNetCore . Http ;
4
5
using Microsoft . EntityFrameworkCore ;
5
6
using Microsoft . Extensions . Options ;
7
+ using OJS . Common ;
6
8
using OJS . Common . Enumerations ;
7
9
using OJS . Data . Models ;
8
10
using OJS . Data . Models . Checkers ;
@@ -14,7 +16,6 @@ namespace OJS.Services.Administration.Business.Contests;
14
16
using OJS . Services . Administration . Business . Problems ;
15
17
using OJS . Services . Administration . Data ;
16
18
using OJS . Services . Administration . Models . Contests ;
17
- using OJS . Services . Common . Models ;
18
19
using OJS . Services . Infrastructure . Configurations ;
19
20
using System ;
20
21
using System . Collections . Generic ;
@@ -34,53 +35,76 @@ public class ContestsImportBusinessService(
34
35
ISubmissionTypesDataService submissionTypesData ,
35
36
ITestRunsDataService testRunsData ,
36
37
IProblemsBusinessService problemsBusiness ,
37
- IProblemGroupsBusinessService problemGroupsBusiness ) : IContestsImportBusinessService
38
+ IProblemGroupsBusinessService problemGroupsBusiness )
39
+ : IContestsImportBusinessService
38
40
{
39
41
private readonly HttpClient httpClient = httpClientFactory . CreateClient ( ) ;
40
42
private readonly ApplicationUrlsConfig urls = urlsConfig . Value ;
41
43
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 )
44
45
{
46
+ response . ContentType = GlobalConstants . MimeTypes . TextHtml ;
47
+
45
48
if ( sourceContestCategoryId == 0 || destinationContestCategoryId == 0 )
46
49
{
47
- return new ServiceResult < string > ( "Invalid contest category ids." ) ;
50
+ await response . WriteAsync ( "<p style='color:red'>Invalid contest category ids.</p>" ) ;
51
+ return ;
48
52
}
49
53
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
+
50
58
var contestIds = await this . httpClient . GetFromJsonAsync < int [ ] > ( $ "{ this . urls . LegacyJudgeUrl } /api/Contests/GetExistingIdsForCategory?contestCategoryId={ sourceContestCategoryId } &apiKey={ this . urls . LegacyJudgeApiKey } ") ;
51
59
52
60
if ( contestIds == null )
53
61
{
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 ;
55
65
}
56
66
57
67
var destinationContestCategory = await contestCategoriesData . OneById ( destinationContestCategoryId ) ;
58
68
59
69
if ( destinationContestCategory == null )
60
70
{
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 ;
62
74
}
63
75
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 ( ) ;
68
80
69
81
if ( dryRun )
70
82
{
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 ( ) ;
73
86
}
74
87
75
88
var checkers = await checkersData . All ( ) . ToListAsync ( ) ;
76
89
var submissionTypes = await submissionTypesData . All ( ) . ToListAsync ( ) ;
77
90
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
+
78
101
foreach ( var contestId in contestIds )
79
102
{
80
103
var contest = await this . httpClient . GetFromJsonAsync < ContestLegacyExportServiceModel > ( $ "{ this . urls . LegacyJudgeUrl } /api/Contests/Export/{ contestId } ?apiKey={ this . urls . LegacyJudgeApiKey } ") ;
81
104
if ( contest == null )
82
105
{
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 ( ) ;
84
108
continue ;
85
109
}
86
110
@@ -102,30 +126,47 @@ public async Task<ServiceResult<string>> ImportContestsFromCategory(int sourceCo
102
126
. ThenInclude ( sp => sp . SubmissionType )
103
127
. FirstOrDefaultAsync ( c => c . CategoryId == destinationContestCategoryId && c . Name == contest . Name ) ;
104
128
129
+ // Create a StringBuilder for this contest's results
130
+ var contestResult = new StringBuilder ( ) ;
131
+
105
132
if ( dryRun )
106
133
{
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>") ;
110
137
}
111
138
112
139
if ( existingContest == null )
113
140
{
114
- await this . ImportNewContest ( destinationContestCategoryId , contest , checkers , submissionTypes , result , dryRun ) ;
141
+ await this . ImportNewContest ( destinationContestCategoryId , contest , checkers , submissionTypes , contestResult , dryRun ) ;
115
142
}
116
143
else
117
144
{
118
- await this . UpdateContest ( existingContest , contest , checkers , submissionTypes , result , dryRun ) ;
145
+ await this . UpdateContest ( existingContest , contest , checkers , submissionTypes , contestResult , dryRun ) ;
119
146
}
120
- }
121
147
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
+ }
127
161
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 ( ) ;
129
170
}
130
171
131
172
private static DateTime ? ConvertTimeToUtc ( DateTime ? dateTime )
0 commit comments