Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 15 additions & 33 deletions Backend.Tests/Controllers/LiftControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,21 @@ public void TestUploadLiftFileAndGetWritingSystems()
_liftService.DeleteImport(UserId);
}

[Test]
public void TestDeleteFrontierAndFinishUploadLiftFileNoPermission()
{
_liftController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();
var result = _liftController.DeleteFrontierAndFinishUploadLiftFile(_projId).Result;
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public void TestDeleteFrontierAndFinishUploadLiftFileInvalidProjectId()
{
var result = _liftController.DeleteFrontierAndFinishUploadLiftFile("../hack").Result;
Assert.That(result, Is.InstanceOf<UnsupportedMediaTypeResult>());
}

[Test]
public void TestFinishUploadLiftFileNothingToFinish()
{
Expand Down Expand Up @@ -377,39 +392,6 @@ public void TestDownloadLiftFileNoPermission()
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public void TestCanUploadLiftNoPermission()
{
_liftController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();
var result = _liftController.CanUploadLift(_projId).Result;
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public void TestCanUploadLiftInvalidProjectId()
{
var result = _liftController.CanUploadLift("../hack").Result;
Assert.That(result, Is.InstanceOf<UnsupportedMediaTypeResult>());
}

[Test]
public void TestCanUploadLiftFalse()
{
var projId = _projRepo.Create(new Project { Name = "has import", LiftImported = true }).Result!.Id;
var result = _liftController.CanUploadLift(projId).Result;
Assert.That(result, Is.InstanceOf<OkObjectResult>());
Assert.That(((OkObjectResult)result).Value, Is.False);
}

[Test]
public void TestCanUploadLiftTrue()
{
var projId = _projRepo.Create(new Project { Name = "has no import", LiftImported = false }).Result!.Id;
var result = _liftController.CanUploadLift(projId).Result;
Assert.That(result, Is.InstanceOf<OkObjectResult>());
Assert.That(((OkObjectResult)result).Value, Is.True);
}

/// <summary>
/// Create three words and delete one. Ensure that the deleted word is still exported to LIFT format and marked
/// as deleted.
Expand Down
6 changes: 6 additions & 0 deletions Backend.Tests/Mocks/WordRepositoryMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public Task<bool> DeleteAllWords(string projectId)
return Task.FromResult(true);
}

public Task<bool> DeleteAllFrontierWords(string projectId)
{
_frontier.RemoveAll(word => word.ProjectId == projectId);
return Task.FromResult(true);
}

public Task<bool> HasWords(string projectId)
{
return Task.FromResult(_words.Any(w => w.ProjectId == projectId));
Expand Down
67 changes: 39 additions & 28 deletions Backend/Controllers/LiftController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,37 @@ internal async Task<IActionResult> UploadLiftFileAndGetWritingSystems(IFormFile?
return Ok(Language.GetWritingSystems(extractedLiftRootPath));
}

/// <summary> Replace all words with data from a directory containing a .lift file </summary>
/// <returns> Number of words added </returns>
[HttpPost("deletefrontierandfinishupload", Name = "DeleteFrontierAndFinishUploadLiftFile")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(int))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(string))]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(string))]
[ProducesResponseType(StatusCodes.Status415UnsupportedMediaType)]
public async Task<IActionResult> DeleteFrontierAndFinishUploadLiftFile(string projectId)
{
if (!await _permissionService.HasProjectPermission(HttpContext, Permission.Import, projectId))
{
return Forbid();
}
var userId = _permissionService.GetUserId(HttpContext);

// Sanitize projectId
try
{
projectId = Sanitization.SanitizeId(projectId);
}
catch
{
return new UnsupportedMediaTypeResult();
}

// Delete all frontier words and load the LIFT data
await _wordRepo.DeleteAllFrontierWords(projectId);
return await FinishUploadLiftFile(projectId, userId, true);
}

/// <summary> Adds data from a directory containing a .lift file </summary>
/// <returns> Number of words added </returns>
[HttpPost("finishupload", Name = "FinishUploadLiftFile")]
Expand All @@ -100,7 +131,7 @@ public async Task<IActionResult> FinishUploadLiftFile(string projectId)
return await FinishUploadLiftFile(projectId, userId);
}

