Skip to content

Commit

Permalink
[CharInv] Track words changed with find-and-replace (#2955)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Mar 28, 2024
1 parent 3c646a2 commit e9b7d72
Show file tree
Hide file tree
Showing 14 changed files with 957 additions and 79 deletions.
71 changes: 71 additions & 0 deletions Backend.Tests/Controllers/WordControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ public void Setup()
_projId = _projRepo.Create(new Project { Name = "WordControllerTests" }).Result!.Id;
}

[Test]
public async Task TestAreInFrontier()
{
var wordNotInFrontier = await _wordRepo.Add(Util.RandomWord(_projId));
var emptyResult = await _wordController.AreInFrontier(_projId, new() { wordNotInFrontier.Id, "non-id" });
Assert.That(((ObjectResult)emptyResult).Value, Is.Empty);

var wordInFrontier = await _wordRepo.AddFrontier(Util.RandomWord(_projId));
var nonemptyResult = await _wordController.AreInFrontier(_projId, new() { wordInFrontier.Id, "non-id" });
Assert.That(((OkObjectResult)nonemptyResult).Value, Is.EqualTo(new List<string> { wordInFrontier.Id }));
}

[Test]
public async Task TestAreInFrontierNoPermission()
{
var wordInFrontier = await _wordRepo.AddFrontier(Util.RandomWord(_projId));
_wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();
var result = await _wordController.AreInFrontier(_projId, new() { wordInFrontier.Id });
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public async Task TestAreInFrontierMissingProject()
{
var result = await _wordController.AreInFrontier(MissingId, new());
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>());
}

[Test]
public async Task TestDeleteFrontierWord()
{
Expand Down Expand Up @@ -266,6 +294,49 @@ public async Task TestGetDuplicateIdMissingProject()
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>());
}

[Test]
public async Task TestRevertWords()
{
var frontierWord0 = await _wordRepo.Create(Util.RandomWord(_projId));
var frontierWord1 = await _wordRepo.AddFrontier(Util.RandomWord(_projId));
var nonFrontierWord0 = await _wordRepo.Add(Util.RandomWord(_projId));
var nonFrontierWord1 = await _wordRepo.Add(Util.RandomWord(_projId));
var nonFrontierWord2 = await _wordRepo.Add(Util.RandomWord(_projId));

var result = await _wordController.RevertWords(_projId, new()
{
["non-id"] = frontierWord1.Id, // Cannot revert with key not a word
[nonFrontierWord1.Id] = nonFrontierWord2.Id, // Cannot revert with value not in frontier
[nonFrontierWord0.Id] = frontierWord0.Id, // Can revert
});
var reverted = (Dictionary<string, string>)((OkObjectResult)result).Value!;
Assert.That(reverted, Has.Count.EqualTo(1));
var frontierIds = (await _wordRepo.GetFrontier(_projId)).Select(w => w.Id).ToList();
Assert.That(frontierIds, Has.Count.EqualTo(2));
Assert.That(frontierIds, Does.Contain(frontierWord1.Id));
Assert.That(frontierIds, Does.Contain(reverted[frontierWord0.Id]));
}

[Test]
public async Task TestRevertWordsNoPermission()
{
_wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();

var oldWord = await _wordRepo.Add(Util.RandomWord(_projId));
var newWord = await _wordRepo.Create(Util.RandomWord(_projId));
var result = await _wordController.RevertWords(_projId, new() { [oldWord.Id] = newWord.Id });
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public async Task TestRevertWordsMissingProject()
{
var oldWord = await _wordRepo.Add(Util.RandomWord(_projId));
var newWord = await _wordRepo.Create(Util.RandomWord(_projId));
var result = await _wordController.RevertWords(MissingId, new() { [oldWord.Id] = newWord.Id });
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>());
}

[Test]
public async Task TestUpdateDuplicate()
{
Expand Down
56 changes: 56 additions & 0 deletions Backend/Controllers/WordController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,31 @@ public async Task<IActionResult> IsInFrontier(string projectId, string wordId)
return Ok(await _wordRepo.IsInFrontier(projectId, wordId));
}

/// <summary> Checks if Frontier has words in specified <see cref="Project"/>. </summary>
[HttpPost("areinfrontier", Name = "AreInFrontier")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<string>))]
public async Task<IActionResult> AreInFrontier(string projectId, [FromBody, BindRequired] List<string> wordIds)
{
if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId))
{
return Forbid();
}
if ((await _projRepo.GetProject(projectId)) is null)
{
return NotFound(projectId);
}

var idsInFrontier = new List<string>();
foreach (var id in wordIds)
{
if (await _wordRepo.IsInFrontier(projectId, id))
{
idsInFrontier.Add(id);
}
}
return Ok(idsInFrontier);
}

/// <summary>
/// Checks if a <see cref="Word"/> is a duplicate--i.e., are its primary text fields
/// (Vernacular, Gloss text, Definition text) contained in a frontier entry?
Expand Down Expand Up @@ -247,5 +272,36 @@ public async Task<IActionResult> UpdateWord(
await _wordService.Update(projectId, userId, wordId, word);
return Ok(word.Id);
}

/// <summary> Revert words from an dictionary of word ids (key: to revert to; value: from frontier). </summary>
/// <returns> Id dictionary of all words successfully updated (key: was in frontier; value: new id). </returns>
[HttpPost("revertwords", Name = "RevertWords")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dictionary<string, string>))]
public async Task<IActionResult> RevertWords(
string projectId, [FromBody, BindRequired] Dictionary<string, string> wordIds)
{
if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId))
{
return Forbid();
}
if ((await _projRepo.GetProject(projectId)) is null)
{
return NotFound(projectId);
}

var updates = new Dictionary<string, string>();
var userId = _permissionService.GetUserId(HttpContext);
foreach (var kv in wordIds)
{
var idToRevert = kv.Value;
var word = await _wordRepo.GetWord(projectId, kv.Key);
if (word is not null && await _wordRepo.IsInFrontier(projectId, idToRevert))
{
await _wordService.Update(projectId, userId, idToRevert, word);
updates[idToRevert] = word.Id;
}
}
return Ok(updates);
}
}
}
17 changes: 13 additions & 4 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@
"undo": {
"undo": "Undo Edit",
"undoDialog": "Undo this edit?",
"undoDisabled": "Undo Unavailable"
"undoDisabled": "Undo unavailable"
}
},
"charInventory": {
Expand Down Expand Up @@ -367,8 +367,17 @@
"no": "no"
},
"changes": {
"noChanges": "No changes made in this goal.",
"more": "more"
"noCharChanges": "No changes made to character inventory.",
"more": "more",
"noWordChangesFindReplace": "No words changed by find-and-replace.",
"wordChanges": "Words changed:",
"wordChangesWithStrings": "Words changed ({{ val1 }} → {{ val2 }}):"
},
"undo": {
"undo": "Undo find-and-replace",
"undoDialog": "Undo words changes by find-and-replace?",
"undoDisabled": "Undo unavailable",
"undoWords": "Undo available for {{ val1 }} of the {{ val2 }} changed words."
}
},
"mergeDups": {
Expand Down Expand Up @@ -425,7 +434,7 @@
"undo": {
"undo": "Undo Merge",
"undoDialog": "Undo this merge?",
"undoDisabled": "Undo Unavailable"
"undoDisabled": "Undo unavailable"
}
},
"flags": {
Expand Down
Loading

0 comments on commit e9b7d72

Please sign in to comment.