Skip to content

Commit 79b210d

Browse files
authored
SF-3638 Store the drafted scripture range and current draft's scripture range in the project (#3574)
1 parent 6a1d94f commit 79b210d

37 files changed

+583
-516
lines changed

src/RealtimeServer/scriptureforge/models/text-info.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ export interface Chapter {
44
isValid: boolean;
55
permissions: { [userRef: string]: string };
66
hasAudio?: boolean;
7+
/**
8+
* @deprecated Use SFProjectService.hasDraft() instead
9+
*/
710
hasDraft?: boolean;
811
draftApplied?: boolean;
912
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export interface DraftConfig {
6969
servalConfig?: string;
7070
usfmConfig?: DraftUsfmConfig;
7171
sendEmailOnBuildFinished?: boolean;
72+
currentScriptureRange?: string;
73+
draftedScriptureRange?: string;
7274
}
7375

7476
export interface TranslateConfig {

src/RealtimeServer/scriptureforge/services/sf-project-migrations.spec.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,123 @@ describe('SFProjectMigrations', () => {
10521052
expect(projectDoc.data.translateConfig.draftConfig.additionalTrainingSourceEnabled).toBeUndefined();
10531053
});
10541054
});
1055+
1056+
describe('version 28', () => {
1057+
it('adds currentScriptureRange to draftConfig', async () => {
1058+
const env = new TestEnvironment(27);
1059+
const conn = env.server.connect();
1060+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
1061+
translateConfig: { draftConfig: {} },
1062+
texts: [
1063+
{ bookNum: 1, chapters: [{ hasDraft: true }] },
1064+
{ bookNum: 2, chapters: [{ hasDraft: false }] },
1065+
{ bookNum: 3, chapters: [{ hasDraft: true }] },
1066+
{ bookNum: 0, chapters: [{ hasDraft: true }] }
1067+
]
1068+
});
1069+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1070+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
1071+
expect(projectDoc.data.translateConfig.draftConfig.draftedScriptureRange).not.toBeDefined();
1072+
1073+
await env.server.migrateIfNecessary();
1074+
1075+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1076+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).toBe('GEN;LEV');
1077+
expect(projectDoc.data.translateConfig.draftConfig.draftedScriptureRange).toBe('GEN;LEV');
1078+
});
1079+
1080+
it('does not add currentScriptureRange to draftConfig if it exists', async () => {
1081+
const env = new TestEnvironment(27);
1082+
const conn = env.server.connect();
1083+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
1084+
translateConfig: { draftConfig: { currentScriptureRange: 'NUM;DEU' } },
1085+
texts: [
1086+
{ bookNum: 1, chapters: [{ hasDraft: true }] },
1087+
{ bookNum: 2, chapters: [{ hasDraft: false }] },
1088+
{ bookNum: 3, chapters: [{ hasDraft: true }] },
1089+
{ bookNum: 0, chapters: [{ hasDraft: true }] }
1090+
]
1091+
});
1092+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1093+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).toBe('NUM;DEU');
1094+
1095+
await env.server.migrateIfNecessary();
1096+
1097+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1098+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).toBe('NUM;DEU');
1099+
});
1100+
1101+
it('does not add draftedScriptureRange to draftConfig if it exists', async () => {
1102+
const env = new TestEnvironment(27);
1103+
const conn = env.server.connect();
1104+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
1105+
translateConfig: { draftConfig: { draftedScriptureRange: 'NUM;DEU' } },
1106+
texts: [
1107+
{ bookNum: 1, chapters: [{ hasDraft: true }] },
1108+
{ bookNum: 2, chapters: [{ hasDraft: false }] },
1109+
{ bookNum: 3, chapters: [{ hasDraft: true }] },
1110+
{ bookNum: 0, chapters: [{ hasDraft: true }] }
1111+
]
1112+
});
1113+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1114+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
1115+
expect(projectDoc.data.translateConfig.draftConfig.draftedScriptureRange).toBeDefined();
1116+
1117+
await env.server.migrateIfNecessary();
1118+
1119+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1120+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).toBe('GEN;LEV');
1121+
expect(projectDoc.data.translateConfig.draftConfig.draftedScriptureRange).toBe('NUM;DEU');
1122+
});
1123+
1124+
it('does not add currentScriptureRange to draftConfig if no drafted chapters', async () => {
1125+
const env = new TestEnvironment(27);
1126+
const conn = env.server.connect();
1127+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
1128+
translateConfig: { draftConfig: {} },
1129+
texts: [
1130+
{ bookNum: 1, chapters: [{ hasDraft: false }] },
1131+
{ bookNum: 2, chapters: [{ hasDraft: false }] }
1132+
]
1133+
});
1134+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1135+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
1136+
1137+
await env.server.migrateIfNecessary();
1138+
1139+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1140+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
1141+
});
1142+
1143+
it('does not add currentScriptureRange to draftConfig if there are no texts', async () => {
1144+
const env = new TestEnvironment(27);
1145+
const conn = env.server.connect();
1146+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
1147+
translateConfig: { draftConfig: {} },
1148+
texts: []
1149+
});
1150+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1151+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
1152+
1153+
await env.server.migrateIfNecessary();
1154+
1155+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1156+
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
1157+
});
1158+
1159+
it('does not add currentScriptureRange to draftConfig if the project is null', async () => {
1160+
const env = new TestEnvironment(27);
1161+
const conn = env.server.connect();
1162+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', null);
1163+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1164+
expect(projectDoc.data?.translateConfig?.draftConfig?.currentScriptureRange).not.toBeDefined();
1165+
1166+
await env.server.migrateIfNecessary();
1167+
1168+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
1169+
expect(projectDoc.data?.translateConfig?.draftConfig?.currentScriptureRange).not.toBeDefined();
1170+
});
1171+
});
10551172
});
10561173

