Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/RealtimeServer/scriptureforge/models/translate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export interface DraftConfig {
servalConfig?: string;
usfmConfig?: DraftUsfmConfig;
sendEmailOnBuildFinished?: boolean;
currentScriptureRange?: string;
draftedScriptureRange?: string;
}

export interface TranslateConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,123 @@ describe('SFProjectMigrations', () => {
expect(projectDoc.data.translateConfig.draftConfig.additionalTrainingSourceEnabled).toBeUndefined();
});
});

describe('version 28', () => {
it('adds currentScriptureRange to draftConfig', async () => {
const env = new TestEnvironment(27);
const conn = env.server.connect();
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
translateConfig: { draftConfig: {} },
texts: [
{ bookNum: 1, chapters: [{ hasDraft: true }] },
{ bookNum: 2, chapters: [{ hasDraft: false }] },
{ bookNum: 3, chapters: [{ hasDraft: true }] },
{ bookNum: 0, chapters: [{ hasDraft: true }] }
]
});
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
expect(projectDoc.data.translateConfig.draftConfig.draftedScriptureRange).not.toBeDefined();

await env.server.migrateIfNecessary();

projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).toBe('GEN;LEV');
expect(projectDoc.data.translateConfig.draftConfig.draftedScriptureRange).toBe('GEN;LEV');
});

it('does not add currentScriptureRange to draftConfig if it exists', async () => {
const env = new TestEnvironment(27);
const conn = env.server.connect();
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
translateConfig: { draftConfig: { currentScriptureRange: 'NUM;DEU' } },
texts: [
{ bookNum: 1, chapters: [{ hasDraft: true }] },
{ bookNum: 2, chapters: [{ hasDraft: false }] },
{ bookNum: 3, chapters: [{ hasDraft: true }] },
{ bookNum: 0, chapters: [{ hasDraft: true }] }
]
});
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).toBe('NUM;DEU');

await env.server.migrateIfNecessary();

projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).toBe('NUM;DEU');
});

it('does not add draftedScriptureRange to draftConfig if it exists', async () => {
const env = new TestEnvironment(27);
const conn = env.server.connect();
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
translateConfig: { draftConfig: { draftedScriptureRange: 'NUM;DEU' } },
texts: [
{ bookNum: 1, chapters: [{ hasDraft: true }] },
{ bookNum: 2, chapters: [{ hasDraft: false }] },
{ bookNum: 3, chapters: [{ hasDraft: true }] },
{ bookNum: 0, chapters: [{ hasDraft: true }] }
]
});
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
expect(projectDoc.data.translateConfig.draftConfig.draftedScriptureRange).toBeDefined();

await env.server.migrateIfNecessary();

projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).toBe('GEN;LEV');
expect(projectDoc.data.translateConfig.draftConfig.draftedScriptureRange).toBe('NUM;DEU');
});

it('does not add currentScriptureRange to draftConfig if no drafted chapters', async () => {
const env = new TestEnvironment(27);
const conn = env.server.connect();
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
translateConfig: { draftConfig: {} },
texts: [
{ bookNum: 1, chapters: [{ hasDraft: false }] },
{ bookNum: 2, chapters: [{ hasDraft: false }] }
]
});
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();

await env.server.migrateIfNecessary();

projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
});

it('does not add currentScriptureRange to draftConfig if there are no texts', async () => {
const env = new TestEnvironment(27);
const conn = env.server.connect();
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
translateConfig: { draftConfig: {} },
texts: []
});
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();

await env.server.migrateIfNecessary();

projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.currentScriptureRange).not.toBeDefined();
});

it('does not add currentScriptureRange to draftConfig if the project is null', async () => {
const env = new TestEnvironment(27);
const conn = env.server.connect();
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', null);
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data?.translateConfig?.draftConfig?.currentScriptureRange).not.toBeDefined();

await env.server.migrateIfNecessary();

projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data?.translateConfig?.draftConfig?.currentScriptureRange).not.toBeDefined();
});
});
});

class TestEnvironment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { submitMigrationOp } from '../../common/realtime-server';
import { NoteTag } from '../models/note-tag';
import { SF_PROJECT_RIGHTS, SFProjectDomain } from '../models/sf-project-rights';
import { SFProjectRole } from '../models/sf-project-role';
import { TextInfo } from '../models/text-info';
import { TextInfoPermission } from '../models/text-info-permission';
import { TranslateShareLevel, TranslateSource } from '../models/translate-config';

