diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.html index 7e85db47cf7..7c573b8e6c9 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.html @@ -309,7 +309,7 @@

{{ t("importing_draft") }}

@if (progress.failedChapters.length > 0) {
error - {{ t("failed") }} {{ getFailedChapters(progress) }} + {{ t("failed") }} {{ getFailedChapters(progress) }}
} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.scss index d734deebb40..3c07f4f1f94 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.scss @@ -46,6 +46,10 @@ .error-icon { font-size: 1.1rem; } + + .error-message { + width: 100%; + } } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.ts index 8fbc151812c..3e0a62cd464 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.ts @@ -792,16 +792,30 @@ export class DraftImportWizardComponent implements OnInit { } getFailedChapters(progress: ImportProgress): string { - const failedChapters: number[] = progress.failedChapters.filter(f => f.chapterNum !== 0).map(f => f.chapterNum); if (!progress.failedChapters.some(f => f.chapterNum === 0)) { - // A subset of chapters failed - return failedChapters.join(', '); + // A subset of chapters failed. Display the failure message next to each chapter, if present. + return progress.failedChapters + .filter(f => f.chapterNum !== 0) + .map(f => (f.message ? `${f.chapterNum} (${f.message})` : f.chapterNum)) + .join(', '); } else if (progress.totalChapters > 1) { - // All chapters failed, so display as a range - return `1-${progress.totalChapters}`; + // All chapters failed, so display as a range, with any failure messages + return ( + `1-${progress.totalChapters}. ` + + progress.failedChapters + .filter(f => f.chapterNum === 0 && f.message != null) + .map(f => f.message) + .join(', ') + ).trim(); } else { - // The only chapter in the book failed - return `${progress.totalChapters}`; + // The only chapter in the book failed, so display that chapter number with any failure messages + return ( + `${progress.totalChapters}. ` + + progress.failedChapters + .filter(f => f.chapterNum === 0 && f.message != null) + .map(f => f.message) + .join(', ') + ).trim(); } } diff --git a/src/SIL.XForge.Scripture/Services/IParatextService.cs b/src/SIL.XForge.Scripture/Services/IParatextService.cs index a1644cd1c2e..75f556051fe 100644 --- a/src/SIL.XForge.Scripture/Services/IParatextService.cs +++ b/src/SIL.XForge.Scripture/Services/IParatextService.cs @@ -82,6 +82,8 @@ Task UpdateParatextPermissionsForNewBooksAsync( UserSecret userSecret, string paratextId, IDocument projectDoc, + int[] booksToUpdate, + bool currentUserOnly, bool writeToParatext ); string? GetLatestSharedVersion(UserSecret userSecret, string paratextId); diff --git a/src/SIL.XForge.Scripture/Services/MachineApiService.cs b/src/SIL.XForge.Scripture/Services/MachineApiService.cs index 35d30caf7b8..5cef43831a7 100644 --- a/src/SIL.XForge.Scripture/Services/MachineApiService.cs +++ b/src/SIL.XForge.Scripture/Services/MachineApiService.cs @@ -501,15 +501,28 @@ await projectService.UpdatePermissionsAsync( .ToList(), cancellationToken ); + + // When the user is a project administrator, and do not have permission to + // update the specified books, add the permissions for them to update them. + await paratextService.UpdateParatextPermissionsForNewBooksAsync( + userSecret, + targetProjectDoc.Data.ParatextId, + targetProjectDoc, + booksToUpdate: [.. updatedBooks], + currentUserOnly: true, + writeToParatext: false + ); } if (createdBooks.Count > 0) { - // Update permissions for new books + // Update permissions for new books, adding all users that should have access to them await paratextService.UpdateParatextPermissionsForNewBooksAsync( userSecret, targetProjectDoc.Data.ParatextId, targetProjectDoc, + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ); } @@ -533,7 +546,7 @@ await draftHubContext.NotifyDraftApplyProgress( BookNum = bookNum, ChapterNum = 0, Status = DraftApplyStatus.Failed, - Message = $"Could not save draft for {Canon.BookNumberToId(bookNum)}.", + Message = $"You do not have permission to write to this book.", } ); } @@ -563,7 +576,7 @@ await draftHubContext.NotifyDraftApplyProgress( BookNum = bookNum, ChapterNum = 0, Status = DraftApplyStatus.Failed, - Message = $"Could not save draft for {Canon.BookNumberToId(bookNum)}.", + Message = $"You do not have permission to write to this book.", } ); } @@ -585,8 +598,7 @@ await draftHubContext.NotifyDraftApplyProgress( BookNum = bookNum, ChapterNum = chapterDelta.Number, Status = DraftApplyStatus.Failed, - Message = - $"Could not save draft for {Canon.BookNumberToId(bookNum)} {chapterDelta.Number}.", + Message = $"You do not have permission to write to this chapter.", } ); continue; @@ -619,8 +631,7 @@ await draftHubContext.NotifyDraftApplyProgress( BookNum = bookNum, ChapterNum = chapterDelta.Number, Status = DraftApplyStatus.Failed, - Message = - $"Could not save draft for {Canon.BookNumberToId(bookNum)} {chapterDelta.Number}.", + Message = $"You do not have permission to write to this chapter.", } ); continue; diff --git a/src/SIL.XForge.Scripture/Services/ParatextService.cs b/src/SIL.XForge.Scripture/Services/ParatextService.cs index 6f62568ee5a..27deb6473d5 100644 --- a/src/SIL.XForge.Scripture/Services/ParatextService.cs +++ b/src/SIL.XForge.Scripture/Services/ParatextService.cs @@ -1732,10 +1732,27 @@ IReadOnlyList biblicalTerms return syncMetricInfo; } + /// + /// Updates Paratext permissions for any new books, or the books specified in . + /// + /// The user secret. + /// The Paratext project identifier + /// The project document. + /// + /// The book numbers to update. + /// If specified these books will be updated, otherwise only new books not on disk will be updated. + /// + /// If true, only update permissions for the current user. + /// + /// If true, update the Paratext project on disk; otherwise if false, update the project document. + /// + /// public async Task UpdateParatextPermissionsForNewBooksAsync( UserSecret userSecret, string paratextId, IDocument projectDoc, + int[] booksToUpdate, + bool currentUserOnly, bool writeToParatext ) { @@ -1751,19 +1768,26 @@ bool writeToParatext return syncMetricInfo; } - // Get all projects that are not on disk + // Get all books that are not on disk for (int i = 0; i < projectDoc.Data.Texts.Count; i++) { TextInfo text = projectDoc.Data.Texts[i]; int bookNum = text.BookNum; - if (scrText.BookPresent(bookNum)) + if (booksToUpdate.Length == 0 && scrText.BookPresent(bookNum)) + { + // This book is already on disk, so skip to the next book + continue; + } + else if (booksToUpdate.Length > 0 && !booksToUpdate.Contains(bookNum)) { - // Book is on disk, skip to the next book + // This book is not one of the ones we are to update, so skip to the next book continue; } // Add any users to the book who would have the ability to access it - foreach (var user in projectDoc.Data.ParatextUsers) + foreach ( + var user in projectDoc.Data.ParatextUsers.Where(u => !currentUserOnly || u.SFUserId == userSecret.Id) + ) { // If there is no SF user id or PT username, ignore this user if (string.IsNullOrEmpty(user.SFUserId) || string.IsNullOrEmpty(user.Username)) diff --git a/src/SIL.XForge.Scripture/Services/ParatextSyncRunner.cs b/src/SIL.XForge.Scripture/Services/ParatextSyncRunner.cs index 1298a152c22..6b48e9fffe5 100644 --- a/src/SIL.XForge.Scripture/Services/ParatextSyncRunner.cs +++ b/src/SIL.XForge.Scripture/Services/ParatextSyncRunner.cs @@ -216,6 +216,8 @@ CancellationToken token _userSecret, targetParatextId, _projectDoc, + booksToUpdate: [], + currentUserOnly: false, writeToParatext: true ); await GetAndUpdateParatextBooksAndNotes( diff --git a/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs index e809c420cc6..4ba95f69ec6 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs @@ -240,6 +240,8 @@ public async Task ApplyPreTranslationToProjectAsync_ExceptionFromParatext() Arg.Any(), Arg.Any(), Arg.Any>(), + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ) .ThrowsAsync(new NotSupportedException()); @@ -5118,13 +5120,15 @@ int writeChapters } }); - // Update the permissions for the user applying the draft + // Update the permissions for the user adding new books ParatextService .When(x => x.UpdateParatextPermissionsForNewBooksAsync( Arg.Any(), Arg.Any(), Arg.Any>(), + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ) ) @@ -5147,6 +5151,39 @@ int writeChapters } } }); + + // Update the permissions for the user applying the draft to existing books + ParatextService + .When(x => + x.UpdateParatextPermissionsForNewBooksAsync( + Arg.Any(), + Arg.Any(), + Arg.Any>(), + booksToUpdate: Arg.Any(), + currentUserOnly: true, + writeToParatext: false + ) + ) + .Do(callInfo => + { + UserSecret userSecret = callInfo.ArgAt(0); + var projectDoc = callInfo.ArgAt>(2); + int[] booksToUpdate = callInfo.ArgAt(3); + foreach (var text in projectDoc.Data.Texts.Where(t => booksToUpdate.Contains(t.BookNum))) + { + text.Permissions.TryAdd( + userSecret.Id, + canWriteBook ? TextInfoPermission.Write : TextInfoPermission.Read + ); + foreach (var chapter in text.Chapters) + { + chapter.Permissions.TryAdd( + userSecret.Id, + chapter.Number <= writeChapters ? TextInfoPermission.Write : TextInfoPermission.Read + ); + } + } + }); } public async Task VerifyDraftAsync( @@ -5166,6 +5203,8 @@ await ParatextService Arg.Any(), Arg.Any(), Arg.Any>(), + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ); } diff --git a/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs index ba2e68f65da..d84258ea479 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs @@ -6443,6 +6443,8 @@ public async Task UpdateParatextPermissionsForNewBooksAsync_AllBooksPresentOnDis userSecret, paratextId, projectDoc, + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ); SyncMetricInfo expected = new SyncMetricInfo(); @@ -6472,6 +6474,8 @@ public async Task UpdateParatextPermissionsForNewBooksAsync_MissingScrText() userSecret, paratextId, projectDoc, + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ); SyncMetricInfo expected = new SyncMetricInfo(); @@ -6501,6 +6505,8 @@ public async Task UpdateParatextPermissionsForNewBooksAsync_ProjectNotEditable() userSecret, paratextId, projectDoc, + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ); SyncMetricInfo expected = new SyncMetricInfo(); @@ -6530,6 +6536,8 @@ public async Task UpdateParatextPermissionsForNewBooksAsync_ProjectHasNoBooks() userSecret, paratextId, projectDoc, + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ); SyncMetricInfo expected = new SyncMetricInfo(); @@ -6564,6 +6572,8 @@ public async Task UpdateParatextPermissionsForNewBooksAsync_UpdatesMongo() userSecret, paratextId, projectDoc, + booksToUpdate: [], + currentUserOnly: false, writeToParatext: false ); SyncMetricInfo expected = new SyncMetricInfo { Updated = 1 }; @@ -6608,6 +6618,8 @@ public async Task UpdateParatextPermissionsForNewBooksAsync_UpdatesParatext() userSecret, paratextId, projectDoc, + booksToUpdate: [], + currentUserOnly: false, writeToParatext: true ); SyncMetricInfo expected = new SyncMetricInfo { Updated = 1 }; diff --git a/test/SIL.XForge.Scripture.Tests/Services/ParatextSyncRunnerTests.cs b/test/SIL.XForge.Scripture.Tests/Services/ParatextSyncRunnerTests.cs index 1b43b7a846d..1955c8cb9d8 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/ParatextSyncRunnerTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/ParatextSyncRunnerTests.cs @@ -758,6 +758,8 @@ await env Arg.Any(), Arg.Any(), Arg.Any>(), + booksToUpdate: [], + currentUserOnly: false, writeToParatext: true ); }