Skip to content

Commit 9f18297

Browse files
committed
SF-3618 Add write permission when importing a new book
fix
1 parent cab9339 commit 9f18297

File tree

5 files changed

+401
-42
lines changed

5 files changed

+401
-42
lines changed

src/SIL.XForge.Scripture/Services/IParatextService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ SyncMetricInfo UpdateBiblicalTerms(
7777
string paratextId,
7878
IReadOnlyList<BiblicalTerm> biblicalTerms
7979
);
80+
Task<SyncMetricInfo> UpdateParatextPermissionsForNewBooksAsync(
81+
UserSecret userSecret,
82+
string paratextId,
83+
IDocument<SFProject> projectDoc,
84+
bool writeToParatext
85+
);
8086
string? GetLatestSharedVersion(UserSecret userSecret, string paratextId);
8187
string GetRepoRevision(UserSecret userSecret, string paratextId);
8288
void SetRepoToRevision(UserSecret userSecret, string paratextId, string desiredRevision);

src/SIL.XForge.Scripture/Services/MachineApiService.cs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ CancellationToken cancellationToken
115115
cancellationToken
116116
);
117117

118+
// Retrieve the user secret
119+
Attempt<UserSecret> attempt = await userSecrets.TryGetAsync(curUserId, cancellationToken);
120+
if (!attempt.TryResult(out UserSecret userSecret))
121+
{
122+
throw new DataNotFoundException("The user does not exist.");
123+
}
124+
118125
// Connect to the realtime server
119126
await using IConnection connection = await realtimeService.ConnectAsync(curUserId);
120127

@@ -126,13 +133,6 @@ CancellationToken cancellationToken
126133
List<(ChapterDelta chapterDelta, int bookNum)> chapterDeltas = [];
127134
try
128135
{
129-
// Retrieve the user secret
130-
Attempt<UserSecret> attempt = await userSecrets.TryGetAsync(curUserId, cancellationToken);
131-
if (!attempt.TryResult(out UserSecret userSecret))
132-
{
133-
throw new DataNotFoundException("The user does not exist.");
134-
}
135-
136136
// Load the target project
137137
targetProjectDoc = await connection.FetchAsync<SFProject>(targetProjectId);
138138
if (!targetProjectDoc.IsLoaded)
@@ -404,13 +404,27 @@ await hubContext.NotifyDraftApplyProgress(
404404
sfProjectId,
405405
new DraftApplyState { State = "Loading permissions from Paratext." }
406406
);
407-
await projectService.UpdatePermissionsAsync(
408-
curUserId,
409-
targetProjectDoc,
410-
users: null,
411-
books: chapterDeltas.Select(c => c.bookNum).Distinct().ToList(),
412-
cancellationToken
413-
);
407+
if (createdBooks.Count == 0)
408+
{
409+
// Update books for which chapters were added
410+
await projectService.UpdatePermissionsAsync(
411+
curUserId,
412+
targetProjectDoc,
413+
users: null,
414+
books: chapterDeltas.Select(c => c.bookNum).Distinct().ToList(),
415+
cancellationToken
416+
);
417+
}
418+
else
419+
{
420+
// Update permissions for new books
421+
await paratextService.UpdateParatextPermissionsForNewBooksAsync(
422+
userSecret,
423+
targetProjectDoc.Data.ParatextId,
424+
targetProjectDoc,
425+
writeToParatext: false
426+
);
427+
}
414428
}
415429

416430
// Create the text data documents, using the permissions matrix calculated above for permissions

src/SIL.XForge.Scripture/Services/ParatextService.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
using SIL.XForge.Services;
4848
using SIL.XForge.Utils;
4949
using StringUtils = SIL.XForge.Utils.StringUtils;
50+
using TextInfo = SIL.XForge.Scripture.Models.TextInfo;
5051

5152
namespace SIL.XForge.Scripture.Services;
5253

@@ -1690,6 +1691,107 @@ IReadOnlyList<BiblicalTerm> biblicalTerms
16901691
return syncMetricInfo;
16911692
}
16921693

