Skip to content

Commit d66fd40

Browse files
authored
Merge branch 'master' into fix/incorrect-import-paths
2 parents 981b953 + 07e0760 commit d66fd40

File tree

49 files changed

+742
-787
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+742
-787
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/permissions.service.spec.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,16 @@ describe('PermissionsService', () => {
105105

106106
const textDoc: Partial<TextDocId> = { projectId: 'project01', bookNum: 41, chapterNum: 1 };
107107

108-
expect(await env.service.canAccessTextAsync(cloneDeep(textDoc) as TextDocId)).toBe(true);
108+
expect(await env.service.canAccessText(cloneDeep(textDoc) as TextDocId)).toBe(true);
109109
}));
110110

111111
it('allows access to text if user is on project and does not have chapter permission', fakeAsync(async () => {
112112
const env = new TestEnvironment();
113-
env.setupProjectData(undefined, false);
113+
env.setupProjectData(undefined);
114114

115115
const textDoc: Partial<TextDocId> = { projectId: 'project01', bookNum: 41, chapterNum: 1 };
116116

117-
expect(await env.service.canAccessTextAsync(cloneDeep(textDoc) as TextDocId)).toBe(true);
117+
expect(await env.service.canAccessText(cloneDeep(textDoc) as TextDocId)).toBe(true);
118118
}));
119119

