Skip to content

Commit 7e32b27

Browse files
authored
SF-3759 Add Slope and Intercept input to Serval Admin (#3766)
1 parent d753095 commit 7e32b27

15 files changed

Lines changed: 688 additions & 39 deletions

File tree

src/RealtimeServer/scriptureforge/models/translate-config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ export interface DraftUsfmConfig {
5858
quoteFormat: QuoteFormat;
5959
}
6060

61+
/**
62+
* The configuration used for Quality Estimation.
63+
*/
64+
export interface QualityEstimationConfig {
65+
version: string;
66+
slope: number;
67+
intercept: number;
68+
}
69+
6170
export interface DraftConfig {
6271
draftingSources: TranslateSource[];
6372
trainingSources: TranslateSource[];
@@ -71,6 +80,7 @@ export interface DraftConfig {
7180
sendEmailOnBuildFinished?: boolean;
7281
currentScriptureRange?: string;
7382
draftedScriptureRange?: string;
83+
qualityEstimationConfig?: QualityEstimationConfig;
7484
}
7585

7686
export interface TranslateConfig {

src/RealtimeServer/scriptureforge/services/sf-project-service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,21 @@ export class SFProjectService extends ProjectService<SFProject> {
275275
},
276276
draftedScriptureRange: {
277277
bsonType: 'string'
278+
},
279+
qualityEstimationConfig: {
280+
bsonType: 'object',
281+
properties: {
282+
version: {
283+
bsonType: 'string'
284+
},
285+
slope: {
286+
bsonType: 'number'
287+
},
288+
intercept: {
289+
bsonType: 'number'
290+
}
291+
},
292+
additionalProperties: false
278293
}
279294
},
280295
additionalProperties: false

src/SIL.XForge.Scripture/ClientApp/src/app/core/sf-project.service.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,19 @@ describe('SFProjectService', () => {
180180
}));
181181
});
182182

183+
describe('onlineSetQualityEstimationConfig', () => {
184+
it('should invoke the command service', fakeAsync(async () => {
185+
const env = new TestEnvironment();
186+
await env.service.onlineSetQualityEstimationConfig('project01', {
187+
version: '0.1',
188+
slope: 109.6145,
189+
intercept: -14.0633
190+
});
191+
verify(mockedCommandService.onlineInvoke(anything(), 'setQualityEstimationConfig', anything())).once();
192+
expect().nothing();
193+
}));
194+
});
195+
183196
class TestEnvironment {
184197
readonly httpTestingController: HttpTestingController;
185198
readonly service: SFProjectService;

src/SIL.XForge.Scripture/ClientApp/src/app/core/sf-project.service.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scri
1414
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
1515
import { getSFProjectUserConfigDocId } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-user-config';
1616
import { TextAudio } from 'realtime-server/lib/esm/scriptureforge/models/text-audio';
17-
import { DraftUsfmConfig } from 'realtime-server/lib/esm/scriptureforge/models/translate-config';
17+
import {
18+
DraftUsfmConfig,
19+
QualityEstimationConfig
20+
} from 'realtime-server/lib/esm/scriptureforge/models/translate-config';
1821
import { Subject } from 'rxjs';
1922
import { CommandService } from 'xforge-common/command.service';
2023
import { LocationService } from 'xforge-common/location.service';
@@ -362,6 +365,16 @@ export class SFProjectService extends ProjectService<SFProject, SFProjectDoc> {
362365
});
363366
}
364367