1694+
public async Task<SyncMetricInfo> UpdateParatextPermissionsForNewBooksAsync(
1695+
UserSecret userSecret,
1696+
string paratextId,
1697+
IDocument<SFProject> projectDoc,
1698+
bool writeToParatext
1699+
)
1700+
{
1701+
var syncMetricInfo = new SyncMetricInfo();
1702+
using ScrText scrText = ScrTextCollection.FindById(GetParatextUsername(userSecret)!, paratextId);
1703+
if (scrText is null)
1704+
{
1705+
return syncMetricInfo;
1706+
}
1707+
1708+
if (!projectDoc.Data.Editable)
1709+
{
1710+
return syncMetricInfo;
1711+
}
1712+
1713+
// Get all projects that are not on disk
1714+
for (int i = 0; i < projectDoc.Data.Texts.Count; i++)
1715+
{
1716+
TextInfo text = projectDoc.Data.Texts[i];
1717+
int bookNum = text.BookNum;
1718+
if (scrText.BookPresent(bookNum))
1719+
{
1720+
// Book is on disk, skip to the next book
1721+
continue;
1722+
}
1723+
1724+
// Add any users to the book who would have the ability to access it
1725+
foreach (var user in projectDoc.Data.ParatextUsers)
1726+
{
1727+
// If there is no SF user id or PT username, ignore this user
1728+
if (string.IsNullOrEmpty(user.SFUserId) || string.IsNullOrEmpty(user.Username))
1729+
{
1730+
continue;
1731+
}
1732+
1733+
bool hasPermissionInParatext = scrText.Permissions.CanEdit(bookNum, chapterNum: 0, user.Username);
1734+
bool hasPermissionInMongo =
1735+
text.Permissions.TryGetValue(user.SFUserId, out string permission)
1736+
&& permission == TextInfoPermission.Write;
1737+
bool userIsAdministrator = scrText.Permissions.GetUser(user.Username)?.Role == UserRoles.Administrator;
1738+
1739+
if (writeToParatext)
1740+
{
1741+
// Grant the user access to edit the new book, if we granted them access in Mongo,
1742+
// they are an administrator, but they do not have access in Paratext.
1743+
//
1744+
// This is based on ParatextData.ImportSfmText.GrantBookPermissions()
1745+
if (hasPermissionInMongo && !hasPermissionInParatext && userIsAdministrator)
1746+
{
1747+
scrText.Permissions.SetPermission(user.Username, bookNum, PermissionSet.Manual, true);
1748+
syncMetricInfo.Updated++;
1749+
}
1750+
}
1751+
else
1752+
{
1753+
// If the user can edit the book and doesn't have permission,
1754+
// or they are the current user and an administrator,
1755+
// update the Scripture Forge permissions to allow writing.
1756+
bool currentUserIsAdministrator = userIsAdministrator && user.SFUserId == userSecret.Id;
1757+
if (!hasPermissionInMongo && (currentUserIsAdministrator || hasPermissionInParatext))
1758+
{
1759+
// Add the user to the new book in SF with book permissions and available chapter permissions
1760+
// This will be empty if the current user is an administrator but has no permissions for the book
1761+
int[] editableChapters =
1762+
[
1763+
.. scrText.Permissions.GetEditableChapters(
1764+
bookNum,
1765+
scrText.Settings.Versification,
1766+
user.Username
1767+
) ?? [],
1768+
];
1769+
await projectDoc.SubmitJson0OpAsync(op =>
1770+
{
1771+
int textIndex = i;
1772+
op.Set(p => p.Texts[textIndex].Permissions[user.SFUserId], TextInfoPermission.Write);
1773+
for (int j = 0; j < text.Chapters.Count; j++)
1774+
{
1775+
int chapterIndex = j;
1776+
int chapterNumber = text.Chapters[chapterIndex].Number;
1777+
if (editableChapters.Contains(chapterNumber) || currentUserIsAdministrator)
1778+
{
1779+
op.Set(
1780+
p => p.Texts[textIndex].Chapters[chapterIndex].Permissions[user.SFUserId],
1781+
TextInfoPermission.Write
1782+
);
1783+
}
1784+
}
1785+
});
1786+
syncMetricInfo.Updated++;
1787+
}
1788+
}
1789+
}
1790+
}
1791+
1792+
return syncMetricInfo;
1793+
}
1794+
16931795
/// <summary>
16941796
/// Get the most recent revision id of a commit from the last push or pull with the PT send/receive server.
16951797
/// </summary>

