From 2f0fc7e95e1428fe365420a29eeef894d8a193f0 Mon Sep 17 00:00:00 2001 From: Peter Chapman Date: Tue, 11 Feb 2025 14:27:35 +1300 Subject: [PATCH 1/2] SF-3202 Add IsDraftingEnabled flag to Projects API --- .../Models/ParatextProject.cs | 10 ++++ .../Services/ParatextService.cs | 10 +++- .../Services/ParatextServiceTests.cs | 52 +++++++++++++++++-- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/SIL.XForge.Scripture/Models/ParatextProject.cs b/src/SIL.XForge.Scripture/Models/ParatextProject.cs index bd782d0a5d..0c703595ed 100644 --- a/src/SIL.XForge.Scripture/Models/ParatextProject.cs +++ b/src/SIL.XForge.Scripture/Models/ParatextProject.cs @@ -56,6 +56,15 @@ public class ParatextProject /// public bool IsConnected { get; init; } + /// + /// If the specified project has drafting enabled. + /// + /// + /// A true value does not infer that the user has access to drafting, + /// but that drafting has been configured or can be configured for the project. + /// + public bool IsDraftingEnabled { get; init; } + /// /// A descriptive string of object's properties, for debugging. /// @@ -77,6 +86,7 @@ public override string ToString() ProjectId, IsConnectable.ToString(), IsConnected.ToString(), + IsDraftingEnabled.ToString(), IsRightToLeft?.ToString(), } ) diff --git a/src/SIL.XForge.Scripture/Services/ParatextService.cs b/src/SIL.XForge.Scripture/Services/ParatextService.cs index e4f8fad6ae..9bfbdc31e7 100644 --- a/src/SIL.XForge.Scripture/Services/ParatextService.cs +++ b/src/SIL.XForge.Scripture/Services/ParatextService.cs @@ -2252,8 +2252,7 @@ private IReadOnlyList GetProjects( IEnumerable projectsMetadata ) { - if (userSecret == null) - throw new ArgumentNullException(); + ArgumentNullException.ThrowIfNull(userSecret, nameof(userSecret)); List paratextProjects = []; IQueryable existingSfProjects = _realtimeService.QuerySnapshots(); @@ -2281,6 +2280,11 @@ IEnumerable projectsMetadata correspondingSfProject?.WritingSystem.Tag ?? projectMD?.LanguageId.Code ); + // Determine if drafting is enabled + bool isBackTranslation = + correspondingSfProject?.TranslateConfig.ProjectType == ProjectType.BackTranslation.ToString(); + bool preTranslationEnabled = correspondingSfProject?.TranslateConfig.PreTranslate == true; + paratextProjects.Add( new ParatextProject { @@ -2293,6 +2297,7 @@ IEnumerable projectsMetadata ProjectId = correspondingSfProject?.Id, IsConnectable = ptProjectIsConnectable, IsConnected = sfProjectExists && sfUserIsOnSfProject, + IsDraftingEnabled = isBackTranslation || preTranslationEnabled, } ); } @@ -2817,6 +2822,7 @@ CancellationToken token : 0, IsConnectable = false, IsConnected = false, + IsDraftingEnabled = false, IsInstalled = resourceRevisions.ContainsKey(r.DBLEntryUid.Id), LanguageRegion = writingSystem.Region, LanguageScript = writingSystem.Script, diff --git a/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs index 4a08a682e4..fff6ff3b55 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs @@ -263,6 +263,40 @@ await env.Service.GetProjectsAsync(testCase.userSecret) } } + [Test] + public async Task GetProjectsAsync_IsDraftingEnabled() + { + // Setup + var env = new TestEnvironment(); + UserSecret userSecret = TestEnvironment.MakeUserSecret(env.User01, env.Username01, env.ParatextUserId01); + env.SetSharedRepositorySource(userSecret, UserRoles.Administrator); + + // 1: A back translation with pre-translation disabled + SFProject project1 = env.NewSFProject(env.Project01); + project1.TranslateConfig.ProjectType = ProjectType.BackTranslation.ToString(); + + // 2: Not a back translation and pre-translation enabled + SFProject project2 = env.NewSFProject(env.Project02); + project2.TranslateConfig.PreTranslate = true; + + // 3: Not a back translation and pre-translation disabled + SFProject project3 = env.NewSFProject(env.Project03); + env.AddProjectRepository([project1, project2, project3]); + + // SUT + IReadOnlyList projects = await env.Service.GetProjectsAsync(userSecret); + Assert.AreEqual(3, projects.Count); + + // 1: A back translation with pre-translation disabled + Assert.IsTrue(projects[0].IsDraftingEnabled); + + // 2: Not a back translation and pre-translation enabled + Assert.IsTrue(projects[1].IsDraftingEnabled); + + // 3: Not a back translation and pre-translation disabled + Assert.IsFalse(projects[2].IsDraftingEnabled); + } + [Test] public async Task GetResourcesAsync_ReturnResources() { @@ -6569,11 +6603,13 @@ public IInternetSharedRepositorySource SetSharedRepositorySource( return mockSource; } - public SFProject NewSFProject() => - new SFProject + public SFProject NewSFProject(string? projectId = null) + { + projectId ??= Project01; + return new SFProject { - Id = "sf_id_" + Project01, - ParatextId = PTProjectIds[Project01].Id, + Id = "sf_id_" + projectId, + ParatextId = PTProjectIds[projectId].Id, Name = "Full Name " + Project01, ShortName = "P01", WritingSystem = new WritingSystem { Tag = "en" }, @@ -6634,6 +6670,7 @@ public SFProject NewSFProject() => }, }, }; + } public void AddTextDataOps(string projectId, string book, int chapter) { @@ -6685,7 +6722,12 @@ public void AddTextDataOps(string projectId, string book, int chapter) public void AddProjectRepository(SFProject proj = null) { proj ??= NewSFProject(); - RealtimeService.AddRepository("sf_projects", OTType.Json0, new MemoryRepository([proj])); + AddProjectRepository([proj]); + } + + public void AddProjectRepository(SFProject[] projects) + { + RealtimeService.AddRepository("sf_projects", OTType.Json0, new MemoryRepository(projects)); MockFileSystemService .DirectoryExists( Arg.Is((string path) => path.EndsWith(Path.Combine(PTProjectIds[Project01].Id, "target"))) From 9ef21388f04dcf3afd71d733616a4e9a83c75185 Mon Sep 17 00:00:00 2001 From: Peter Chapman Date: Thu, 13 Feb 2025 09:30:27 +1300 Subject: [PATCH 2/2] SF-3202 Add HasDraft flag to Projects API --- .../Models/ParatextProject.cs | 6 ++ .../Services/ParatextService.cs | 62 ++++++++-------- .../Services/ParatextServiceTests.cs | 74 ++++++++++++++++--- 3 files changed, 102 insertions(+), 40 deletions(-) diff --git a/src/SIL.XForge.Scripture/Models/ParatextProject.cs b/src/SIL.XForge.Scripture/Models/ParatextProject.cs index 0c703595ed..9cbc53250c 100644 --- a/src/SIL.XForge.Scripture/Models/ParatextProject.cs +++ b/src/SIL.XForge.Scripture/Models/ParatextProject.cs @@ -65,6 +65,11 @@ public class ParatextProject /// public bool IsDraftingEnabled { get; init; } + /// + /// If the specified project has a draft generated. + /// + public bool HasDraft { get; init; } + /// /// A descriptive string of object's properties, for debugging. /// @@ -87,6 +92,7 @@ public override string ToString() IsConnectable.ToString(), IsConnected.ToString(), IsDraftingEnabled.ToString(), + HasDraft.ToString(), IsRightToLeft?.ToString(), } ) diff --git a/src/SIL.XForge.Scripture/Services/ParatextService.cs b/src/SIL.XForge.Scripture/Services/ParatextService.cs index 9bfbdc31e7..750059b257 100644 --- a/src/SIL.XForge.Scripture/Services/ParatextService.cs +++ b/src/SIL.XForge.Scripture/Services/ParatextService.cs @@ -675,23 +675,25 @@ CancellationToken token token ); - users = JArray - .Parse(response) - .Where(m => - !string.IsNullOrEmpty((string?)m["userId"]) - && !string.IsNullOrEmpty((string)m["username"]) - && !string.IsNullOrEmpty((string?)m["role"]) - ) - .Select(m => new ParatextProjectUser - { - ParatextId = (string)m["userId"] ?? string.Empty, - Role = (string)m["role"] ?? string.Empty, - Username = (string)m["username"] ?? string.Empty, - }) - .ToList(); + users = + [ + .. JArray + .Parse(response) + .Where(m => + !string.IsNullOrEmpty((string?)m["userId"]) + && !string.IsNullOrEmpty((string)m["username"]) + && !string.IsNullOrEmpty((string?)m["role"]) + ) + .Select(m => new ParatextProjectUser + { + ParatextId = (string)m["userId"] ?? string.Empty, + Role = (string)m["role"] ?? string.Empty, + Username = (string)m["username"] ?? string.Empty, + }), + ]; // Get the mapping of Scripture Forge user IDs to Paratext usernames - string[] paratextIds = users.Select(p => p.ParatextId).ToArray(); + string[] paratextIds = [.. users.Select(p => p.ParatextId)]; Dictionary userMapping = _realtimeService .QuerySnapshots() .Where(u => paratextIds.Contains(u.ParatextId)) @@ -1253,10 +1255,7 @@ Dictionary ptProjectUsers if (existingThread is null) { // The thread has been removed - threadChange.NoteIdsRemoved = threadDoc - .Data.Notes.Where(n => !n.Deleted) - .Select(n => n.DataId) - .ToList(); + threadChange.NoteIdsRemoved = [.. threadDoc.Data.Notes.Where(n => !n.Deleted).Select(n => n.DataId)]; if (threadChange.NoteIdsRemoved.Count > 0) changes.Add(threadChange); continue; @@ -1534,8 +1533,8 @@ Term term in projectSettingsBiblicalTerms.Terms.Where(t => TermLocalization termLocalization = termLocalizations.GetTermLocalization(term.Id); BiblicalTermDefinition biblicalTermDefinition = new BiblicalTermDefinition { - Categories = term.CategoryIds.Select(termLocalizations.GetCategoryLocalization).ToList(), - Domains = term.SemanticDomains.Select(termLocalizations.GetDomainLocalization).ToList(), + Categories = [.. term.CategoryIds.Select(termLocalizations.GetCategoryLocalization)], + Domains = [.. term.SemanticDomains.Select(termLocalizations.GetDomainLocalization)], Gloss = !string.IsNullOrEmpty(termLocalization.Gloss) ? termLocalization.Gloss : term.Gloss, Notes = termLocalization.Notes, }; @@ -1546,11 +1545,11 @@ Term term in projectSettingsBiblicalTerms.Terms.Where(t => { TermId = term.Id, Transliteration = term.Transliteration, - Renderings = termRendering.RenderingsEntries.ToList(), + Renderings = [.. termRendering.RenderingsEntries], Description = termRendering.Notes, Language = term.Language, - Links = term.Links.ToList(), - References = term.VerseRefs().Select(v => v.BBBCCCVVV).ToList(), + Links = [.. term.Links], + References = [.. term.VerseRefs().Select(v => v.BBBCCCVVV)], Definitions = definitions, }; biblicalTermsChanges.BiblicalTerms.Add(biblicalTerm); @@ -2284,6 +2283,12 @@ IEnumerable projectsMetadata bool isBackTranslation = correspondingSfProject?.TranslateConfig.ProjectType == ProjectType.BackTranslation.ToString(); bool preTranslationEnabled = correspondingSfProject?.TranslateConfig.PreTranslate == true; + bool isDraftingEnabled = isBackTranslation || preTranslationEnabled; + + // Determine if there is a draft + bool hasDraft = + isDraftingEnabled + && correspondingSfProject?.Texts.Any(t => t.Chapters.Any(c => c.HasDraft == true)) == true; paratextProjects.Add( new ParatextProject @@ -2297,7 +2302,8 @@ IEnumerable projectsMetadata ProjectId = correspondingSfProject?.Id, IsConnectable = ptProjectIsConnectable, IsConnected = sfProjectExists && sfUserIsOnSfProject, - IsDraftingEnabled = isBackTranslation || preTranslationEnabled, + IsDraftingEnabled = isDraftingEnabled, + HasDraft = hasDraft, } ); } @@ -2717,9 +2723,7 @@ private SyncMetricInfo PutCommentThreads( private void WriteCommentXml(CommentManager commentManager, string username) { - CommentList userComments = new CommentList( - commentManager.AllComments.Where(comment => comment.User == username) - ); + CommentList userComments = [.. commentManager.AllComments.Where(comment => comment.User == username)]; string fileName = commentManager.GetUserFileName(username); string path = Path.Combine(commentManager.ScrText.Directory, fileName); using Stream stream = _fileSystemService.CreateFile(path); @@ -3114,7 +3118,7 @@ private static string GetNoteContentFromComment(Paratext.Data.ProjectComments.Co return content; XDocument doc = XDocument.Parse(content); XElement contentNode = (XElement)doc.FirstNode; - XNode[] nodes = contentNode.Nodes().ToArray(); + XNode[] nodes = [.. contentNode.Nodes()]; if (!nodes.Any()) return string.Empty; diff --git a/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs index fff6ff3b55..2496af8460 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs @@ -100,7 +100,7 @@ public async Task GetProjectsAsync_ReturnCorrectRepos() ); // Repos are returned in alphabetical order by paratext project name. - List repoList = repos.Select(repo => repo.Name).ToList(); + List repoList = [.. repos.Select(repo => repo.Name)]; Assert.That(StringComparer.InvariantCultureIgnoreCase.Compare(repoList[0], repoList[1]), Is.LessThan(0)); Assert.That(StringComparer.InvariantCultureIgnoreCase.Compare(repoList[1], repoList[2]), Is.LessThan(0)); } @@ -297,6 +297,41 @@ public async Task GetProjectsAsync_IsDraftingEnabled() Assert.IsFalse(projects[2].IsDraftingEnabled); } + [Test] + public async Task GetProjectsAsync_HasDraft() + { + // Setup + var env = new TestEnvironment(); + UserSecret userSecret = TestEnvironment.MakeUserSecret(env.User01, env.Username01, env.ParatextUserId01); + env.SetSharedRepositorySource(userSecret, UserRoles.Administrator); + + // 1: Pre-translation enabled and a draft is present + SFProject project1 = env.NewSFProject(env.Project01); + project1.TranslateConfig.PreTranslate = true; + project1.Texts[0].Chapters[0].HasDraft = true; + + // 2: Pre-translation enabled and no draft is present + SFProject project2 = env.NewSFProject(env.Project02); + project2.TranslateConfig.PreTranslate = true; + + // 3: Pre-translation disabled + SFProject project3 = env.NewSFProject(env.Project03); + env.AddProjectRepository([project1, project2, project3]); + + // SUT + IReadOnlyList projects = await env.Service.GetProjectsAsync(userSecret); + Assert.AreEqual(3, projects.Count); + + // 1: Pre-translation enabled and a draft is present + Assert.IsTrue(projects[0].HasDraft); + + // 2: Pre-translation enabled and no draft is present + Assert.IsFalse(projects[1].HasDraft); + + // 3: Pre-translation disabled + Assert.IsFalse(projects[2].HasDraft); + } + [Test] public async Task GetResourcesAsync_ReturnResources() { @@ -1732,9 +1767,17 @@ public async Task GetNoteThreadChanges_DuplicateCommentsToExistingThread() { new ParatextUserProfile { OpaqueUserId = "syncuser01", Username = env.Username01 }, }.ToDictionary(u => u.Username); - List changes = env - .Service.GetNoteThreadChanges(userSecret, projectId, 40, noteThreadDocs, chapterDeltas, ptProjectUsers) - .ToList(); + List changes = + [ + .. env.Service.GetNoteThreadChanges( + userSecret, + projectId, + 40, + noteThreadDocs, + chapterDeltas, + ptProjectUsers + ), + ]; Assert.That(changes.Count, Is.EqualTo(1)); Assert.That(changes[0].NotesAdded.Count, Is.EqualTo(1)); @@ -2691,9 +2734,17 @@ public async Task GetNoteThreadChanges_SupportsBiblicalTerms() Dictionary chapterDeltas = env.GetChapterDeltasByBook(1, "Context before ", "Text selected"); // SUT - IList changes = env - .Service.GetNoteThreadChanges(userSecret, ptProjectId, 40, noteThreadDocs, chapterDeltas, ptProjectUsers) - .ToList(); + IList changes = + [ + .. env.Service.GetNoteThreadChanges( + userSecret, + ptProjectId, + 40, + noteThreadDocs, + chapterDeltas, + ptProjectUsers + ), + ]; // We fetched a single change, of one new note to create. Assert.That(changes.Count, Is.EqualTo(1)); @@ -2789,9 +2840,10 @@ public async Task UpdateParatextComments_AddsComment() CommentThread thread = env.ProjectCommentManager.FindThread(thread1); Assert.That(thread, Is.Null); string[] noteThreadDataIds = [dataId1, dataId2, dataId3]; - List> noteThreadDocs = ( - await TestEnvironment.GetNoteThreadDocsAsync(conn, noteThreadDataIds) - ).ToList(); + List> noteThreadDocs = + [ + .. (await TestEnvironment.GetNoteThreadDocsAsync(conn, noteThreadDataIds)), + ]; Dictionary ptProjectUsers = new Dictionary { { @@ -7361,7 +7413,7 @@ private static string GetAssignedUserStr(ThreadNoteComponents[] notes) { if (notes == null) return CommentThread.unassignedUser; - List notesList = new List(notes); + List notesList = [.. notes]; return notesList.LastOrDefault(n => n.assignedPTUser != null).assignedPTUser ?? CommentThread.unassignedUser; }