10571174
class TestEnvironment {

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { submitMigrationOp } from '../../common/realtime-server';
66
import { NoteTag } from '../models/note-tag';
77
import { SF_PROJECT_RIGHTS, SFProjectDomain } from '../models/sf-project-rights';
88
import { SFProjectRole } from '../models/sf-project-role';
9+
import { TextInfo } from '../models/text-info';
910
import { TextInfoPermission } from '../models/text-info-permission';
1011
import { TranslateShareLevel, TranslateSource } from '../models/translate-config';
1112

@@ -630,6 +631,38 @@ class SFProjectMigration27 extends DocMigration {
630631
}
631632
}
632633

634+
class SFProjectMigration28 extends DocMigration {
635+
static readonly VERSION = 28;
636+
637+
async migrateDoc(doc: Doc): Promise<void> {
638+
const ops: Op[] = [];
639+
if (doc.data?.texts != null && doc.data?.translateConfig?.draftConfig?.currentScriptureRange == null) {
640+
const currentScriptureRange = doc.data.texts
641+
// eslint-disable-next-line @typescript-eslint/no-deprecated
642+
.filter((t: TextInfo) => t.chapters.some(c => c.hasDraft))
643+
.map((t: TextInfo) => Canon.bookNumberToId(t.bookNum, ''))
644+
.filter((id: string) => id !== '')
645+
.join(';');
646+
if (currentScriptureRange !== '' && currentScriptureRange != null) {
647+
ops.push({
648+
p: ['translateConfig', 'draftConfig', 'currentScriptureRange'],
649+
oi: currentScriptureRange
650+
});
651+
if (doc.data.translateConfig?.draftConfig?.draftedScriptureRange == null) {
652+
ops.push({
653+
p: ['translateConfig', 'draftConfig', 'draftedScriptureRange'],
654+
oi: currentScriptureRange
655+
});
656+
}
657+
}
658+
}
659+
660+
if (ops.length > 0) {
661+
await submitMigrationOp(SFProjectMigration28.VERSION, doc, ops);
662+
}
663+
}
664+
}
665+
633666
export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncreasingMigrationList([
634667
SFProjectMigration1,
635668
SFProjectMigration2,
@@ -657,5 +690,6 @@ export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncrea
657690
SFProjectMigration24,
658691
SFProjectMigration25,
659692
SFProjectMigration26,
660-
SFProjectMigration27
693+
SFProjectMigration27,
694+
SFProjectMigration28
661695
]);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ export class SFProjectService extends ProjectService<SFProject> {
269269
},
270270
sendEmailOnBuildFinished: {
271271
bsonType: 'bool'
272+
},
273+
currentScriptureRange: {
274+
bsonType: 'string'
275+
},
276+
draftedScriptureRange: {
277+
bsonType: 'string'
272278
}
273279
},
274280
additionalProperties: false

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
22
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
33
import { fakeAsync, TestBed } from '@angular/core/testing';
4+
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
45
import { anything, mock, verify, when } from 'ts-mockito';
56
import { CommandService } from 'xforge-common/command.service';
67
import { RealtimeService } from 'xforge-common/realtime.service';
@@ -20,6 +21,80 @@ describe('SFProjectService', () => {
2021
]
2122
}));
2223

