diff --git a/src/SIL.XForge.Scripture/Controllers/MachineApiController.cs b/src/SIL.XForge.Scripture/Controllers/MachineApiController.cs
index 9d3448f3c6..cf5682a522 100644
--- a/src/SIL.XForge.Scripture/Controllers/MachineApiController.cs
+++ b/src/SIL.XForge.Scripture/Controllers/MachineApiController.cs
@@ -7,6 +7,7 @@
using Microsoft.AspNetCore.Mvc;
using Polly.CircuitBreaker;
using Serval.Client;
+using SIL.Converters.Usj;
using SIL.XForge.Models;
using SIL.XForge.Realtime;
using SIL.XForge.Scripture.Models;
@@ -427,6 +428,61 @@ CancellationToken cancellationToken
}
}
+ ///
+ /// Gets the pre-translations for the specified chapter as USJ.
+ ///
+ /// The Scripture Forge project identifier.
+ /// The book number.
+ /// The chapter number. If zero, the entire book is returned.
+ /// The cancellation token.
+ /// The pre-translations were successfully queried for.
+ /// You do not have permission to retrieve the pre-translations for this project.
+ /// The project does not exist or is not configured on the ML server.
+ /// Retrieving the pre-translations in this format is not supported.
+ /// The engine has not been built on the ML server.
+ /// The ML server is temporarily unavailable or unresponsive.
+ [HttpGet(MachineApi.GetPreTranslationUsj)]
+ public async Task> GetPreTranslationUsjAsync(
+ string sfProjectId,
+ int bookNum,
+ int chapterNum,
+ CancellationToken cancellationToken
+ )
+ {
+ try
+ {
+ Usj usj = await _machineApiService.GetPreTranslationUsjAsync(
+ _userAccessor.UserId,
+ sfProjectId,
+ bookNum,
+ chapterNum,
+ cancellationToken
+ );
+ return Ok(usj);
+ }
+ catch (BrokenCircuitException e)
+ {
+ _exceptionHandler.ReportException(e);
+ return StatusCode(StatusCodes.Status503ServiceUnavailable, MachineApiUnavailable);
+ }
+ catch (DataNotFoundException)
+ {
+ return NotFound();
+ }
+ catch (ForbiddenException)
+ {
+ return Forbid();
+ }
+ catch (InvalidOperationException)
+ {
+ return Conflict();
+ }
+ catch (NotSupportedException)
+ {
+ return new StatusCodeResult(StatusCodes.Status405MethodNotAllowed);
+ }
+ }
+
///
/// Gets the pre-translations for the specified chapter as USX.
///
diff --git a/src/SIL.XForge.Scripture/Models/MachineApi.cs b/src/SIL.XForge.Scripture/Models/MachineApi.cs
index cc76baa421..26f171363d 100644
--- a/src/SIL.XForge.Scripture/Models/MachineApi.cs
+++ b/src/SIL.XForge.Scripture/Models/MachineApi.cs
@@ -27,6 +27,8 @@ public static class MachineApi
"translation/engines/project:{sfProjectId}/actions/preTranslate/{bookNum}_{chapterNum}/delta";
public const string GetPreTranslationUsfm =
"translation/engines/project:{sfProjectId}/actions/preTranslate/{bookNum}_{chapterNum}/usfm";
+ public const string GetPreTranslationUsj =
+ "translation/engines/project:{sfProjectId}/actions/preTranslate/{bookNum}_{chapterNum}/usj";
public const string GetPreTranslationUsx =
"translation/engines/project:{sfProjectId}/actions/preTranslate/{bookNum}_{chapterNum}/usx";
public const string GetLastCompletedPreTranslationBuild =
diff --git a/src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj b/src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj
index f50af68bbc..73b7c8be61 100644
--- a/src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj
+++ b/src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj
@@ -62,6 +62,7 @@
+
diff --git a/src/SIL.XForge.Scripture/Services/IMachineApiService.cs b/src/SIL.XForge.Scripture/Services/IMachineApiService.cs
index 7572ab8846..77a0b906ec 100644
--- a/src/SIL.XForge.Scripture/Services/IMachineApiService.cs
+++ b/src/SIL.XForge.Scripture/Services/IMachineApiService.cs
@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Autofac.Extras.DynamicProxy;
using Serval.Client;
+using SIL.Converters.Usj;
using SIL.XForge.EventMetrics;
using SIL.XForge.Realtime;
using SIL.XForge.Scripture.Models;
@@ -67,6 +68,13 @@ Task GetPreTranslationUsfmAsync(
bool isServalAdmin,
CancellationToken cancellationToken
);
+ Task GetPreTranslationUsjAsync(
+ string curUserId,
+ string sfProjectId,
+ int bookNum,
+ int chapterNum,
+ CancellationToken cancellationToken
+ );
Task GetPreTranslationUsxAsync(
string curUserId,
string sfProjectId,
diff --git a/src/SIL.XForge.Scripture/Services/MachineApiService.cs b/src/SIL.XForge.Scripture/Services/MachineApiService.cs
index e95f21282d..53e33531e8 100644
--- a/src/SIL.XForge.Scripture/Services/MachineApiService.cs
+++ b/src/SIL.XForge.Scripture/Services/MachineApiService.cs
@@ -11,6 +11,7 @@
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Serval.Client;
+using SIL.Converters.Usj;
using SIL.ObjectModel;
using SIL.XForge.Configuration;
using SIL.XForge.DataAccess;
@@ -558,6 +559,42 @@ CancellationToken cancellationToken
}
}
+ public async Task GetPreTranslationUsjAsync(
+ string curUserId,
+ string sfProjectId,
+ int bookNum,
+ int chapterNum,
+ CancellationToken cancellationToken
+ )
+ {
+ // Ensure that the user has permission
+ SFProject project = await EnsureProjectPermissionAsync(curUserId, sfProjectId);
+
+ // Retrieve the user secret
+ Attempt attempt = await userSecrets.TryGetAsync(curUserId);
+ if (!attempt.TryResult(out UserSecret userSecret))
+ {
+ throw new DataNotFoundException("The user does not exist.");
+ }
+
+ try
+ {
+ string usfm = await preTranslationService.GetPreTranslationUsfmAsync(
+ sfProjectId,
+ bookNum,
+ chapterNum,
+ cancellationToken
+ );
+ string usx = paratextService.GetBookText(userSecret, project.ParatextId, bookNum, usfm);
+ return UsxToUsj.UsxStringToUsj(usx);
+ }
+ catch (ServalApiException e)
+ {
+ ProcessServalApiException(e);
+ throw;
+ }
+ }
+
public async Task GetPreTranslationUsxAsync(
string curUserId,
string sfProjectId,
diff --git a/test/SIL.XForge.Scripture.Tests/Controllers/MachineApiControllerTests.cs b/test/SIL.XForge.Scripture.Tests/Controllers/MachineApiControllerTests.cs
index 03de4cc6d5..63f780e51a 100644
--- a/test/SIL.XForge.Scripture.Tests/Controllers/MachineApiControllerTests.cs
+++ b/test/SIL.XForge.Scripture.Tests/Controllers/MachineApiControllerTests.cs
@@ -9,6 +9,7 @@
using NUnit.Framework;
using Polly.CircuitBreaker;
using Serval.Client;
+using SIL.Converters.Usj;
using SIL.XForge.Models;
using SIL.XForge.Realtime;
using SIL.XForge.Scripture.Models;
@@ -1045,6 +1046,123 @@ await env
.GetPreTranslationUsfmAsync(User01, Project01, 40, 1, false, CancellationToken.None);
}
+ [Test]
+ public async Task GetPreTranslationUsjAsync_MachineApiDown()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None)
+ .Throws(new BrokenCircuitException());
+
+ // SUT
+ ActionResult actual = await env.Controller.GetPreTranslationUsjAsync(
+ Project01,
+ 40,
+ 1,
+ CancellationToken.None
+ );
+
+ env.ExceptionHandler.Received(1).ReportException(Arg.Any());
+ Assert.IsInstanceOf(actual.Result);
+ Assert.AreEqual(StatusCodes.Status503ServiceUnavailable, (actual.Result as ObjectResult)?.StatusCode);
+ }
+
+ [Test]
+ public async Task GetPreTranslationUsjAsync_NoPermission()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None)
+ .Throws(new ForbiddenException());
+
+ // SUT
+ ActionResult actual = await env.Controller.GetPreTranslationUsjAsync(
+ Project01,
+ 40,
+ 1,
+ CancellationToken.None
+ );
+
+ Assert.IsInstanceOf(actual.Result);
+ }
+
+ [Test]
+ public async Task GetPreTranslationUsjAsync_NoProject()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None)
+ .Throws(new DataNotFoundException(string.Empty));
+
+ // SUT
+ ActionResult actual = await env.Controller.GetPreTranslationUsjAsync(
+ Project01,
+ 40,
+ 1,
+ CancellationToken.None
+ );
+
+ Assert.IsInstanceOf(actual.Result);
+ }
+
+ [Test]
+ public async Task GetPreTranslationUsjAsync_NotBuilt()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None)
+ .Throws(new InvalidOperationException());
+
+ // SUT
+ ActionResult actual = await env.Controller.GetPreTranslationUsjAsync(
+ Project01,
+ 40,
+ 1,
+ CancellationToken.None
+ );
+
+ Assert.IsInstanceOf(actual.Result);
+ }
+
+ [Test]
+ public async Task GetPreTranslationUsjAsync_NotSupported()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None)
+ .Throws(new NotSupportedException());
+
+ // SUT
+ ActionResult actual = await env.Controller.GetPreTranslationUsjAsync(
+ Project01,
+ 40,
+ 1,
+ CancellationToken.None
+ );
+
+ Assert.IsInstanceOf(actual.Result);
+ Assert.AreEqual(StatusCodes.Status405MethodNotAllowed, (actual.Result as IStatusCodeActionResult)?.StatusCode);
+ }
+
+ [Test]
+ public async Task GetPreTranslationUsjAsync_Success()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None)
+ .Returns(Task.FromResult(new Usj()));
+
+ // SUT
+ ActionResult actual = await env.Controller.GetPreTranslationUsjAsync(
+ Project01,
+ 40,
+ 1,
+ CancellationToken.None
+ );
+
+ Assert.IsInstanceOf(actual.Result);
+ }
+
[Test]
public async Task GetPreTranslationUsxAsync_MachineApiDown()
{
diff --git a/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs
index f022dd2a23..e2d09a3eac 100644
--- a/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs
+++ b/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs
@@ -14,6 +14,7 @@
using NUnit.Framework;
using Polly.CircuitBreaker;
using Serval.Client;
+using SIL.Converters.Usj;
using SIL.XForge.DataAccess;
using SIL.XForge.Models;
using SIL.XForge.Realtime;
@@ -1263,6 +1264,78 @@ public async Task GetPreTranslationUsfmAsync_Success()
Assert.AreEqual(expected, usfm);
}
+ [Test]
+ public void GetPreTranslationUsjAsync_CorpusDoesNotSupportUsfm()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ env.PreTranslationService.GetPreTranslationUsfmAsync(Project01, 40, 1, CancellationToken.None)
+ .Throws(ServalApiExceptions.InvalidCorpus);
+
+ // SUT
+ Assert.ThrowsAsync(
+ () => env.Service.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None)
+ );
+ }
+
+ [Test]
+ public async Task GetPreTranslationUsjAsync_MissingUserSecret()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ await env.UserSecrets.DeleteAllAsync(_ => true);
+
+ // SUT
+ Assert.ThrowsAsync(
+ () => env.Service.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None)
+ );
+ }
+
+ [Test]
+ public async Task GetPreTranslationUsjAsync_Success()
+ {
+ // Set up test environment
+ var env = new TestEnvironment();
+ const string usfm = "\\c 1 \\v1 Verse 1";
+ const string usx =
+ ""
+ + "Verse 1";
+ Usj expected = new Usj
+ {
+ Type = Usj.UsjType,
+ Version = Usj.UsjVersion,
+ Content =
+ [
+ new UsjMarker
+ {
+ Type = "book",
+ Marker = "id",
+ Code = "MAT",
+ },
+ new UsjMarker
+ {
+ Type = "chapter",
+ Marker = "c",
+ Number = "1",
+ },
+ new UsjMarker
+ {
+ Type = "verse",
+ Marker = "v",
+ Number = "1",
+ },
+ "Verse 1",
+ ],
+ };
+ env.PreTranslationService.GetPreTranslationUsfmAsync(Project01, 40, 1, CancellationToken.None)
+ .Returns(Task.FromResult(usfm));
+ env.ParatextService.GetBookText(Arg.Any(), Arg.Any(), 40, usfm).Returns(usx);
+
+ // SUT
+ Usj actual = await env.Service.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None);
+ Assert.That(actual, Is.EqualTo(expected).UsingPropertiesComparer());
+ }
+
[Test]
public void GetPreTranslationUsxAsync_CorpusDoesNotSupportUsfm()
{