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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const mockNoticeService = mock(NoticeService);
const mockPermissionService = mock(PermissionsService);
const mockProjectService = mock(ActivatedProjectService);

const defaultChaptersNum = 20;
const defaultTranslatedNum = 9;
const defaultBlankNum = 5;

describe('progress service', () => {
configureTestingModule(() => ({
providers: [
Expand Down Expand Up @@ -161,17 +165,63 @@ describe('progress service', () => {
expect(env.service.canTrainSuggestions).toBeFalsy();
discardPeriodicTasks();
}));

it('returns text progress for texts on a project', fakeAsync(async () => {
const booksWithTexts = 5;
const totalTranslated = booksWithTexts * defaultChaptersNum * defaultTranslatedNum;
const totalBlank = booksWithTexts * defaultChaptersNum * defaultBlankNum;
const env = new TestEnvironment(totalTranslated, totalBlank);
tick();

const texts: TextInfo[] = env.createTexts();
const projectDoc: SFProjectProfileDoc = {
id: 'sourceId',
data: createTestProjectProfile({ texts })
} as SFProjectProfileDoc;
when(mockSFProjectService.getProfile(projectDoc.id)).thenResolve(projectDoc);
when(mockPermissionService.isUserOnProject(anything())).thenResolve(true);

// SUT
const progressList = await env.service.getTextProgressForProject(projectDoc.id);
tick();
expect(progressList.length).toEqual(texts.length);
for (let i = 0; i < progressList.length; i++) {
const progress = progressList[i];
if (i < booksWithTexts) {
expect(progress.translated).toEqual(defaultTranslatedNum * defaultChaptersNum);
expect(progress.blank).toEqual(defaultBlankNum * defaultChaptersNum);
} else {
expect(progress.translated).toEqual(0);
expect(progress.blank).toEqual(0);
}
}
}));

it('returns empty text progress if user does not have permission', fakeAsync(async () => {
const env = new TestEnvironment(1000, 500);
tick();
const texts: TextInfo[] = env.createTexts();
const projectDoc: SFProjectProfileDoc = {
id: 'sourceId',
data: createTestProjectProfile({ texts })
} as SFProjectProfileDoc;
when(mockPermissionService.isUserOnProject(anything())).thenResolve(false);

// SUT
const progressList = await env.service.getTextProgressForProject(projectDoc.id);
tick();
expect(progressList.length).toEqual(0);
}));
});

class TestEnvironment {
readonly ngZone: NgZone = TestBed.inject(NgZone);
readonly service: ProgressService;
private readonly numBooks = 20;
private readonly numChapters = 20;

readonly mockProject = mock(SFProjectProfileDoc);
readonly project$ = new BehaviorSubject(instance(this.mockProject));

private readonly numBooks = 20;
// Store all text data in a single map to avoid repeated deepEqual calls
private readonly allTextData = new Map<string, { translated: number; blank: number }>();

Expand Down Expand Up @@ -242,10 +292,10 @@ class TestEnvironment {

private populateTextData(projectId: string, translatedSegments: number, blankSegments: number): void {
for (let book = 1; book <= this.numBooks; book++) {
for (let chapter = 0; chapter < this.numChapters; chapter++) {
const translated = translatedSegments >= 9 ? 9 : translatedSegments;
for (let chapter = 0; chapter < defaultChaptersNum; chapter++) {
const translated = translatedSegments >= defaultTranslatedNum ? defaultTranslatedNum : translatedSegments;
translatedSegments -= translated;
const blank = blankSegments >= 5 ? 5 : blankSegments;
const blank = blankSegments >= defaultBlankNum ? defaultBlankNum : blankSegments;
blankSegments -= blank;

const key = `${projectId}:${book}:${chapter}:target`;
Expand Down Expand Up @@ -283,7 +333,7 @@ class TestEnvironment {
const texts: TextInfo[] = [];
for (let book = 1; book <= this.numBooks; book++) {
const chapters: Chapter[] = [];
for (let chapter = 0; chapter < this.numChapters; chapter++) {
for (let chapter = 0; chapter < defaultChaptersNum; chapter++) {
chapters.push({ isValid: true, lastVerse: 1, number: chapter, permissions: {}, hasAudio: false });
}
texts.push({ bookNum: book, chapters: chapters, hasSource: true, permissions: {} });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,45 @@ export class ProgressService extends DataLoadingComponent implements OnDestroy {
this._allChaptersChangeSub?.unsubscribe();
}

/** Calculate the text progress for a project by reading every text doc for each book. */
async getTextProgressForProject(projectId: string): Promise<TextProgress[]> {
if (!(await this.permissionsService.isUserOnProject(projectId))) return [];

const projectDoc = await this.projectService.getProfile(projectId);
if (projectDoc.data == null) {
return [];
}
const chapterPromises: Promise<TextDoc[]>[] = [];
const chaptersByBook: Map<number, TextDoc[]> = new Map();

// for every book that exists in the project calculate the translated verses
for (const book of projectDoc.data.texts) {
const bookChapters: TextDocId[] = book.chapters.map(
c => new TextDocId(projectDoc.id, book.bookNum, c.number, 'target')
);
const chapterTextDocPromises = Promise.all(bookChapters.map(c => this.projectService.getText(c)));
// set the map of books to text docs
void chapterTextDocPromises.then(textDocs => {
chaptersByBook.set(book.bookNum, textDocs);
});
chapterPromises.push(chapterTextDocPromises);
}

await Promise.all(chapterPromises);
const textProgressList = projectDoc.data.texts.map(t => new TextProgress(t));
for (const textProgress of textProgressList) {
const chapterTextDocs: TextDoc[] = chaptersByBook.get(textProgress.text.bookNum) ?? [];
for (const chapterTextDoc of chapterTextDocs) {
// get the translated and blank segments from the chapter docs
const { translated, blank } = chapterTextDoc.getSegmentCount();
textProgress.translated += translated;
textProgress.blank += blank;
}
}

return textProgressList;
}

private async initialize(projectId: string): Promise<void> {
this._canTrainSuggestions = false;
this._projectDoc = await this.projectService.getProfile(projectId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ <h2>{{ t("translated_books") }}</h2>
(bookSelect)="onTranslatedBookSelect($event)"
data-test-id="draft-stepper-training-books"
></app-book-multi-select>
@if (unusableTrainingSourceBooks.length) {
@if (unusableTrainingSourceBooks.length > 0 || emptyTrainingSourceBooks.length > 0) {
<app-notice icon="info" mode="basic" type="light" class="unusable-training-books">
<div class="notice-container">
<span
Expand All @@ -142,22 +142,33 @@ <h2>{{ t("translated_books") }}</h2>
[innerHtml]="
!expandUnusableTrainingBooks
? i18n.translateAndInsertTags('draft_generation_steps.books_are_hidden_show_why', {
numBooks: unusableTrainingSourceBooks.length
numBooks: unusableTrainingSourceBooks.length + emptyTrainingSourceBooks.length
})
: i18n.translateAndInsertTags('draft_generation_steps.books_are_hidden_hide_explanation', {
numBooks: unusableTrainingSourceBooks.length
numBooks: unusableTrainingSourceBooks.length + emptyTrainingSourceBooks.length
})
"
>
</span>
@if (expandUnusableTrainingBooks) {
<h4 class="explanation">
<transloco
key="draft_generation_steps.these_source_books_cannot_be_used_for_training"
[params]="{ firstTrainingSource }"
></transloco>
</h4>
<span class="book-names">{{ bookNames(unusableTrainingSourceBooks) }}</span>
@if (emptyTrainingSourceBooks.length > 0) {
<h4 class="explanation">
<transloco
key="draft_generation_steps.these_training_source_books_are_blank"
[params]="{ firstTrainingSource }"
></transloco>
</h4>
<span class="book-names">{{ bookNames(emptyTrainingSourceBooks) }}</span>
}
@if (unusableTrainingSourceBooks.length > 0) {
<h4 class="explanation">
<transloco
key="draft_generation_steps.these_source_books_cannot_be_used_for_training"
[params]="{ firstTrainingSource }"
></transloco>
</h4>
<span class="book-names">{{ bookNames(unusableTrainingSourceBooks) }}</span>
}
}
</div>
</app-notice>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ describe('DraftGenerationStepsComponent', () => {
{ text: { bookNum: 6 }, translated: 20, blank: 2, percentage: 90 } as TextProgress,
{ text: { bookNum: 7 }, translated: 0 } as TextProgress
]);
const defaultTextProgress = [
{ text: { bookNum: 1 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 2 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 3 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 4 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 5 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 6 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 7 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 8 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 9 }, translated: 100, percentage: 100 } as TextProgress,
{ text: { bookNum: 10 }, translated: 100, percentage: 100 } as TextProgress
];
when(mockProgressService.getTextProgressForProject(anything())).thenResolve(defaultTextProgress);
when(mockOnlineStatusService.isOnline).thenReturn(true);
}));

Expand Down Expand Up @@ -291,6 +304,7 @@ describe('DraftGenerationStepsComponent', () => {
when(mockActivatedProjectService.projectDoc).thenReturn(mockTargetProjectDoc);
when(mockActivatedProjectService.projectDoc$).thenReturn(targetProjectDoc$);
when(mockActivatedProjectService.changes$).thenReturn(targetProjectDoc$);
// setup mock source with empty book
setupProjectProfileMock(
sourceProjectId,
sourceBooks.map(b => b.bookNum),
Expand Down Expand Up @@ -331,14 +345,12 @@ describe('DraftGenerationStepsComponent', () => {
project01: [
{ number: 1, selected: true },
{ number: 2, selected: true },
{ number: 3, selected: false },
{ number: 5, selected: false }
{ number: 3, selected: false }
],
sourceProject: [
{ number: 1, selected: true },
{ number: 2, selected: true },
{ number: 3, selected: false },
{ number: 5, selected: false }
{ number: 3, selected: false }
]
});
}));
Expand All @@ -347,8 +359,7 @@ describe('DraftGenerationStepsComponent', () => {
expect(component.selectableTrainingBooksByProj('project01')).toEqual([
{ number: 1, selected: true },
{ number: 2, selected: true },
{ number: 3, selected: false },
{ number: 5, selected: false }
{ number: 3, selected: false }
]);
expect(component.selectableTrainingBooksByProj(sourceProjectId)).toEqual([
{ number: 1, selected: true },
Expand All @@ -374,6 +385,10 @@ describe('DraftGenerationStepsComponent', () => {
expect(component.emptyTranslateSourceBooks).toEqual([5]);
}));

it('should set "emptyTrainingSourceBooks"', fakeAsync(() => {
expect(component.emptyTrainingSourceBooks).toEqual([5]);
}));

it('should set "unusableTranslateTargetBooks" and "unusableTrainingTargetBooks" correctly', fakeAsync(() => {
expect(component.unusableTranslateTargetBooks).toEqual([7]);
expect(component.unusableTrainingTargetBooks).toEqual([7]);
Expand Down Expand Up @@ -496,10 +511,7 @@ describe('DraftGenerationStepsComponent', () => {
component.tryAdvanceStep();
fixture.detectChanges();
component.onTranslatedBookSelect([1]);
expect(component.selectableTrainingBooksByProj('project01')).toEqual([
{ number: 1, selected: true },
{ number: 5, selected: false }
]);
expect(component.selectableTrainingBooksByProj('project01')).toEqual([{ number: 1, selected: true }]);
expect(component.selectedTrainingBooksByProj('project01')).toEqual([{ number: 1, selected: true }]);
expect(component.selectedTrainingBooksByProj('sourceProject')).toEqual([{ number: 1, selected: true }]);
component.stepper.selectedIndex = 1;
Expand All @@ -511,8 +523,7 @@ describe('DraftGenerationStepsComponent', () => {
// Exodus becomes a selectable training book
expect(component.selectableTrainingBooksByProj('project01')).toEqual([
{ number: 1, selected: true },
{ number: 2, selected: false },
{ number: 5, selected: false }
{ number: 2, selected: false }
]);
expect(component.selectedTrainingBooksByProj('sourceProject')).toEqual([{ number: 1, selected: true }]);
expect(component.selectedTrainingBooksByProj('project01')).toEqual([{ number: 1, selected: true }]);
Expand Down Expand Up @@ -618,9 +629,11 @@ describe('DraftGenerationStepsComponent', () => {
});

describe('two training sources', () => {
const availableBooks = [{ bookNum: 2 }, { bookNum: 3 }];
const availableBooks = [{ bookNum: 2 }, { bookNum: 3 }, { bookNum: 5 }];
const allBooks = [{ bookNum: 1 }, ...availableBooks, { bookNum: 6 }, { bookNum: 7 }, { bookNum: 8 }];
const draftingSourceBooks = availableBooks.concat({ bookNum: 7 });
const trainingSource1Books = availableBooks.concat({ bookNum: 1 });
const trainingSource2Books = availableBooks.concat({ bookNum: 6 });
const draftingSourceId = 'draftingSource';
const config = {
trainingSources: [
Expand All @@ -629,14 +642,14 @@ describe('DraftGenerationStepsComponent', () => {
paratextId: 'PT_SP1',
shortName: 'sP1',
writingSystem: { tag: 'eng' },
texts: availableBooks.concat({ bookNum: 1 })
texts: trainingSource1Books
},
{
projectRef: 'source2',
paratextId: 'PT_SP2',
shortName: 'sP2',
writingSystem: { tag: 'eng' },
texts: availableBooks.concat({ bookNum: 6 })
texts: trainingSource2Books
}
] as [DraftSource, DraftSource],
trainingTargets: [
Expand All @@ -657,14 +670,27 @@ describe('DraftGenerationStepsComponent', () => {
] as [DraftSource]
};

const emptyBooks = [5];
beforeEach(fakeAsync(() => {
when(mockDraftSourceService.getDraftProjectSources()).thenReturn(of(config));
when(mockActivatedProjectService.projectDoc$).thenReturn(of({} as any));
when(mockActivatedProjectService.changes$).thenReturn(of({} as any));
when(mockActivatedProjectService.projectDoc).thenReturn({} as any);
// setup mock sources with empty book
setupProjectProfileMock(
draftingSourceId,
draftingSourceBooks.map(b => b.bookNum)
draftingSourceBooks.map(b => b.bookNum),
emptyBooks
);
setupProjectProfileMock(
'source1',
trainingSource1Books.map(b => b.bookNum),
emptyBooks
);
setupProjectProfileMock(
'source2',
trainingSource2Books.map(b => b.bookNum),
emptyBooks
);
when(mockFeatureFlagService.showDeveloperTools).thenReturn(createTestFeatureFlag(false));
when(mockNllbLanguageService.isNllbLanguageAsync(anything())).thenResolve(true);
Expand All @@ -680,6 +706,7 @@ describe('DraftGenerationStepsComponent', () => {
expect(component.allAvailableTranslateBooks).toEqual([
{ number: 2, selected: false },
{ number: 3, selected: false },
{ number: 5, selected: false },
{ number: 7, selected: false }
]);
}));
Expand Down Expand Up @@ -720,6 +747,8 @@ describe('DraftGenerationStepsComponent', () => {
fixture.detectChanges();
expect(component.unusableTranslateSourceBooks).toEqual([1, 6, 8]);
expect(component.unusableTrainingSourceBooks).toEqual([6, 7, 8]);
expect(component.emptyTranslateSourceBooks).toEqual([5]);
expect(component.emptyTrainingSourceBooks).toEqual([5]);

// interact with unusable books notice
const unusableTranslateBooks = fixture.nativeElement.querySelector('.unusable-translate-books');
Expand Down Expand Up @@ -1456,10 +1485,18 @@ describe('DraftGenerationStepsComponent', () => {
const profileDoc = {
id: projectId,
data: createTestProjectProfile({
texts: texts.map(b => ({ bookNum: b, chapters: [{ number: 1, lastVerse: emptyBooks.includes(b) ? 0 : 10 }] }))
texts: texts.map(b => ({ bookNum: b, chapters: [{ number: 1, lastVerse: 10 }] }))
})
} as SFProjectProfileDoc;

when(mockProjectService.getProfile(projectId)).thenResolve(profileDoc);
const textProgress = texts.map(bookNum => {
return {
text: { bookNum },
translated: emptyBooks.includes(bookNum) ? 0 : 100,
percentage: emptyBooks.includes(bookNum) ? 0 : 100
} as TextProgress;
});
when(mockProgressService.getTextProgressForProject(projectId)).thenResolve(textProgress);
}
});
Loading
Loading