Skip to content

Commit b1f1587

Browse files
committed
Migrate editing to editingRequires, disabling editing on older clients
1 parent da88714 commit b1f1587

14 files changed

Lines changed: 218 additions & 17 deletions

File tree

src/RealtimeServer/scriptureforge/models/sf-project-test-data.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import merge from 'lodash/merge';
22
import { RecursivePartial } from '../../common/utils/type-utils';
33
import { CheckingAnswerExport } from './checking-config';
4-
import { SFProject, SFProjectProfile } from './sf-project';
4+
import { EditingRequires, SFProject, SFProjectProfile } from './sf-project';
55

66
function testProjectProfile(ordinal: number): SFProjectProfile {
77
return {
@@ -50,7 +50,8 @@ function testProjectProfile(ordinal: number): SFProjectProfile {
5050
punctuationCheckerEnabled: false,
5151
allowedCharacterCheckerEnabled: false
5252
},
53-
editable: true,
53+
editable: false,
54+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
5455
defaultFontSize: 12,
5556
defaultFont: 'Charis SIL',
5657
maxGeneratedUsersPerShareKey: 250

src/RealtimeServer/scriptureforge/models/sf-project.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface SFProjectProfile extends Project {
4444
noteTags?: NoteTag[];
4545
sync: Sync;
4646
editable: boolean;
47+
editingRequires: EditingRequires;
4748
defaultFontSize?: number;
4849
defaultFont?: string;
4950
maxGeneratedUsersPerShareKey?: number;
@@ -69,3 +70,26 @@ export function isResource(project: SFProjectProfile): boolean {
6970
const resourceIdLength: number = DBL_RESOURCE_ID_LENGTH;
7071
return project.paratextId.length === resourceIdLength;
7172
}
73+
74+
/**
75+
* A bitwise-flag enumeration to represent what frontend features are required to edit this project's text documents.
76+
*
77+
* To add more required features, add as follows:
78+
*
79+
* FutureFeatureA = 1 << 2, // 4
80+
* FutureFeatureB = 1 << 3, // 8
81+
*
82+
* NOTE: Adding a new required feature and migrating editingRequires to include it will block older editors.
83+
* The new required featured should be added to the MaxSupportedEditingRequiresValue below.
84+
*/
85+
export enum EditingRequires {
86+
ParatextEditingEnabled = 1 << 0, // 1
87+
ViewModelBlankSupport = 1 << 1 // 2
88+
}
89+
90+
/**
91+
* This value is by the frontend to determine if a feature has been added
92+
* which should disable editing on the frontend until it is updated.
93+
*/
94+
export const MaxSupportedEditingRequiresValue: EditingRequires =
95+
EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport;

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,69 @@ describe('SFProjectMigrations', () => {
627627
expect(projectDoc.data.translateConfig.draftConfig.additionalTrainingData).toBeUndefined();
628628
});
629629
});
630+
631+
describe('version 26', () => {
632+
it('migrates editable to editingRequires when true', async () => {
633+
const env = new TestEnvironment(25);
634+
const conn = env.server.connect();
635+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
636+
editable: true
637+
});
638+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
639+
expect(projectDoc.data.editable).toBe(true);
640+
641+
await env.server.migrateIfNecessary();
642+
643+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
644+
expect(projectDoc.data.editable).toBe(false);
645+
expect(projectDoc.data.editingRequires).toBe(3);
646+
});
647+
648+
it('migrates editable to editingRequires when false', async () => {
649+
const env = new TestEnvironment(25);
650+
const conn = env.server.connect();
651+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
652+
editable: false
653+
});
654+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
655+
expect(projectDoc.data.editable).toBe(false);
656+
657+
await env.server.migrateIfNecessary();
658+
659+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
660+
expect(projectDoc.data.editable).toBe(false);
661+
expect(projectDoc.data.editingRequires).toBe(2);
662+
});
663+
664+
it('migrates editable to editingRequires when null', async () => {
665+
const env = new TestEnvironment(25);
666+
const conn = env.server.connect();
667+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {});
668+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
669+
expect(projectDoc.data.editable).toBeUndefined();
670+
671+
await env.server.migrateIfNecessary();
672+
673+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
674+
expect(projectDoc.data.editable).toBe(false);
675+
expect(projectDoc.data.editingRequires).toBe(3);
676+
});
677+
678+
it('does not remigrate editingRequires', async () => {
679+
const env = new TestEnvironment(25);
680+
const conn = env.server.connect();
681+
await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', { editingRequires: 6 });
682+
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
683+
expect(projectDoc.data.editable).toBeUndefined();
684+
expect(projectDoc.data.editingRequires).toBe(6);
685+
686+
await env.server.migrateIfNecessary();
687+
688+
projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
689+
expect(projectDoc.data.editable).toBeUndefined();
690+
expect(projectDoc.data.editingRequires).toBe(6);
691+
});
692+
});
630693
});
631694

