diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index 530e618a80..a2c7c5ccf6 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -314,8 +314,8 @@ public async Task TestDeletedWordsExportToLift() word.Id = ""; word.Vernacular = "updated"; - await _wordService.Update(_projId, wordToUpdate.Id, word); - await _wordService.DeleteFrontierWord(_projId, wordToDelete.Id); + await _wordService.Update(_projId, UserId, wordToUpdate.Id, word); + await _wordService.DeleteFrontierWord(_projId, UserId, wordToDelete.Id); await _liftController.CreateLiftExportThenSignal(_projId, UserId); var text = await DownloadAndReadLift(_liftController, _projId); diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index bc0226c802..a62c7c07a4 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -49,36 +49,6 @@ public void Setup() _projId = _projRepo.Create(new Project { Name = "WordControllerTests" }).Result!.Id; } - [Test] - public async Task TestDeleteAllWords() - { - await _wordRepo.Create(Util.RandomWord(_projId)); - await _wordRepo.Create(Util.RandomWord(_projId)); - const string diffProjId = "OTHER_PROJECT"; - await _wordRepo.Create(Util.RandomWord(diffProjId)); - - await _wordController.DeleteProjectWords(_projId); - Assert.That(await _wordRepo.GetAllWords(_projId), Is.Empty); - Assert.That(await _wordRepo.GetFrontier(_projId), Is.Empty); - Assert.That(await _wordRepo.GetAllWords(diffProjId), Has.Count.EqualTo(1)); - Assert.That(await _wordRepo.GetFrontier(diffProjId), Has.Count.EqualTo(1)); - } - - [Test] - public async Task TestDeleteAllWordsNoPermission() - { - _wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); - var result = await _wordController.DeleteProjectWords(_projId); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public async Task TestDeleteAllWordsMissingProject() - { - var result = await _wordController.DeleteProjectWords(MissingId); - Assert.That(result, Is.InstanceOf()); - } - [Test] public async Task TestDeleteFrontierWord() { @@ -275,7 +245,8 @@ public async Task TestUpdateDuplicate() var dupWord = origWord.Clone(); dupWord.Flag = new Flag("New Flag"); var expectedWord = dupWord.Clone(); - var id = (string)((ObjectResult)await _wordController.UpdateDuplicate(_projId, origWord.Id, "", dupWord)).Value!; + var result = (ObjectResult)await _wordController.UpdateDuplicate(_projId, origWord.Id, dupWord); + var id = (string)result.Value!; var updatedWord = await _wordRepo.GetWord(_projId, id); Assert.That(expectedWord.ContentEquals(updatedWord!), Is.True); } @@ -286,7 +257,7 @@ public async Task TestUpdateDuplicateNoPermission() _wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); var word = await _wordRepo.Create(Util.RandomWord(_projId)); - var result = await _wordController.UpdateDuplicate(_projId, word.Id, "", word); + var result = await _wordController.UpdateDuplicate(_projId, word.Id, word); Assert.That(result, Is.InstanceOf()); } @@ -294,7 +265,7 @@ public async Task TestUpdateDuplicateNoPermission() public async Task TestUpdateDuplicateMissingProject() { var word = await _wordRepo.Create(Util.RandomWord(_projId)); - var result = await _wordController.UpdateDuplicate(MissingId, word.Id, "", word); + var result = await _wordController.UpdateDuplicate(MissingId, word.Id, word); Assert.That(result, Is.InstanceOf()); } @@ -302,7 +273,7 @@ public async Task TestUpdateDuplicateMissingProject() public async Task TestUpdateDuplicateMissingWord() { var word = Util.RandomWord(_projId); - var result = await _wordController.UpdateDuplicate(_projId, MissingId, "", word); + var result = await _wordController.UpdateDuplicate(_projId, MissingId, word); Assert.That(result, Is.InstanceOf()); } @@ -312,7 +283,7 @@ public async Task TestUpdateDuplicateNonDuplicate() var origWord = await _wordRepo.Create(Util.RandomWord(_projId)); var nonDup = origWord.Clone(); nonDup.Vernacular = "differentVern"; - var result = await _wordController.UpdateDuplicate(_projId, origWord.Id, "", nonDup); + var result = await _wordController.UpdateDuplicate(_projId, origWord.Id, nonDup); Assert.That(result, Is.InstanceOf()); } diff --git a/Backend.Tests/Mocks/WordRepositoryMock.cs b/Backend.Tests/Mocks/WordRepositoryMock.cs index fad84be28d..49a6ecaa1b 100644 --- a/Backend.Tests/Mocks/WordRepositoryMock.cs +++ b/Backend.Tests/Mocks/WordRepositoryMock.cs @@ -65,6 +65,11 @@ public Task IsFrontierNonempty(string projectId) return Task.FromResult(_frontier.Find(w => w.ProjectId == projectId) is not null); } + public Task IsInFrontier(string projectId, string wordId) + { + return Task.FromResult(_frontier.Find(w => w.ProjectId == projectId && w.Id == wordId) is not null); + } + public Task> GetFrontier(string projectId) { return Task.FromResult(_frontier.Where(w => w.ProjectId == projectId).Select(w => w.Clone()).ToList()); diff --git a/Backend.Tests/Services/MergeServiceTests.cs b/Backend.Tests/Services/MergeServiceTests.cs index c5373b552f..ab25ee2739 100644 --- a/Backend.Tests/Services/MergeServiceTests.cs +++ b/Backend.Tests/Services/MergeServiceTests.cs @@ -44,7 +44,7 @@ public void MergeWordsOneChildTest() } }; - var newWords = _mergeService.Merge(ProjId, new List { mergeObject }).Result; + var newWords = _mergeService.Merge(ProjId, UserId, new List { mergeObject }).Result; // There should only be 1 word added and it should be identical to what we passed in Assert.That(newWords, Has.Count.EqualTo(1)); @@ -76,7 +76,7 @@ public void MergeWordsDeleteTest() DeleteOnly = true }; - var newWords = _mergeService.Merge(ProjId, new List { mergeObject }).Result; + var newWords = _mergeService.Merge(ProjId, UserId, new List { mergeObject }).Result; // There should be no word added and no words left in the frontier. Assert.That(newWords, Is.Empty); @@ -100,7 +100,7 @@ public void MergeWordsMultiChildTest() Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(numberOfChildren)); var mergeWordsList = new List { mergeWords }; - var newWords = _mergeService.Merge(ProjId, mergeWordsList).Result; + var newWords = _mergeService.Merge(ProjId, UserId, mergeWordsList).Result; // Check for correct history length. var dbParent = newWords.First(); @@ -117,7 +117,7 @@ public void MergeWordsMultipleTest() int wordCount = 100; var randWords = Util.RandomWordList(wordCount, ProjId); var mergeWordsList = randWords.Select(word => new MergeWords { Parent = word }).ToList(); - var newWords = _mergeService.Merge(ProjId, mergeWordsList).Result; + var newWords = _mergeService.Merge(ProjId, UserId, mergeWordsList).Result; Assert.That(newWords, Has.Count.EqualTo(wordCount)); Assert.That(newWords.First().Id, Is.Not.EqualTo(newWords.Last().Id)); @@ -144,7 +144,7 @@ public void UndoMergeOneChildTest() } }; - var newWords = _mergeService.Merge(ProjId, new List { mergeObject }).Result; + var newWords = _mergeService.Merge(ProjId, UserId, new List { mergeObject }).Result; // There should only be 1 word added and it should be identical to what we passed in Assert.That(newWords, Has.Count.EqualTo(1)); @@ -153,7 +153,7 @@ public void UndoMergeOneChildTest() var childIds = mergeObject.Children.Select(word => word.SrcWordId).ToList(); var parentIds = new List { newWords[0].Id }; var mergedWord = new MergeUndoIds(parentIds, childIds); - var undo = _mergeService.UndoMerge(ProjId, mergedWord).Result; + var undo = _mergeService.UndoMerge(ProjId, UserId, mergedWord).Result; Assert.That(undo, Is.True); var frontierWords = _wordRepo.GetFrontier(ProjId).Result; @@ -179,14 +179,14 @@ public void UndoMergeMultiChildTest() Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(numberOfChildren)); var mergeWordsList = new List { mergeWords }; - var newWords = _mergeService.Merge(ProjId, mergeWordsList).Result; + var newWords = _mergeService.Merge(ProjId, UserId, mergeWordsList).Result; Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(1)); var childIds = mergeWords.Children.Select(word => word.SrcWordId).ToList(); var parentIds = new List { newWords[0].Id }; var mergedWord = new MergeUndoIds(parentIds, childIds); - var undo = _mergeService.UndoMerge(ProjId, mergedWord).Result; + var undo = _mergeService.UndoMerge(ProjId, UserId, mergedWord).Result; Assert.That(undo, Is.True); var frontierWords = _wordRepo.GetFrontier(ProjId).Result; diff --git a/Backend.Tests/Services/WordServiceTests.cs b/Backend.Tests/Services/WordServiceTests.cs index 2a01fbfbca..9d1ae31861 100644 --- a/Backend.Tests/Services/WordServiceTests.cs +++ b/Backend.Tests/Services/WordServiceTests.cs @@ -14,6 +14,8 @@ public class WordServiceTests private IWordService _wordService = null!; private const string ProjId = "WordServiceTestProjId"; + private const string UserId = "WordServiceTestUserId"; + private const string WordId = "WordServiceTestWordId"; [SetUp] public void Setup() @@ -22,6 +24,80 @@ public void Setup() _wordService = new WordService(_wordRepo); } + [Test] + public void TestCreateAddsUserId() + { + var word = _wordService.Create(UserId, new Word { EditedBy = new List { "other" } }).Result; + Assert.That(word.EditedBy, Has.Count.EqualTo(2)); + Assert.That(word.EditedBy.Last(), Is.EqualTo(UserId)); + } + + [Test] + public void TestCreateDoesNotAddDuplicateUserId() + { + var word = _wordService.Create(UserId, new Word { EditedBy = new List { UserId } }).Result; + Assert.That(word.EditedBy, Has.Count.EqualTo(1)); + } + + [Test] + public void TestCreateMultipleWords() + { + _ = _wordService.Create( + UserId, new List { new() { ProjectId = ProjId }, new() { ProjectId = ProjId } }).Result; + Assert.That(_wordRepo.GetAllWords(ProjId).Result, Has.Count.EqualTo(2)); + Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(2)); + } + + [Test] + public void TestUpdateNotInFrontierFalse() + { + Assert.That(_wordService.Update(ProjId, UserId, WordId, new Word()).Result, Is.False); + } + + [Test] + public void TestUpdateReplacesFrontierWord() + { + var word = _wordRepo.AddFrontier(new Word { ProjectId = ProjId }).Result; + Assert.That(word, Is.Not.Null); + var oldId = word.Id; + word.Vernacular = "NewVern"; + Assert.That(_wordService.Update(ProjId, UserId, oldId, word).Result, Is.True); + var frontier = _wordRepo.GetFrontier(ProjId).Result; + Assert.That(frontier, Has.Count.EqualTo(1)); + var newWord = frontier.First(); + Assert.That(newWord.Id, Is.Not.EqualTo(oldId)); + Assert.That(newWord.History.Last(), Is.EqualTo(oldId)); + } + + [Test] + public void TestRestoreFrontierWordsMissingWordFalse() + { + var word = _wordRepo.Add(new Word { ProjectId = ProjId }).Result; + Assert.That(_wordService.RestoreFrontierWords( + ProjId, new List { "NotAnId", word.Id }).Result, Is.False); + } + + [Test] + public void TestRestoreFrontierWordsFrontierWordFalse() + { + var wordNoFrontier = _wordRepo.Add(new Word { ProjectId = ProjId }).Result; + var wordYesFrontier = _wordRepo.Create(new Word { ProjectId = ProjId }).Result; + Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(1)); + Assert.That(_wordService.RestoreFrontierWords( + ProjId, new List { wordNoFrontier.Id, wordYesFrontier.Id }).Result, Is.False); + } + + [Test] + public void TestRestoreFrontierWordsTrue() + { + var word1 = _wordRepo.Add(new Word { ProjectId = ProjId }).Result; + var word2 = _wordRepo.Add(new Word { ProjectId = ProjId }).Result; + Assert.That(_wordRepo.GetFrontier(ProjId).Result, Is.Empty); + Assert.That(_wordService.RestoreFrontierWords( + ProjId, new List { word1.Id, word2.Id }).Result, Is.True); + Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(2)); + } + [Test] public void TestFindContainingWordNoFrontier() { diff --git a/Backend/Controllers/AudioController.cs b/Backend/Controllers/AudioController.cs index 9a04ed8325..4ccd93df5e 100644 --- a/Backend/Controllers/AudioController.cs +++ b/Backend/Controllers/AudioController.cs @@ -74,6 +74,7 @@ public async Task UploadAudioFile(string projectId, string wordId { return Forbid(); } + var userId = _permissionService.GetUserId(HttpContext); // sanitize user input try @@ -117,7 +118,7 @@ public async Task UploadAudioFile(string projectId, string wordId word.Audio.Add(Path.GetFileName(fileUpload.FilePath)); // Update the word with new audio file - await _wordService.Update(projectId, wordId, word); + await _wordService.Update(projectId, userId, wordId, word); return Ok(word.Id); } @@ -131,6 +132,7 @@ public async Task DeleteAudioFile(string projectId, string wordId { return Forbid(); } + var userId = _permissionService.GetUserId(HttpContext); // sanitize user input try @@ -144,7 +146,7 @@ public async Task DeleteAudioFile(string projectId, string wordId return new UnsupportedMediaTypeResult(); } - var newWord = await _wordService.Delete(projectId, wordId, fileName); + var newWord = await _wordService.Delete(projectId, userId, wordId, fileName); if (newWord is not null) { return Ok(newWord.Id); diff --git a/Backend/Controllers/MergeController.cs b/Backend/Controllers/MergeController.cs index 09b46c1468..2abd23d0bc 100644 --- a/Backend/Controllers/MergeController.cs +++ b/Backend/Controllers/MergeController.cs @@ -36,10 +36,11 @@ public async Task MergeWords( { return Forbid(); } + var userId = _permissionService.GetUserId(HttpContext); try { - var newWords = await _mergeService.Merge(projectId, mergeWordsList); + var newWords = await _mergeService.Merge(projectId, userId, mergeWordsList); return Ok(newWords.Select(w => w.Id).ToList()); } catch @@ -59,8 +60,9 @@ public async Task UndoMerge(string projectId, [FromBody, BindRequ { return Forbid(); } + var userId = _permissionService.GetUserId(HttpContext); - var undo = await _mergeService.UndoMerge(projectId, merge); + var undo = await _mergeService.UndoMerge(projectId, userId, merge); return Ok(undo); } diff --git a/Backend/Controllers/WordController.cs b/Backend/Controllers/WordController.cs index 388506dc58..0f6db9917f 100644 --- a/Backend/Controllers/WordController.cs +++ b/Backend/Controllers/WordController.cs @@ -28,24 +28,6 @@ public WordController(IWordRepository repo, IWordService wordService, IProjectRe _wordService = wordService; } - /// Deletes all s for specified . - /// true: if success, false: if there were no words - [HttpDelete(Name = "DeleteProjectWords")] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(bool))] - public async Task DeleteProjectWords(string projectId) - { - if (!await _permissionService.HasProjectPermission(HttpContext, Permission.Archive, projectId)) - { - return Forbid(); - } - var proj = await _projRepo.GetProject(projectId); - if (proj is null) - { - return NotFound(projectId); - } - return Ok(await _wordRepo.DeleteAllWords(projectId)); - } - /// Deletes specified Frontier . [HttpDelete("frontier/{wordId}", Name = "DeleteFrontierWord")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))] @@ -60,7 +42,8 @@ public async Task DeleteFrontierWord(string projectId, string wor { return NotFound(projectId); } - var id = await _wordService.DeleteFrontierWord(projectId, wordId); + var userId = _permissionService.GetUserId(HttpContext); + var id = await _wordService.DeleteFrontierWord(projectId, userId, wordId); if (id is null) { return NotFound(wordId); @@ -169,7 +152,7 @@ public async Task GetDuplicateId(string projectId, [FromBody, Bin [HttpPost("{dupId}", Name = "UpdateDuplicate")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))] public async Task UpdateDuplicate( - string projectId, string dupId, string? userId, [FromBody, BindRequired] Word word) + string projectId, string dupId, [FromBody, BindRequired] Word word) { if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId)) { @@ -188,16 +171,13 @@ public async Task UpdateDuplicate( return NotFound(dupId); } - if (string.IsNullOrEmpty(userId)) - { - userId = ""; - } + var userId = _permissionService.GetUserId(HttpContext); if (!duplicatedWord.AppendContainedWordContents(word, userId)) { return Conflict(); } - await _wordService.Update(duplicatedWord.ProjectId, duplicatedWord.Id, duplicatedWord); + await _wordService.Update(duplicatedWord.ProjectId, userId, duplicatedWord.Id, duplicatedWord); return Ok(duplicatedWord.Id); } @@ -218,9 +198,8 @@ public async Task CreateWord(string projectId, [FromBody, BindReq return NotFound(projectId); } word.ProjectId = projectId; - - await _wordRepo.Create(word); - return Ok(word.Id); + var userId = _permissionService.GetUserId(HttpContext); + return Ok((await _wordService.Create(userId, word)).Id); } /// Updates a . @@ -247,7 +226,8 @@ public async Task UpdateWord( // Add the found id to the updated word. word.Id = document.Id; - await _wordService.Update(projectId, wordId, word); + var userId = _permissionService.GetUserId(HttpContext); + await _wordService.Update(projectId, userId, wordId, word); return Ok(word.Id); } } diff --git a/Backend/Interfaces/IMergeService.cs b/Backend/Interfaces/IMergeService.cs index 40beae11b5..223a6a91f0 100644 --- a/Backend/Interfaces/IMergeService.cs +++ b/Backend/Interfaces/IMergeService.cs @@ -6,8 +6,8 @@ namespace BackendFramework.Interfaces { public interface IMergeService { - Task> Merge(string projectId, List mergeWordsList); - Task UndoMerge(string projectId, MergeUndoIds ids); + Task> Merge(string projectId, string userId, List mergeWordsList); + Task UndoMerge(string projectId, string userId, MergeUndoIds ids); Task AddToMergeBlacklist(string projectId, string userId, List wordIds); Task AddToMergeGraylist(string projectId, string userId, List wordIds); Task RemoveFromMergeGraylist(string projectId, string userId, List wordIds); diff --git a/Backend/Interfaces/IWordRepository.cs b/Backend/Interfaces/IWordRepository.cs index 1f2db182f7..342970e443 100644 --- a/Backend/Interfaces/IWordRepository.cs +++ b/Backend/Interfaces/IWordRepository.cs @@ -13,6 +13,7 @@ public interface IWordRepository Task Add(Word word); Task DeleteAllWords(string projectId); Task IsFrontierNonempty(string projectId); + Task IsInFrontier(string projectId, string wordId); Task> GetFrontier(string projectId); Task AddFrontier(Word word); Task> AddFrontier(List words); diff --git a/Backend/Interfaces/IWordService.cs b/Backend/Interfaces/IWordService.cs index 7df1234dd9..e8b9ddd557 100644 --- a/Backend/Interfaces/IWordService.cs +++ b/Backend/Interfaces/IWordService.cs @@ -1,14 +1,18 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using BackendFramework.Models; namespace BackendFramework.Interfaces { public interface IWordService { - Task Update(string projectId, string wordId, Word word); - Task Delete(string projectId, string wordId); + Task Create(string userId, Word word); + Task> Create(string userId, List words); + Task Update(string projectId, string userId, string wordId, Word word); + Task Delete(string projectId, string userId, string wordId); + Task Delete(string projectId, string userId, string wordId, string fileName); + Task DeleteFrontierWord(string projectId, string userId, string wordId); + Task RestoreFrontierWords(string projectId, List wordIds); Task FindContainingWord(Word word); - Task Delete(string projectId, string wordId, string fileName); - Task DeleteFrontierWord(string projectId, string wordId); } } diff --git a/Backend/Repositories/WordRepository.cs b/Backend/Repositories/WordRepository.cs index 0423ff8a5d..95259027e4 100644 --- a/Backend/Repositories/WordRepository.cs +++ b/Backend/Repositories/WordRepository.cs @@ -131,6 +131,14 @@ public async Task IsFrontierNonempty(string projectId) return word is not null; } + /// Checks if specified word is in Frontier for specified + public async Task IsInFrontier(string projectId, string wordId) + { + var word = await _wordDatabase.Frontier + .Find(w => w.ProjectId == projectId && w.Id == wordId).FirstOrDefaultAsync(); + return word is not null; + } + /// Finds all s in the Frontier for specified public async Task> GetFrontier(string projectId) { @@ -196,22 +204,5 @@ public async Task UpdateFrontier(Word word) } return deleted; } - - /// Updates in the Words collection with same wordId and projectId - /// A bool: success of operation - private async Task UpdateWord(Word word) - { - var filterDef = new FilterDefinitionBuilder(); - var filter = filterDef.And( - filterDef.Eq(x => x.ProjectId, word.ProjectId), - filterDef.Eq(x => x.Id, word.Id)); - - var deleted = (await _wordDatabase.Words.DeleteOneAsync(filter)).DeletedCount > 0; - if (deleted) - { - await Add(word); - } - return deleted; - } } } diff --git a/Backend/Services/MergeService.cs b/Backend/Services/MergeService.cs index 01be6ef365..c045745a71 100644 --- a/Backend/Services/MergeService.cs +++ b/Backend/Services/MergeService.cs @@ -52,11 +52,6 @@ private async Task MergePrepParent(string projectId, MergeWords mergeWords // Remove duplicates. parent.Audio = parent.Audio.Distinct().ToList(); parent.History = parent.History.Distinct().ToList(); - - // Clear fields to be automatically regenerated. - parent.Id = ""; - parent.Modified = ""; - return parent; } @@ -73,17 +68,17 @@ private async Task MergeDeleteChildren(string projectId, MergeWords mergeW /// from the frontier, and adds the new words to the database. /// /// List of new words added. - public async Task> Merge(string projectId, List mergeWordsList) + public async Task> Merge(string projectId, string userId, List mergeWordsList) { var keptWords = mergeWordsList.Where(m => !m.DeleteOnly); var newWords = keptWords.Select(m => MergePrepParent(projectId, m).Result).ToList(); await Task.WhenAll(mergeWordsList.Select(m => MergeDeleteChildren(projectId, m))); - return await _wordRepo.Create(newWords); + return await _wordService.Create(userId, newWords); } /// Undo merge /// True if merge was successfully undone - public async Task UndoMerge(string projectId, MergeUndoIds ids) + public async Task UndoMerge(string projectId, string userId, MergeUndoIds ids) { foreach (var parentId in ids.ParentIds) { @@ -94,25 +89,15 @@ public async Task UndoMerge(string projectId, MergeUndoIds ids) } } - var childWords = new List(); - foreach (var childId in ids.ChildIds) + // If children are not restorable, return without any undo. + if (!await _wordService.RestoreFrontierWords(projectId, ids.ChildIds)) { - var childWord = await _wordRepo.GetWord(projectId, childId); - if (childWord is null) - { - return false; - } - childWords.Add(childWord); - } - - - // Separate foreach loop for deletion to prevent partial undos + return false; + }; foreach (var parentId in ids.ParentIds) { - await _wordService.DeleteFrontierWord(projectId, parentId); + await _wordService.DeleteFrontierWord(projectId, userId, parentId); } - - await _wordRepo.AddFrontier(childWords); return true; } diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index d089a59c6e..15ce094eb3 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using BackendFramework.Interfaces; using BackendFramework.Models; @@ -15,9 +16,45 @@ public WordService(IWordRepository wordRepo) _wordRepo = wordRepo; } + /// + /// Clear the given word's Id and Metadata to be generated by the word repo, + /// and add the given userId to EditedBy if it's not already last on the list. + /// + private static Word PrepEditedData(string userId, Word word) + { + word.Id = ""; + word.Modified = ""; + if (!string.IsNullOrWhiteSpace(userId) && userId != word.EditedBy.LastOrDefault("")) + { + word.EditedBy.Add(userId); + } + return word; + } + + /// Creates a new word with updated edited data. + /// The created word + public async Task Create(string userId, Word word) + { + return await _wordRepo.Create(PrepEditedData(userId, word)); + } + + /// Creates new words with updated edited data. + /// The created word + public async Task> Create(string userId, List words) + { + return await _wordRepo.Create(words.Select(w => PrepEditedData(userId, w)).ToList()); + } + + /// Adds a new word with updated edited data. + /// The added word + private async Task Add(string userId, Word word) + { + return await _wordRepo.Add(PrepEditedData(userId, word)); + } + /// Makes a new word in Frontier that has deleted tag on each sense /// A bool: success of operation - public async Task Delete(string projectId, string wordId) + public async Task Delete(string projectId, string userId, string wordId) { var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); @@ -33,8 +70,7 @@ public async Task Delete(string projectId, string wordId) return false; } - wordToDelete.Id = ""; - wordToDelete.Modified = ""; + wordToDelete.EditedBy = new List(); wordToDelete.History = new List { wordId }; wordToDelete.Accessibility = Status.Deleted; @@ -43,14 +79,14 @@ public async Task Delete(string projectId, string wordId) senseAcc.Accessibility = Status.Deleted; } - await _wordRepo.Create(wordToDelete); + await Create(userId, wordToDelete); return wordIsInFrontier; } - /// Removes audio with specified Id from a word + /// Removes audio with specified fileName from a word /// New word - public async Task Delete(string projectId, string wordId, string fileName) + public async Task Delete(string projectId, string userId, string wordId, string fileName) { var wordWithAudioToDelete = await _wordRepo.GetWord(projectId, wordId); if (wordWithAudioToDelete is null) @@ -67,21 +103,14 @@ public async Task Delete(string projectId, string wordId) } wordWithAudioToDelete.Audio.Remove(fileName); - wordWithAudioToDelete.Id = ""; - wordWithAudioToDelete.Modified = ""; - wordWithAudioToDelete.ProjectId = projectId; - - // Keep track of the old word, adding it to the history. wordWithAudioToDelete.History.Add(wordId); - wordWithAudioToDelete = await _wordRepo.Create(wordWithAudioToDelete); - - return wordWithAudioToDelete; + return await Create(userId, wordWithAudioToDelete); } /// Deletes word in frontier collection and adds word with deleted tag in word collection /// A string: id of new word - public async Task DeleteFrontierWord(string projectId, string wordId) + public async Task DeleteFrontierWord(string projectId, string userId, string wordId) { var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); if (!wordIsInFrontier) @@ -95,21 +124,34 @@ public async Task Delete(string projectId, string wordId) return null; } - word.Id = ""; - word.Modified = ""; word.ProjectId = projectId; word.Accessibility = Status.Deleted; - - // Keep track of the old word, adding it to the history. word.History.Add(wordId); - var deletedWord = await _wordRepo.Add(word); - return deletedWord.Id; + return (await Add(userId, word)).Id; + } + + /// Restores words to the Frontier + /// A bool: true if successful, false if any don't exist or are already in the Frontier. + public async Task RestoreFrontierWords(string projectId, List wordIds) + { + var words = new List(); + foreach (var id in wordIds) + { + var word = await _wordRepo.GetWord(projectId, id); + if (word is null || await _wordRepo.IsInFrontier(projectId, id)) + { + return false; + } + words.Add(word); + } + await _wordRepo.AddFrontier(words); + return true; } /// Makes a new word in the Frontier with changes made /// A bool: success of operation - public async Task Update(string projectId, string wordId, Word word) + public async Task Update(string projectId, string userId, string wordId, Word word) { var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); @@ -119,14 +161,10 @@ public async Task Update(string projectId, string wordId, Word word) return wordIsInFrontier; } - word.Id = ""; word.ProjectId = projectId; - word.Modified = ""; - - // Keep track of the old word, adding it to the history. word.History.Add(wordId); - await _wordRepo.Create(word); + await Create(userId, word); return wordIsInFrontier; } diff --git a/src/api/api/word-api.ts b/src/api/api/word-api.ts index cf0b8e3a41..ce1bbdedec 100644 --- a/src/api/api/word-api.ts +++ b/src/api/api/word-api.ts @@ -150,51 +150,6 @@ export const WordApiAxiosParamCreator = function ( options: localVarRequestOptions, }; }, - /** - * - * @param {string} projectId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - deleteProjectWords: async ( - projectId: string, - options: any = {} - ): Promise => { - // verify required parameter 'projectId' is not null or undefined - assertParamExists("deleteProjectWords", "projectId", projectId); - const localVarPath = `/v1/projects/{projectId}/words`.replace( - `{${"projectId"}}`, - encodeURIComponent(String(projectId)) - ); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { - method: "DELETE", - ...baseOptions, - ...options, - }; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); - let headersFromBaseOptions = - baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = { - ...localVarHeaderParameter, - ...headersFromBaseOptions, - ...options.headers, - }; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {string} projectId @@ -441,7 +396,6 @@ export const WordApiAxiosParamCreator = function ( * @param {string} projectId * @param {string} dupId * @param {Word} word - * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -449,7 +403,6 @@ export const WordApiAxiosParamCreator = function ( projectId: string, dupId: string, word: Word, - userId?: string, options: any = {} ): Promise => { // verify required parameter 'projectId' is not null or undefined @@ -476,10 +429,6 @@ export const WordApiAxiosParamCreator = function ( const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; - if (userId !== undefined) { - localVarQueryParameter["userId"] = userId; - } - localVarHeaderParameter["Content-Type"] = "application/json"; setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); @@ -623,27 +572,6 @@ export const WordApiFp = function (configuration?: Configuration) { configuration ); }, - /** - * - * @param {string} projectId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async deleteProjectWords( - projectId: string, - options?: any - ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise - > { - const localVarAxiosArgs = - await localVarAxiosParamCreator.deleteProjectWords(projectId, options); - return createRequestFunction( - localVarAxiosArgs, - globalAxios, - BASE_PATH, - configuration - ); - }, /** * * @param {string} projectId @@ -769,7 +697,6 @@ export const WordApiFp = function (configuration?: Configuration) { * @param {string} projectId * @param {string} dupId * @param {Word} word - * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -777,7 +704,6 @@ export const WordApiFp = function (configuration?: Configuration) { projectId: string, dupId: string, word: Word, - userId?: string, options?: any ): Promise< (axios?: AxiosInstance, basePath?: string) => AxiosPromise @@ -786,7 +712,6 @@ export const WordApiFp = function (configuration?: Configuration) { projectId, dupId, word, - userId, options ); return createRequestFunction( @@ -871,20 +796,6 @@ export const WordApiFactory = function ( .deleteFrontierWord(projectId, wordId, options) .then((request) => request(axios, basePath)); }, - /** - * - * @param {string} projectId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - deleteProjectWords( - projectId: string, - options?: any - ): AxiosPromise { - return localVarFp - .deleteProjectWords(projectId, options) - .then((request) => request(axios, basePath)); - }, /** * * @param {string} projectId @@ -964,7 +875,6 @@ export const WordApiFactory = function ( * @param {string} projectId * @param {string} dupId * @param {Word} word - * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -972,11 +882,10 @@ export const WordApiFactory = function ( projectId: string, dupId: string, word: Word, - userId?: string, options?: any ): AxiosPromise { return localVarFp - .updateDuplicate(projectId, dupId, word, userId, options) + .updateDuplicate(projectId, dupId, word, options) .then((request) => request(axios, basePath)); }, /** @@ -1042,20 +951,6 @@ export interface WordApiDeleteFrontierWordRequest { readonly wordId: string; } -/** - * Request parameters for deleteProjectWords operation in WordApi. - * @export - * @interface WordApiDeleteProjectWordsRequest - */ -export interface WordApiDeleteProjectWordsRequest { - /** - * - * @type {string} - * @memberof WordApiDeleteProjectWords - */ - readonly projectId: string; -} - /** * Request parameters for getDuplicateId operation in WordApi. * @export @@ -1166,13 +1061,6 @@ export interface WordApiUpdateDuplicateRequest { * @memberof WordApiUpdateDuplicate */ readonly word: Word; - - /** - * - * @type {string} - * @memberof WordApiUpdateDuplicate - */ - readonly userId?: string; } /** @@ -1246,22 +1134,6 @@ export class WordApi extends BaseAPI { .then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {WordApiDeleteProjectWordsRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof WordApi - */ - public deleteProjectWords( - requestParameters: WordApiDeleteProjectWordsRequest, - options?: any - ) { - return WordApiFp(this.configuration) - .deleteProjectWords(requestParameters.projectId, options) - .then((request) => request(this.axios, this.basePath)); - } - /** * * @param {WordApiGetDuplicateIdRequest} requestParameters Request parameters. @@ -1359,7 +1231,6 @@ export class WordApi extends BaseAPI { requestParameters.projectId, requestParameters.dupId, requestParameters.word, - requestParameters.userId, options ) .then((request) => request(this.axios, this.basePath)); diff --git a/src/backend/index.ts b/src/backend/index.ts index c3266860a5..0ea96fb74a 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -709,15 +709,9 @@ export async function isFrontierNonempty(projectId?: string): Promise { export async function updateDuplicate( dupId: string, - userId: string, word: Word ): Promise { - const params = { - projectId: LocalStorage.getProjectId(), - dupId, - userId, - word, - }; + const params = { projectId: LocalStorage.getProjectId(), dupId, word }; const resp = await wordApi.updateDuplicate(params, defaultOptions()); return await getWord(resp.data); } diff --git a/src/components/DataEntry/DataEntryTable/index.tsx b/src/components/DataEntry/DataEntryTable/index.tsx index bfd161daff..5ebc1bc007 100644 --- a/src/components/DataEntry/DataEntryTable/index.tsx +++ b/src/components/DataEntry/DataEntryTable/index.tsx @@ -597,7 +597,7 @@ export default function DataEntryTable( state.recentWords.findIndex((w) => w.word.id === oldId) > -1; defunctWord(oldId); - const newWord = await backend.updateDuplicate(oldId, getUserId(), word); + const newWord = await backend.updateDuplicate(oldId, word); defunctWord(oldId, newWord.id); const newId = await addAudiosToBackend(newWord.id, audioURLs);