Expand Down Expand Up @@ -630,6 +631,37 @@ class SFProjectMigration27 extends DocMigration {
}
}

class SFProjectMigration28 extends DocMigration {
static readonly VERSION = 28;

async migrateDoc(doc: Doc): Promise<void> {
const ops: Op[] = [];
if (doc.data?.texts != null && doc.data?.translateConfig?.draftConfig?.currentScriptureRange == null) {
const currentScriptureRange = doc.data.texts
.filter((t: TextInfo) => t.chapters.some(c => c.hasDraft))
.map((t: TextInfo) => Canon.bookNumberToId(t.bookNum, ''))
.filter((id: string) => id !== '')
.join(';');
if (currentScriptureRange !== '' && currentScriptureRange != null) {
ops.push({
p: ['translateConfig', 'draftConfig', 'currentScriptureRange'],
oi: currentScriptureRange
});
if (doc.data.translateConfig?.draftConfig?.draftedScriptureRange == null) {
ops.push({
p: ['translateConfig', 'draftConfig', 'draftedScriptureRange'],
oi: currentScriptureRange
});
}
}
}

if (ops.length > 0) {
await submitMigrationOp(SFProjectMigration28.VERSION, doc, ops);
}
}
}

export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncreasingMigrationList([
SFProjectMigration1,
SFProjectMigration2,
Expand Down Expand Up @@ -657,5 +689,6 @@ export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncrea
SFProjectMigration24,
SFProjectMigration25,
SFProjectMigration26,
SFProjectMigration27
SFProjectMigration27,
SFProjectMigration28
]);
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ export class SFProjectService extends ProjectService<SFProject> {
},
sendEmailOnBuildFinished: {
bsonType: 'bool'
},
currentScriptureRange: {
bsonType: 'string'
},
draftedScriptureRange: {
bsonType: 'string'
}
},
additionalProperties: false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
import { anything, mock, verify, when } from 'ts-mockito';
import { CommandService } from 'xforge-common/command.service';
import { RealtimeService } from 'xforge-common/realtime.service';
Expand All @@ -20,6 +21,80 @@ describe('SFProjectService', () => {
]
}));

describe('hasDraft', () => {
it('should return true if the book is in the drafted scripture range', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV', currentScriptureRange: 'MAT;MRK' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2);
expect(actual).toBe(true);
}));

it('should return true if the book is in the current scripture range when current build is true', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'MAT;MRK', currentScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2, true);
expect(actual).toBe(true);
}));

it('should return true if the drafted scripture range has books', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project);
expect(actual).toBe(true);
}));

it('should return true if the current scripture range has books', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { currentScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, undefined, true);
expect(actual).toBe(true);
}));

it('should return false if the book is not in the drafted scripture range', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'MAT;MRK', currentScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2);
expect(actual).toBe(false);
}));

it('should return false if the book is not in the current scripture range when current build is true', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV', currentScriptureRange: 'MAT;MRK' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2, true);
expect(actual).toBe(false);
}));

it('should return false if the drafted scripture range does not have books', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { currentScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project);
expect(actual).toBe(false);
}));

it('should return false if the current scripture range does not have books', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, undefined, true);
expect(actual).toBe(false);
}));
});