368+
async onlineSetQualityEstimationConfig(
369+
projectId: string,
370+
qualityEstimationConfig: QualityEstimationConfig | null
371+
): Promise<void> {
372+
return await this.onlineInvoke<void>('setQualityEstimationConfig', {
373+
projectId,
374+
qualityEstimationConfig
375+
});
376+
}
377+
365378
async onlineSetServalConfig(projectId: string, servalConfig: string | null | undefined): Promise<void> {
366379
return await this.onlineInvoke<void>('setServalConfig', {
367380
projectId,

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.html

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,56 @@ <h2>Pre-Translation Configuration</h2>
5959
</mat-card-header>
6060
<mat-card-content>
6161
<p>
62-
This value must be a valid JSON string. See the
62+
This value must be valid JSON. See the
6363
<a href="https://github.com/sillsdev/serval/wiki/NMT-Build-Options" target="_blank">Serval Wiki</a> for
6464
configuration values.
6565
</p>
6666
<mat-form-field [formGroup]="form" appearance="outline" class="serval-config-field">
6767
<mat-label>NMT Build Options Override (JSON)</mat-label>
6868
<textarea matInput cdkTextareaAutosize id="serval-config" formControlName="servalConfig"></textarea>
69-
<app-write-status [state]="updateState" [formGroup]="form" id="serval-config-status"></app-write-status>
69+
<app-write-status
70+
[state]="servalConfigUpdateState"
71+
[formGroup]="form"
72+
id="serval-config-status"
73+
></app-write-status>
7074
</mat-form-field>
7175
</mat-card-content>
7276
<mat-card-actions>
7377
<button mat-flat-button id="save-serval-config" color="primary" (click)="updateServalConfig()">Save</button>
7478
</mat-card-actions>
7579
</mat-card>
80+
<mat-card>
81+
<mat-card-header>
82+
<mat-card-title>Quality Estimation Configuration</mat-card-title>
83+
</mat-card-header>
84+
<mat-card-content>
85+
<p>This value must be valid JSON, and will be in the file linregress.json created by quality_estimation.py.</p>
86+
<mat-form-field [formGroup]="form" appearance="outline" class="quality-estimation-config-field">
87+
<mat-label>Quality Estimation Configuration (JSON)</mat-label>
88+
<textarea
89+
matInput
90+
cdkTextareaAutosize
91+
id="quality-estimation-config"
92+
formControlName="qualityEstimationConfig"
93+
></textarea>
94+
<app-write-status
95+
[state]="qualityEstimationConfigUpdateState"
96+
[formGroup]="form"
97+
id="quality-estimation-config-status"
98+
></app-write-status>
99+
</mat-form-field>
100+
</mat-card-content>
101+
<mat-card-actions>
102+
<button
103+
mat-flat-button
104+
id="save-quality-estimation-config"
105+
color="primary"
106+
(click)="updateQualityEstimationConfig()"
107+
>
108+
Save
109+
</button>
110+
</mat-card-actions>
111+
</mat-card>
76112
</div>
77113
</div>
78114

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
flex-direction: row;
99
column-gap: 10px;
1010

11-
.serval-config-field {
11+
.serval-config-field,
12+
.quality-estimation-config-field {
1213
width: 100%;
1314
}
1415
}

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.spec.ts

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { BehaviorSubject, of, throwError } from 'rxjs';
1212
import { anything, instance, mock, verify, when } from 'ts-mockito';
1313
import { ActivatedProjectService } from 'xforge-common/activated-project.service';
1414
import { AuthService } from 'xforge-common/auth.service';
15+
import { CommandError, CommandErrorCode } from 'xforge-common/command.service';
1516
import { FileService } from 'xforge-common/file.service';
1617
import { I18nService } from 'xforge-common/i18n.service';
1718
import { FileType } from 'xforge-common/models/file-offline-data';
@@ -308,6 +309,120 @@ describe('ServalProjectComponent', () => {
308309
verify(mockSFProjectService.onlineSetServalConfig(env.mockProjectId, anything())).never();
309310
expect(env.statusDone(env.servalConfigStatus)).toBeNull();
310311
}));
312+
313+
it('should notify of a backend error', fakeAsync(() => {
314+
const env = new TestEnvironment();
315+
when(mockSFProjectService.onlineSetServalConfig(env.mockProjectId, anything())).thenReject(
316+
new CommandError(CommandErrorCode.InternalError, 'error')
317+
);
318+
expect(env.servalConfigTextArea.value).toBe('');
319+
expect(env.statusError(env.servalConfigStatus)).toBeNull();
320+
321+
env.setServalConfigValue('{}');
322+
env.clickElement(env.saveServalConfigButton);
323+
324+
verify(mockSFProjectService.onlineSetServalConfig(env.mockProjectId, anything())).once();
325+
expect(env.statusError(env.servalConfigStatus)).not.toBeNull();
326+
}));
327+
});
328+
329+
describe('quality estimation configuration', () => {
330+
it('should change quality estimation config value', fakeAsync(() => {
331+
const env = new TestEnvironment();
332+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
333+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
334+
335+
env.setQualityEstimationConfigValue('{ "version": "0.1", "slope": 109.6145, "intercept": -14.0633 }');
336+
env.clickElement(env.saveQualityEstimationConfigButton);
337+
338+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).once();
339+
expect(env.statusDone(env.qualityEstimationConfigStatus)).not.toBeNull();
340+
}));
341+
342+
it('should clear the quality estimation config value', fakeAsync(() => {
343+
const env = new TestEnvironment({
344+
preTranslate: true,
345+
draftConfig: { qualityEstimationConfig: { version: '0.1', slope: 109.6145, intercept: -14.0633 } }
346+
});
347+
expect(env.qualityEstimationConfigTextArea.value).toBe(
348+
'{"version":"0.1","slope":109.6145,"intercept":-14.0633}'
349+
);
350+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
351+
352+
env.setQualityEstimationConfigValue('');
353+
env.clickElement(env.saveQualityEstimationConfigButton);
354+
355+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).once();
356+
expect(env.statusDone(env.qualityEstimationConfigStatus)).not.toBeNull();
357+
}));
358+
359+
it('should not update an unchanged quality estimation config value', fakeAsync(() => {
360+
const env = new TestEnvironment({
361+
preTranslate: true,
362+
draftConfig: { qualityEstimationConfig: { version: '0.1', slope: 109.6145, intercept: -14.0633 } }
363+
});
364+
expect(env.qualityEstimationConfigTextArea.value).toBe(
365+
'{"version":"0.1","slope":109.6145,"intercept":-14.0633}'
366+
);
367+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
368+
369+
env.setQualityEstimationConfigValue('{ "version": "0.1", "slope": 109.6145, "intercept": -14.0633 }');
370+
env.clickElement(env.saveQualityEstimationConfigButton);
371+
372+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).never();
373+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
374+
}));
375+
376+
it('should not update an unchanged empty quality estimation config value', fakeAsync(() => {
377+
const env = new TestEnvironment();
378+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
379+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
380+
381+
env.setQualityEstimationConfigValue('');
382+
env.clickElement(env.saveQualityEstimationConfigButton);
383+
384+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).never();
385+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
386+
}));
387+
388+
it('should not update a non-JSON value', fakeAsync(() => {
389+
const env = new TestEnvironment();
390+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
391+
expect(env.statusError(env.qualityEstimationConfigStatus)).toBeNull();
392+
393+
env.setQualityEstimationConfigValue('test');
394+
env.clickElement(env.saveQualityEstimationConfigButton);
395+
396+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).never();
397+
expect(env.statusError(env.qualityEstimationConfigStatus)).not.toBeNull();
398+
}));
399+
400+
it('should not update an invalid value', fakeAsync(() => {
401+
const env = new TestEnvironment();
402+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
403+
expect(env.statusError(env.qualityEstimationConfigStatus)).toBeNull();
404+
405+
env.setQualityEstimationConfigValue('{"prop": "value"}');
406+
env.clickElement(env.saveQualityEstimationConfigButton);
407+
408+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).never();
409+
expect(env.statusError(env.qualityEstimationConfigStatus)).not.toBeNull();
410+
}));
411+
412+
it('should notify of a backend error', fakeAsync(() => {
413+
const env = new TestEnvironment();
414+
when(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).thenReject(
415+
new CommandError(CommandErrorCode.InternalError, 'error')
416+
);
417+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
418+
expect(env.statusError(env.qualityEstimationConfigStatus)).toBeNull();
419+
420+
env.setQualityEstimationConfigValue('{ "version": "0.1", "slope": 109.6145, "intercept": -14.0633 }');
421+
env.clickElement(env.saveQualityEstimationConfigButton);
422+
423+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).once();
424+
expect(env.statusError(env.qualityEstimationConfigStatus)).not.toBeNull();
425+
}));
311426
});
312427
});
313428