internal async Task<IActionResult> FinishUploadLiftFile(string projectId, string userId)
internal async Task<IActionResult> FinishUploadLiftFile(string projectId, string userId, bool allowReupload = false)
{
// Sanitize projectId
try
Expand All @@ -113,7 +144,7 @@ internal async Task<IActionResult> FinishUploadLiftFile(string projectId, string
}

// Ensure LIFT file has not already been imported.
if (!await _projRepo.CanImportLift(projectId))
if (!allowReupload && !await _projRepo.CanImportLift(projectId))
{
return BadRequest("A LIFT file has already been uploaded into this project.");
}
Expand Down Expand Up @@ -254,6 +285,12 @@ private async Task<IActionResult> AddImportToProject(string liftStoragePath, str
return BadRequest("Error processing the LIFT data. Contact support for help.");
}

// Don't update project if no words were imported in the project's vernacular language.
if (countWordsImported == 0)
{
return Ok(0);
}

var project = await _projRepo.GetProject(projectId);
if (project is null)
{
Expand Down Expand Up @@ -446,31 +483,5 @@ internal IActionResult DeleteLiftFile(string userId)
_liftService.DeleteExport(userId);
return Ok(userId);
}

/// <summary> Check if LIFT import has already happened for this project </summary>
/// <returns> A bool </returns>
[HttpGet("check", Name = "CanUploadLift")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(bool))]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status415UnsupportedMediaType)]
public async Task<IActionResult> CanUploadLift(string projectId)
{
if (!await _permissionService.HasProjectPermission(HttpContext, Permission.Import, projectId))
{
return Forbid();
}

// Sanitize user input
try
{
projectId = Sanitization.SanitizeId(projectId);
}
catch
{
return new UnsupportedMediaTypeResult();
}

return Ok(await _projRepo.CanImportLift(projectId));
}
}
}
1 change: 1 addition & 0 deletions Backend/Interfaces/IWordRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public interface IWordRepository
Task<List<Word>> Create(List<Word> words);
Task<Word> Add(Word word);
Task<bool> DeleteAllWords(string projectId);
Task<bool> DeleteAllFrontierWords(string projectId);
Task<bool> HasWords(string projectId);
Task<bool> HasFrontierWords(string projectId);
Task<bool> IsInFrontier(string projectId, string wordId);
Expand Down
13 changes: 13 additions & 0 deletions Backend/Repositories/WordRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ public async Task<bool> DeleteAllWords(string projectId)
return deleted.DeletedCount != 0;
}

/// <summary> Removes all <see cref="Word"/>s from the Frontier for specified <see cref="Project"/> </summary>
/// <returns> A bool: success of operation </returns>
public async Task<bool> DeleteAllFrontierWords(string projectId)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting all words from Frontier");

var filterDef = new FilterDefinitionBuilder<Word>();
var filter = filterDef.Eq(x => x.ProjectId, projectId);

var deleted = await _frontier.DeleteManyAsync(filter);
return deleted.DeletedCount != 0;
}

/// <summary>
/// If the <see cref="Word"/> Created or Modified times are blank, fill them in the current time.
/// </summary>
Expand Down
11 changes: 9 additions & 2 deletions docs/user_guide/docs/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,16 @@ used for the file names).

Currently, the maximum size of LIFT files supported for import is 100MB.

!!! note "Note"
When you import a LIFT file into The Combine, it will import every entry with lexeme form or citation form that matches
the project's vernacular language.

The first time you import into a project, the imported words will be added alongside any words collected in The Combine.
No automatic deduplication, merging, or syncing will be performed.

Currently, only one LIFT file can be imported per project.
If you do a second import, all words in The Combine will be automatically deleted before the new words are imported. Do
not do a second import unless you have already exported your project and imported it into FieldWorks. Then, if you want
to do more word collection in The Combine, you can export from FieldWorks and import into the Combine. The previous
words will be deleted to allow for a clean start with the up-to-date data from FieldWorks.

#### Export {#export}

Expand Down
2 changes: 2 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@
"body": "Imported data will be added to this project. The Combine will make no attempt to deduplicate, overwrite, or sync.",
"chooseFile": "Choose File",
"notAllowed": "You have already imported a LIFT file to this project.",
"reuploadWarning": "You may upload a new LIFT file, but it will overwrite all words in this project.",
"reuploadConfirm": "All words in this project will be deleted and replaced with words from the new LIFT file. Are you sure you want to proceed?",
"wordsUploaded": "Words uploaded: {{ val }}",
"noWordsUploaded": "The LIFT file has no words in this project's vernacular language.",
"liftLanguageMismatch": "The languages in this LIFT file ({{ val1 }}) do not include the project's vernacular language ({{ val2 }}). Likely no words will be imported. Are you sure you want to proceed?"
Expand Down
Loading
Loading