632695
class TestEnvironment {

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DocMigration, MigrationConstructor, monotonicallyIncreasingMigrationLis
44
import { Operation } from '../../common/models/project-rights';
55
import { submitMigrationOp } from '../../common/realtime-server';
66
import { NoteTag } from '../models/note-tag';
7+
import { EditingRequires } from '../models/sf-project';
78
import { SF_PROJECT_RIGHTS, SFProjectDomain } from '../models/sf-project-rights';
89
import { SFProjectRole } from '../models/sf-project-role';
910
import { TextInfoPermission } from '../models/text-info-permission';
@@ -477,6 +478,39 @@ class SFProjectMigration25 extends DocMigration {
477478
}
478479
}
479480

481+
class SFProjectMigration26 extends DocMigration {
482+
static readonly VERSION = 26;
483+
484+
async migrateDoc(doc: Doc): Promise<void> {
485+
const ops: Op[] = [];
486+
487+
if (doc.data.editingRequires == null) {
488+
const editable: boolean = doc.data.editable !== false;
489+
if (doc.data.editable == null) {
490+
ops.push({
491+
p: ['editable'],
492+
oi: false
493+
});
494+
} else if (doc.data.editable == true) {
495+
ops.push({
496+
p: ['editable'],
497+
od: true,
498+
oi: false
499+
});
500+
}
501+
502+
ops.push({
503+
p: ['editingRequires'],
504+
oi: (editable ? EditingRequires.ParatextEditingEnabled : 0) | EditingRequires.ViewModelBlankSupport
505+
});
506+
}
507+
508+
if (ops.length > 0) {
509+
await submitMigrationOp(SFProjectMigration26.VERSION, doc, ops);
510+
}
511+
}
512+
}
513+
480514
export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncreasingMigrationList([
481515
SFProjectMigration1,
482516
SFProjectMigration2,
@@ -502,5 +536,6 @@ export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncrea
502536
SFProjectMigration22,
503537
SFProjectMigration23,
504538
SFProjectMigration24,
505-
SFProjectMigration25
539+
SFProjectMigration25,
540+
SFProjectMigration26
506541
]);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const SF_PROJECT_PROFILE_FIELDS: ShareDB.ProjectionFields = {
2727
isRightToLeft: true,
2828
biblicalTermsConfig: true,
2929
editable: true,
30+
editingRequires: true,
3031
defaultFontSize: true,
3132
defaultFont: true,
3233
translateConfig: true,
@@ -539,6 +540,9 @@ export class SFProjectService extends ProjectService<SFProject> {
539540
editable: {
540541
bsonType: 'bool'
541542
},
543+
editingRequires: {
544+
bsonType: 'int'
545+
},
542546
defaultFontSize: {
543547
bsonType: 'int'
544548
},

src/SIL.XForge.Scripture/ClientApp/src/app/core/text-doc.service.spec.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
22
import { Delta } from 'quill';
3+
import { EditingRequires } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
34
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
45
import { createTestProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data';
56
import { TextData } from 'realtime-server/lib/esm/scriptureforge/models/text-data';
@@ -79,7 +80,7 @@ describe('TextDocService', () => {
7980
it('should return true if the project and user are correctly configured', () => {
8081
const env = new TestEnvironment();
8182
const project = createTestProjectProfile({
82-
editable: true,
83+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
8384
sync: { dataInSync: true },
8485
texts: [
8586
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -105,7 +106,7 @@ describe('TextDocService', () => {
105106
it('should return false if user does not have general edit right', () => {
106107
const env = new TestEnvironment();
107108
const project = createTestProjectProfile({
108-
editable: true,
109+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
109110
sync: { dataInSync: true },
110111
texts: [
111112
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -121,7 +122,7 @@ describe('TextDocService', () => {
121122
it('should return false if user does not have chapter edit permission', () => {
122123
const env = new TestEnvironment();
123124
const project = createTestProjectProfile({
124-
editable: true,
125+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
125126
sync: { dataInSync: true },
126127
texts: [
127128
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Read } }] }
@@ -137,7 +138,7 @@ describe('TextDocService', () => {
137138
it('should return false if data is not in sync', () => {
138139
const env = new TestEnvironment();
139140
const project = createTestProjectProfile({
140-
editable: true,
141+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
141142
sync: { dataInSync: false },
142143
texts: [
143144
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -153,7 +154,7 @@ describe('TextDocService', () => {
153154
it('should return false if editing is disabled', () => {
154155
const env = new TestEnvironment();
155156
const project = createTestProjectProfile({
156-
editable: false,
157+
editingRequires: EditingRequires.ViewModelBlankSupport,
157158
sync: { dataInSync: true },
158159
texts: [
159160
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -169,7 +170,7 @@ describe('TextDocService', () => {
169170
it('should return true if all conditions are met', () => {
170171
const env = new TestEnvironment();
171172
const project = createTestProjectProfile({
172-
editable: true,
173+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport,
173174
sync: { dataInSync: true },
174175
texts: [
175176
{ bookNum: 1, chapters: [{ number: 1, isValid: true, permissions: { user01: TextInfoPermission.Write } }] }
@@ -241,7 +242,9 @@ describe('TextDocService', () => {
241242

242243
it('should return false if the project is editable', () => {
243244
const env = new TestEnvironment();
244-
const project = createTestProjectProfile({ editable: true });
245+
const project = createTestProjectProfile({
246+
editingRequires: EditingRequires.ParatextEditingEnabled | EditingRequires.ViewModelBlankSupport
247+
});
245248

246249
// SUT
247250
const actual: boolean = env.textDocService.isEditingDisabled(project);
@@ -250,7 +253,29 @@ describe('TextDocService', () => {
250253

251254
it('should return true if the project is not editable', () => {
252255
const env = new TestEnvironment();
253-
const project = createTestProjectProfile({ editable: false });
256+
const project = createTestProjectProfile({ editingRequires: EditingRequires.ViewModelBlankSupport });
257+
258+
// SUT
259+
const actual: boolean = env.textDocService.isEditingDisabled(project);
260+
expect(actual).toBe(true);
261+
});
262+
263+
it('should return true if the project is has been upgraded to a version beyond the supported version', () => {
264+
const env = new TestEnvironment();
265+
const project = createTestProjectProfile({
266+
editingRequires: Number.MAX_SAFE_INTEGER
267+
});
268+
269+
// SUT
270+
const actual: boolean = env.textDocService.isEditingDisabled(project);
271+
expect(actual).toBe(true);
272+
});
273+
274+
it('should return true if the project is has not been upgraded to view model support', () => {
275+
const env = new TestEnvironment();
276+
const project = createTestProjectProfile({
277+
editingRequires: EditingRequires.ParatextEditingEnabled
278+
});
254279

255280
// SUT
256281
const actual: boolean = env.textDocService.isEditingDisabled(project);

src/SIL.XForge.Scripture/ClientApp/src/app/core/text-doc.service.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Injectable } from '@angular/core';
22
import { Delta } from 'quill';
33
import { Operation } from 'realtime-server/lib/esm/common/models/project-rights';
4-
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
4+
import {
5+
EditingRequires,
6+
MaxSupportedEditingRequiresValue,
7+
SFProjectProfile
8+
} from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
59
import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights';
610
import { TextData } from 'realtime-server/lib/esm/scriptureforge/models/text-data';
711
import { Chapter, TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
@@ -103,14 +107,29 @@ export class TextDocService {
103107
return project?.sync?.dataInSync !== false;
104108
}
105109

110+
/**
111+
* Determines if an update is required to allow editing.
112+
*
113+
* @param {SFProjectProfile | undefined} project The project.
114+
* @returns {boolean} A value indicating whether the app must be updated.
115+
*/
116+
isUpdateRequired(project: SFProjectProfile | undefined): boolean {
117+
return (project?.editingRequires ?? 0) > MaxSupportedEditingRequiresValue;
118+
}
119+
106120
/**
107121
* Determines if editing is disabled for a project.
108122
*
109123
* @param {SFProjectProfile | undefined} project The project.
110124
* @returns {boolean} A value indicating whether editing is disabled for the project.
111125
*/
112126
isEditingDisabled(project: SFProjectProfile | undefined): boolean {
113-
return project?.editable === false;
127+
return (
128+
project != null &&
129+
(this.isUpdateRequired(project) ||
130+
(project.editingRequires & EditingRequires.ViewModelBlankSupport) !== EditingRequires.ViewModelBlankSupport ||
131+
(project.editingRequires & EditingRequires.ParatextEditingEnabled) !== EditingRequires.ParatextEditingEnabled)
132+
);
114133
}
115134

116135
/**

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@
142142
{{ t("project_data_out_of_sync") }}
143143
</app-notice>
144144
}
145+
@if (updateRequired && hasEditRight) {
146+
<app-notice mode="fill-dark" type="warning" icon="warning" class="update-required-warning">
147+
{{ t("update_required") }}
148+
</app-notice>
149+
}
145150
@if (target.areOpsCorrupted && hasEditRight) {
146151
<app-notice mode="fill-dark" type="error" icon="error" class="doc-corrupted-warning">
147152
{{ t("text_doc_corrupted") }}

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,10 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy,
540540
return this.textDocService.isDataInSync(this.projectDoc?.data);
541541
}
542542

543+
get updateRequired(): boolean {
544+
return this.textDocService.isUpdateRequired(this.projectDoc?.data);
545+
}
546+
543547
get issueEmailLink(): string {
544548
return getLinkHTML(environment.issueEmail, issuesEmailTemplate());
545549
}

src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@
395395
"text_doc_corrupted": "This chapter cannot be edited because the data has become corrupted.",
396396
"text_has_been_deleted": "The book has been deleted or is no longer accessible.",
397397
"to_report_issue_email": "To report an issue, email {{ issueEmailLink }}.",
398+
"update_required": "You are running an older version of Scripture Forge. Please refresh this page to edit this text.",
398399
"verse_too_long_for_suggestions": "This verse is too long to generate suggestions.",
399400
"your_comment": "Your comment"
400401
},

0 commit comments

Comments
 (0)