24+
describe('hasDraft', () => {
25+
it('should return true if the book is in the drafted scripture range', fakeAsync(() => {
26+
const env = new TestEnvironment();
27+
const project = {
28+
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV', currentScriptureRange: 'MAT;MRK' } }
29+
} as SFProjectProfile;
30+
const actual = env.service.hasDraft(project, 2);
31+
expect(actual).toBe(true);
32+
}));
33+
34+
it('should return true if the book is in the current scripture range when current build is true', fakeAsync(() => {
35+
const env = new TestEnvironment();
36+
const project = {
37+
translateConfig: { draftConfig: { draftedScriptureRange: 'MAT;MRK', currentScriptureRange: 'GEN;EXO;LEV' } }
38+
} as SFProjectProfile;
39+
const actual = env.service.hasDraft(project, 2, true);
40+
expect(actual).toBe(true);
41+
}));
42+
43+
it('should return true if the drafted scripture range has books', fakeAsync(() => {
44+
const env = new TestEnvironment();
45+
const project = {
46+
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV' } }
47+
} as SFProjectProfile;
48+
const actual = env.service.hasDraft(project);
49+
expect(actual).toBe(true);
50+
}));
51+
52+
it('should return true if the current scripture range has books', fakeAsync(() => {
53+
const env = new TestEnvironment();
54+
const project = {
55+
translateConfig: { draftConfig: { currentScriptureRange: 'GEN;EXO;LEV' } }
56+
} as SFProjectProfile;
57+
const actual = env.service.hasDraft(project, undefined, true);
58+
expect(actual).toBe(true);
59+
}));
60+
61+
it('should return false if the book is not in the drafted scripture range', fakeAsync(() => {
62+
const env = new TestEnvironment();
63+
const project = {
64+
translateConfig: { draftConfig: { draftedScriptureRange: 'MAT;MRK', currentScriptureRange: 'GEN;EXO;LEV' } }
65+
} as SFProjectProfile;
66+
const actual = env.service.hasDraft(project, 2);
67+
expect(actual).toBe(false);
68+
}));
69+
70+
it('should return false if the book is not in the current scripture range when current build is true', fakeAsync(() => {
71+
const env = new TestEnvironment();
72+
const project = {
73+
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV', currentScriptureRange: 'MAT;MRK' } }
74+
} as SFProjectProfile;
75+
const actual = env.service.hasDraft(project, 2, true);
76+
expect(actual).toBe(false);
77+
}));
78+
79+
it('should return false if the drafted scripture range does not have books', fakeAsync(() => {
80+
const env = new TestEnvironment();
81+
const project = {
82+
translateConfig: { draftConfig: { currentScriptureRange: 'GEN;EXO;LEV' } }
83+
} as SFProjectProfile;
84+
const actual = env.service.hasDraft(project);
85+
expect(actual).toBe(false);
86+
}));
87+
88+
it('should return false if the current scripture range does not have books', fakeAsync(() => {
89+
const env = new TestEnvironment();
90+
const project = {
91+
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV' } }
92+
} as SFProjectProfile;
93+
const actual = env.service.hasDraft(project, undefined, true);
94+
expect(actual).toBe(false);
95+
}));
96+
});
97+
2398
describe('onlineSetRoleProjectPermissions', () => {
2499
it('should invoke the command service', fakeAsync(async () => {
25100
const env = new TestEnvironment();

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { QueryParameters, QueryResults } from 'xforge-common/query-parameters';
2323
import { RealtimeService } from 'xforge-common/realtime.service';
2424
import { RetryingRequest, RetryingRequestService } from 'xforge-common/retrying-request.service';
2525
import { EventMetric } from '../event-metrics/event-metric';
26+
import { booksFromScriptureRange } from '../shared/utils';
2627
import { BiblicalTermDoc } from './models/biblical-term-doc';
2728
import { InviteeStatus } from './models/invitee-status';
2829
import { NoteThreadDoc } from './models/note-thread-doc';
@@ -53,8 +54,28 @@ export class SFProjectService extends ProjectService<SFProject, SFProjectDoc> {
5354
super(realtimeService, commandService, retryingRequestService, SF_PROJECT_ROLES);
5455
}
5556

56-
static hasDraft(project: SFProjectProfile): boolean {
57-
return project.texts.some(text => text.chapters.some(chapter => chapter.hasDraft));
57+
/**
58+
* Determines if there is a draft in the project for the specified scripture range or book number.
59+
* @param project The project.
60+
* @param scriptureRange The scripture range or book number.
61+
* @param currentBuild If true, only return true if the current build on serval contains the scripture range.
62+
* @returns true if the project contains a draft for the specified scripture range or book number.
63+
*/
64+
hasDraft(
65+
project: SFProjectProfile | undefined,
66+
bookNum: number | undefined = undefined,
67+
currentBuild: boolean = false
68+
): boolean {
69+
const books: number[] = booksFromScriptureRange(
70+
currentBuild
71+
? project?.translateConfig.draftConfig.currentScriptureRange
72+
: project?.translateConfig.draftConfig.draftedScriptureRange
73+
);
74+
if (bookNum == null) {
75+
return books.length > 0;
76+
} else {
77+
return books.includes(bookNum);
78+
}
5879
}
5980

6081
async onlineCreate(settings: SFProjectCreateSettings): Promise<string> {

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,10 @@ describe('ServalProjectComponent', () => {
293293
name: 'Project 01',
294294
shortName: 'P1',
295295
texts: [
296-
{ bookNum: 1, chapters: [{ number: 1, hasDraft: false }] },
297-
{ bookNum: 2, chapters: [{ number: 1, hasDraft: false }] },
298-
{ bookNum: 3, chapters: [{ number: 1, hasDraft: args.preTranslate }] },
299-
{ bookNum: 4, chapters: [{ number: 1, hasDraft: args.preTranslate }] }
296+
{ bookNum: 1, chapters: [{ number: 1 }] },
297+
{ bookNum: 2, chapters: [{ number: 1 }] },
298+
{ bookNum: 3, chapters: [{ number: 1 }] },
299+
{ bookNum: 4, chapters: [{ number: 1 }] }
300300
],
301301
translateConfig: {
302302
draftConfig: {
@@ -358,6 +358,7 @@ describe('ServalProjectComponent', () => {
358358
when(mockServalAdministrationService.downloadProject(anything())).thenReturn(of(new Blob()));
359359
when(mockAuthService.currentUserRoles).thenReturn([SystemRole.ServalAdmin]);
360360
when(mockDraftGenerationService.getBuildProgress(anything())).thenReturn(of({ additionalInfo: {} } as BuildDto));
361+
when(mockSFProjectService.hasDraft(anything())).thenReturn(args.preTranslate);
361362
when(mockSFProjectService.onlineSetServalConfig(this.mockProjectId, anything())).thenResolve();
362363

363364
spyOn(saveAs, 'saveAs').and.stub();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,13 @@ export class ServalProjectComponent extends DataLoadingComponent implements OnIn
234234
}
235235

236236
this.draftConfig = draftConfig;
237-
this.draftJob$ = SFProjectService.hasDraft(project) ? this.getDraftJob(projectDoc.id) : of(undefined);
237+
this.draftJob$ = this.projectService.hasDraft(project) ? this.getDraftJob(projectDoc.id) : of(undefined);
238238

239239
// Setup the serval config value
240240
this.servalConfig.setValue(project.translateConfig.draftConfig.servalConfig);
241241

242242
// Get the last completed build
243-
if (this.isOnline && SFProjectService.hasDraft(project)) {
243+
if (this.isOnline && this.projectService.hasDraft(project)) {
244244
return this.draftGenerationService.getLastCompletedBuild(projectDoc.id);
245245
} else {
246246
return of(undefined);

0 commit comments

Comments
 (0)