test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -224,29 +224,28 @@ public async Task ApplyPreTranslationToProjectAsync_ExceptionFromParatext()
224224
// Set up test environment
225225
var env = new TestEnvironment();
226226
env.ConfigureDraft(
227-
Project01,
227+
Project02,
228228
bookNum: 39,
229229
numberOfChapters: 3,
230230
bookExists: true,
231231
draftExists: true,
232232
canWriteBook: true,
233233
writeChapters: 3
234234
);
235-
env.ProjectService.UpdatePermissionsAsync(
235+
env.ParatextService.UpdateParatextPermissionsForNewBooksAsync(
236+
Arg.Any<UserSecret>(),
236237
Arg.Any<string>(),
237238
Arg.Any<IDocument<SFProject>>(),
238-
users: null,
239-
books: Arg.Any<IReadOnlyList<int>>(),
240-
CancellationToken.None
239+
writeToParatext: false
241240
)
242241
.ThrowsAsync(new NotSupportedException());
243242

244243
// SUT
245244
DraftApplyResult actual = await env.Service.ApplyPreTranslationToProjectAsync(
246245
User01,
247-
Project01,
246+
Project02,
248247
scriptureRange: "MAL",
249-
targetProjectId: Project02,
248+
targetProjectId: Project01,
250249
DateTime.UtcNow,
251250
CancellationToken.None
252251
);
@@ -354,19 +353,16 @@ public async Task ApplyPreTranslationToProjectAsync_MissingUserSecret()
354353
await env.UserSecrets.DeleteAllAsync(_ => true);
355354

356355
// SUT
357-
DraftApplyResult actual = await env.Service.ApplyPreTranslationToProjectAsync(
358-
User01,
359-
Project01,
360-
scriptureRange: "GEN",
361-
Project02,
362-
DateTime.UtcNow,
363-
CancellationToken.None
356+
Assert.ThrowsAsync<DataNotFoundException>(() =>
357+
env.Service.ApplyPreTranslationToProjectAsync(
358+
User01,
359+
Project01,
360+
scriptureRange: "GEN",
361+
Project02,
362+
DateTime.UtcNow,
363+
CancellationToken.None
364+
)
364365
);
365-
366-
env.MockLogger.AssertHasEvent(logEvent => logEvent.Exception?.GetType() == typeof(DataNotFoundException));
367-
env.ExceptionHandler.Received().ReportException(Arg.Is<DataNotFoundException>(e => e.Message.Contains("user")));
368-
Assert.That(actual.Log, Is.Not.Empty);
369-
Assert.That(actual.ChangesSaved, Is.False);
370366
}
371367

372368
[Test]
@@ -4930,6 +4926,36 @@ int writeChapters
49304926
}
49314927
}
49324928
});
4929+
4930+
// Update the permissions for the user applying the draft
4931+
ParatextService
4932+
.When(x =>
4933+
x.UpdateParatextPermissionsForNewBooksAsync(
4934+
Arg.Any<UserSecret>(),
4935+
Arg.Any<string>(),
4936+
Arg.Any<IDocument<SFProject>>(),
4937+
writeToParatext: false
4938+
)
4939+
)
4940+
.Do(callInfo =>
4941+
{
4942+
UserSecret userSecret = callInfo.ArgAt<UserSecret>(0);
4943+
var projectDoc = callInfo.ArgAt<IDocument<SFProject>>(2);
4944+
foreach (var text in projectDoc.Data.Texts)
4945+
{
4946+
text.Permissions.TryAdd(
4947+
userSecret.Id,
4948+
canWriteBook ? TextInfoPermission.Write : TextInfoPermission.Read
4949+
);
4950+
foreach (var chapter in text.Chapters)
4951+
{
4952+
chapter.Permissions.TryAdd(
4953+
userSecret.Id,
4954+
chapter.Number <= writeChapters ? TextInfoPermission.Write : TextInfoPermission.Read
4955+
);
4956+
}
4957+
}
4958+
});
49334959
}
49344960

49354961
public async Task VerifyDraftAsync(
@@ -4941,15 +4967,30 @@ public async Task VerifyDraftAsync(
49414967
int writeChapters
49424968
)
49434969
{
4944-
await ProjectService
4945-
.Received()
4946-
.UpdatePermissionsAsync(
4947-
User01,
4948-
Arg.Any<IDocument<SFProject>>(),
4949-
users: null,
4950-
books: Arg.Any<IReadOnlyList<int>>(),
4951-
CancellationToken.None
4952-
);
4970+
if (targetProjectId == Project02)
4971+
{
4972+
await ParatextService
4973+
.Received()
4974+
.UpdateParatextPermissionsForNewBooksAsync(
4975+
Arg.Any<UserSecret>(),
4976+
Arg.Any<string>(),
4977+
Arg.Any<IDocument<SFProject>>(),
4978+
writeToParatext: false
4979+
);
4980+
}
4981+
else
4982+
{
4983+
await ProjectService
4984+
.Received()
4985+
.UpdatePermissionsAsync(
4986+
User01,
4987+
Arg.Any<IDocument<SFProject>>(),
4988+
users: null,
4989+
books: Arg.Any<IReadOnlyList<int>>(),
4990+
CancellationToken.None
4991+
);
4992+
}
4993+
49534994
ExceptionHandler.DidNotReceive().ReportException(Arg.Any<Exception>());
49544995

49554996
await Assert.ThatAsync(

0 commit comments

Comments
 (0)