describe('onlineSetRoleProjectPermissions', () => {
it('should invoke the command service', fakeAsync(async () => {
const env = new TestEnvironment();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { RetryingRequest, RetryingRequestService } from 'xforge-common/retrying-
import { TransceleratorQuestion } from '../checking/import-questions-dialog/import-questions-dialog.component';
import { EventMetric } from '../event-metrics/event-metric';
import { ShareLinkType } from '../shared/share/share-dialog.component';
import { booksFromScriptureRange } from '../shared/utils';
import { InviteeStatus } from '../users/collaborators/collaborators.component';
import { BiblicalTermDoc } from './models/biblical-term-doc';
import { NoteThreadDoc } from './models/note-thread-doc';
Expand Down Expand Up @@ -53,8 +54,28 @@ export class SFProjectService extends ProjectService<SFProject, SFProjectDoc> {
super(realtimeService, commandService, retryingRequestService, SF_PROJECT_ROLES);
}

static hasDraft(project: SFProjectProfile): boolean {
return project.texts.some(text => text.chapters.some(chapter => chapter.hasDraft));
/**
* Determines if there is a draft in the project for the specified scripture range or book number.
* @param project The project.
* @param scriptureRange The scripture range or book number.
* @param currentBuild If true, only return true if the current build on serval contains the scripture range.
* @returns true if the project contains a draft for the specified scripture range or book number.
*/
hasDraft(
project: SFProjectProfile | undefined,
bookNum: number | undefined = undefined,
currentBuild: boolean = false
): boolean {
const books: number[] = booksFromScriptureRange(
currentBuild
? project?.translateConfig.draftConfig.currentScriptureRange
: project?.translateConfig.draftConfig.draftedScriptureRange
);
if (bookNum == null) {
return books.length > 0;
} else {
return books.includes(bookNum);
}
}

async onlineCreate(settings: SFProjectCreateSettings): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,10 @@ describe('ServalProjectComponent', () => {
name: 'Project 01',
shortName: 'P1',
texts: [
{ bookNum: 1, chapters: [{ number: 1, hasDraft: false }] },
{ bookNum: 2, chapters: [{ number: 1, hasDraft: false }] },
{ bookNum: 3, chapters: [{ number: 1, hasDraft: args.preTranslate }] },
{ bookNum: 4, chapters: [{ number: 1, hasDraft: args.preTranslate }] }
{ bookNum: 1, chapters: [{ number: 1 }] },
{ bookNum: 2, chapters: [{ number: 1 }] },
{ bookNum: 3, chapters: [{ number: 1 }] },
{ bookNum: 4, chapters: [{ number: 1 }] }
],
translateConfig: {
draftConfig: {
Expand Down Expand Up @@ -358,6 +358,7 @@ describe('ServalProjectComponent', () => {
when(mockServalAdministrationService.downloadProject(anything())).thenReturn(of(new Blob()));
when(mockAuthService.currentUserRoles).thenReturn([SystemRole.ServalAdmin]);
when(mockDraftGenerationService.getBuildProgress(anything())).thenReturn(of({ additionalInfo: {} } as BuildDto));
when(mockSFProjectService.hasDraft(anything())).thenReturn(args.preTranslate);
when(mockSFProjectService.onlineSetServalConfig(this.mockProjectId, anything())).thenResolve();

spyOn(saveAs, 'saveAs').and.stub();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,13 @@ export class ServalProjectComponent extends DataLoadingComponent implements OnIn
}

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

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

// Get the last completed build
if (this.isOnline && SFProjectService.hasDraft(project)) {
if (this.isOnline && this.projectService.hasDraft(project)) {
return this.draftGenerationService.getLastCompletedBuild(projectDoc.id);
} else {
return of(undefined);
Expand Down
16 changes: 16 additions & 0 deletions src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SFProject } from 'realtime-server/lib/esm/scriptureforge/models/sf-proj
import { DeltaOperation } from 'rich-text';
import { SelectableProject } from '../core/paratext.service';
import {
booksFromScriptureRange,
compareProjectsForSorting,
getBookFileNameDigits,
getUnsupportedTags,
Expand Down Expand Up @@ -95,6 +96,21 @@ describe('shared utils', () => {
expect(projects.map(project => project.shortName)).toEqual(['AAA', 'bbb', 'CCC']);
});

describe('booksFromScriptureRange', () => {
it('should return an empty array for non-scripture book values', () => {
expect(booksFromScriptureRange(undefined)).toEqual([]);
expect(booksFromScriptureRange('')).toEqual([]);
expect(booksFromScriptureRange(' ')).toEqual([]);
expect(booksFromScriptureRange('NOT_A_BOOK')).toEqual([]);
});

it('should return numbers for valid scripture book values', () => {
expect(booksFromScriptureRange('GEN')).toEqual([1]);
expect(booksFromScriptureRange('GEN;EXO')).toEqual([1, 2]);
expect(booksFromScriptureRange('GEN;NOT_A_BOOK;EXO')).toEqual([1, 2]);
});
});

describe('Xml Utils', () => {
it('should convert plain text to xml', () => {
expect(XmlUtils.encodeForXml('')).toEqual('');
Expand Down
Loading
Loading