120120
it('does not allow access to text if user is not on project', fakeAsync(async () => {
@@ -123,7 +123,7 @@ describe('PermissionsService', () => {
123123

124124
const textDoc: Partial<TextDocId> = { projectId: 'project01', bookNum: 41, chapterNum: 1 };
125125

126-
expect(await env.service.canAccessTextAsync(cloneDeep(textDoc) as TextDocId)).toBe(false);
126+
expect(await env.service.canAccessText(cloneDeep(textDoc) as TextDocId)).toBe(false);
127127
}));
128128

129129
it('does not allow access to text if user has no access', fakeAsync(async () => {
@@ -132,7 +132,7 @@ describe('PermissionsService', () => {
132132

133133
const textDoc: Partial<TextDocId> = { projectId: 'project01', bookNum: 41, chapterNum: 1 };
134134

135-
expect(await env.service.canAccessTextAsync(cloneDeep(textDoc) as TextDocId)).toBe(false);
135+
expect(await env.service.canAccessText(cloneDeep(textDoc) as TextDocId)).toBe(false);
136136
}));
137137

138138
it('does not allow access to text if the chapter does not exist', fakeAsync(async () => {
@@ -141,7 +141,7 @@ describe('PermissionsService', () => {
141141

142142
const textDoc: Partial<TextDocId> = { projectId: 'project01', bookNum: 41, chapterNum: 2 };
143143

144-
expect(await env.service.canAccessTextAsync(cloneDeep(textDoc) as TextDocId)).toBe(false);
144+
expect(await env.service.canAccessText(cloneDeep(textDoc) as TextDocId)).toBe(false);
145145
}));
146146

147147
it('checks current user doc to determine if user is on project', fakeAsync(async () => {
@@ -264,7 +264,7 @@ class TestEnvironment {
264264
this.setupUserData();
265265
}
266266

267-
setupProjectData(textPermission?: TextInfoPermission | undefined, chapterPermissions: boolean = true): void {
267+
setupProjectData(textPermission?: TextInfoPermission | undefined): void {
268268
const projectId: string = 'project01';
269269
const permission: TextInfoPermission = textPermission ?? TextInfoPermission.Write;
270270

@@ -284,12 +284,10 @@ class TestEnvironment {
284284
number: 1,
285285
lastVerse: 3,
286286
isValid: true,
287-
permissions: chapterPermissions
288-
? {
289-
user01: permission,
290-
user02: permission
291-
}
292-
: {}
287+
permissions: {
288+
user01: permission,
289+
user02: permission
290+
}
293291
}
294292
],
295293
hasSource: true,

src/SIL.XForge.Scripture/ClientApp/src/app/core/permissions.service.ts

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Injectable } from '@angular/core';
22
import { Operation } from 'realtime-server/lib/esm/common/models/project-rights';
3-
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
43
import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights';
54
import { isParatextRole, SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
6-
import { Chapter, TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
5+
import { Chapter } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
76
import { TextInfoPermission } from 'realtime-server/lib/esm/scriptureforge/models/text-info-permission';
87
import { UserDoc } from 'xforge-common/models/user-doc';
98
import { UserService } from 'xforge-common/user.service';
@@ -59,25 +58,25 @@ export class PermissionsService {
5958
return isParatextRole(projectDoc.data?.userRoles[currentUserDoc.id] ?? SFProjectRole.None);
6059
}
6160

62-
/**
63-
* Determines if a user can access the text in the specified project.
64-
* @param textDocId The text document id.
65-
* @param project The project.
66-
* @returns A boolean value.
67-
*/
68-
canAccessText(textDocId: TextDocId, project?: SFProjectProfile): boolean {
61+
async canAccessText(textDocId: TextDocId): Promise<boolean> {
62+
// Get the project doc, if the user is on that project
63+
let projectDoc: SFProjectProfileDoc | undefined;
64+
if (textDocId.projectId != null) {
65+
const isUserOnProject = await this.isUserOnProject(textDocId.projectId);
66+
projectDoc = isUserOnProject ? await this.projectService.getProfile(textDocId.projectId) : undefined;
67+
}
68+
6969
// Ensure the user has project level permission to view the text
7070
if (
71-
project != null &&
72-
SF_PROJECT_RIGHTS.hasRight(project, this.userService.currentUserId, SFProjectDomain.Texts, Operation.View)
71+
projectDoc?.data != null &&
72+
SF_PROJECT_RIGHTS.hasRight(projectDoc.data, this.userService.currentUserId, SFProjectDomain.Texts, Operation.View)
7373
) {
7474
// Check chapter permissions
75-
const text: TextInfo | undefined = project.texts.find(t => t.bookNum === textDocId.bookNum);
76-
const chapter: Chapter | undefined = text?.chapters.find(c => c.number === textDocId.chapterNum);
77-
if (text != null && chapter != null) {
78-
// If the chapter permission is not present, use the book permission instead
79-
const chapterPermission: string | undefined =
80-
chapter.permissions[this.userService.currentUserId] ?? text.permissions[this.userService.currentUserId];
75+
const chapter: Chapter | undefined = projectDoc.data.texts
76+
.find(t => t.bookNum === textDocId.bookNum)
77+
?.chapters.find(c => c.number === textDocId.chapterNum);
78+
if (chapter != null) {
79+
const chapterPermission: string | undefined = chapter.permissions[this.userService.currentUserId];
8180
// If there is no chapter permission, they will have access to the chapter as they have access to the project.
8281
// We should only deny access if there is an explicit "None" permission.
8382
return chapterPermission !== TextInfoPermission.None;
@@ -87,23 +86,6 @@ export class PermissionsService {
8786
return false;
8887
}
8988

90-
/**
91-
* Determines if a user can access a text.
92-
* @param textDocId The text document id.
93-
* @returns A promise for a boolean value.
94-
*/
95-
async canAccessTextAsync(textDocId: TextDocId): Promise<boolean> {
96-
// Get the project doc, if the user is on that project
97-
let projectDoc: SFProjectProfileDoc | undefined;
98-
if (textDocId.projectId != null) {
99-
const isUserOnProject = await this.isUserOnProject(textDocId.projectId);
100-
projectDoc = isUserOnProject ? await this.projectService.getProfile(textDocId.projectId) : undefined;
101-
return this.canAccessText(textDocId, projectDoc?.data);
102-
}
103-
104-
return false;
105-
}
106-
10789
canSync(projectDoc: SFProjectProfileDoc, userId?: string): boolean {
10890
if (projectDoc.data == null) {
10991
return false;

0 commit comments

Comments
 (0)