@@ -363,7 +478,8 @@ describe('ServalProjectComponent', () => {
363478
lastSelectedTrainingScriptureRanges: args.draftConfig?.lastSelectedTrainingScriptureRanges ?? undefined,
364479
lastSelectedTranslationScriptureRanges:
365480
args.draftConfig?.lastSelectedTranslationScriptureRanges ?? undefined,
366-
servalConfig: args.draftConfig?.servalConfig ?? undefined
481+
servalConfig: args.draftConfig?.servalConfig ?? undefined,
482+
qualityEstimationConfig: args.draftConfig?.qualityEstimationConfig ?? undefined
367483
},
368484
preTranslate: args.preTranslate,
369485
source: {
@@ -395,6 +511,7 @@ describe('ServalProjectComponent', () => {
395511
when(mockDraftGenerationService.getBuildProgress(anything())).thenReturn(of({ additionalInfo: {} } as BuildDto));
396512
when(mockSFProjectService.hasDraft(anything())).thenReturn(args.preTranslate);
397513
when(mockSFProjectService.onlineSetServalConfig(this.mockProjectId, anything())).thenResolve();
514+
when(mockSFProjectService.onlineSetQualityEstimationConfig(this.mockProjectId, anything())).thenResolve();
398515
const trainingData: TrainingDataDoc[] = [
399516
{
400517
id: 'training01',
@@ -444,6 +561,18 @@ describe('ServalProjectComponent', () => {
444561
return this.fixture.nativeElement.querySelector('.training-data-table td button');
445562
}
446563

564+
get qualityEstimationConfigStatus(): DebugElement {
565+
return this.fixture.debugElement.query(By.css('#quality-estimation-config-status'));
566+
}
567+
568+
get qualityEstimationConfigTextArea(): HTMLTextAreaElement {
569+
return this.fixture.nativeElement.querySelector('#quality-estimation-config') as HTMLTextAreaElement;
570+
}
571+
572+
get saveQualityEstimationConfigButton(): HTMLInputElement {
573+
return this.fixture.nativeElement.querySelector('#save-quality-estimation-config');
574+
}
575+
447576
get saveServalConfigButton(): HTMLInputElement {
448577
return this.fixture.nativeElement.querySelector('#save-serval-config');
449578
}
@@ -485,6 +614,14 @@ describe('ServalProjectComponent', () => {
485614
return node.querySelector('.translation-range')?.textContent ?? '';
486615
}
487616

617+
setQualityEstimationConfigValue(value: string): void {
618+
this.qualityEstimationConfigTextArea.value = value;
619+
this.qualityEstimationConfigTextArea.dispatchEvent(new Event('input'));
620+
this.fixture.detectChanges();
621+
tick();
622+
this.fixture.detectChanges();
623+
}
624+
488625
setServalConfigValue(value: string): void {
489626
this.servalConfigTextArea.value = value;
490627
this.servalConfigTextArea.dispatchEvent(new Event('input'));
@@ -496,5 +633,9 @@ describe('ServalProjectComponent', () => {
496633
statusDone(element: DebugElement): HTMLElement {
497634
return element.nativeElement.querySelector('.check-icon') as HTMLElement;
498635
}
636+
637+
statusError(element: DebugElement): HTMLElement {
638+
return element.nativeElement.querySelector('.error-icon') as HTMLElement;
639+
}
499640
}
500641
});

0 commit comments

Comments
 (0)