Skip to content

Commit

Permalink
Merge branch 'master' into login-function-component
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Nov 14, 2023
2 parents 557e04d + 51f9db2 commit a69a7b1
Show file tree
Hide file tree
Showing 66 changed files with 2,591 additions and 1,891 deletions.
5 changes: 3 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
**

# Except the following.
!docs/user_guide
!nginx
!public
!src
!docs/user_guide
!.env
!.eslintrc.cjs
!dev-requirements.txt
!package*.json
!tsconfig.json
!tox.ini
!tsconfig.json

# Ignore user guide build directory.
docs/user_guide/site
69 changes: 69 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module.exports = {
env: {
browser: true,
jest: true,
},
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:import/recommended",
],
ignorePatterns: ["*.dic.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: { jsx: true },
project: "./tsconfig.json",
},
plugins: ["@typescript-eslint", "react", "unused-imports"],
root: true,
rules: {
"import/first": "warn",
"import/newline-after-import": "warn",
"import/no-duplicates": "warn",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"import/order": [
"warn",
{
alphabetize: { order: "asc" },
groups: [
"builtin",
"external",
["internal", "parent", "sibling", "index", "object", "type"],
],
"newlines-between": "always",
},
],
"no-undef": "off",
"prefer-const": "warn",
"react/jsx-boolean-value": "warn",
"unused-imports/no-unused-imports": "warn",
},
settings: {
react: { version: "detect" },
"import/resolver": {
typescript: { alwaysTryTypes: true },
},
},
overrides: [
{
files: ["*.ts", "*.tsx"],
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
],
rules: {
"@typescript-eslint/explicit-function-return-type": [
"warn",
{ allowExpressions: true },
],
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/switch-exhaustiveness-check": "warn",
},
},
],
};
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"thecombine",
"upsert",
"venv",
"verns",
"wordlist",
"wordlists"
],
Expand Down
117 changes: 117 additions & 0 deletions Backend.Tests/Controllers/LiftControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,38 @@ private static async Task<string> DownloadAndReadLift(LiftController liftControl
return liftText;
}

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

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

[Test]
public void TestUploadLiftFileAlreadyImported()
{
var projId = _projRepo.Create(new Project { Name = "already has import", LiftImported = true }).Result!.Id;
var result = _liftController.UploadLiftFile(projId, new FileUpload()).Result;
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
Assert.That(((BadRequestObjectResult)result).Value, Contains.Substring("LIFT"));
}

[Test]
public void TestUploadLiftFileBadFile()
{
var result = _liftController.UploadLiftFile(_projId, new FileUpload()).Result;
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
Assert.That(((BadRequestObjectResult)result).Value, Is.InstanceOf<string>());
}

[Test]
public void TestUploadLiftFileAndGetWritingSystems()
{
Expand Down Expand Up @@ -271,6 +303,21 @@ public void TestFinishUploadLiftFileNothingToFinish()
Assert.That(_liftService.RetrieveImport(UserId), Is.Null);
}

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

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

[Test]
public async Task TestModifiedTimeExportsToLift()
{
Expand All @@ -285,6 +332,35 @@ public async Task TestModifiedTimeExportsToLift()
Assert.That(liftContents, Does.Contain("dateModified=\"2000-01-01T00:00:00Z\""));
}

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

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

[Test]
public void TestExportLiftFileNoProject()
{
var result = _liftController.ExportLiftFile("non-existent-project").Result;
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>());
}

[Test]
public void TestExportLiftFileNoWordsInProject()
{
var result = _liftController.ExportLiftFile(_projId).Result;
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}

[Test]
public void TestExportInvalidProjectId()
{
Expand All @@ -294,6 +370,47 @@ public void TestExportInvalidProjectId()
Throws.TypeOf<MissingProjectException>());
}

[Test]
public void TestDownloadLiftFileNoPermission()
{
_liftController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();
var result = _liftController.DownloadLiftFile(_projId).Result;
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
3 changes: 2 additions & 1 deletion Backend.Tests/Mocks/ProjectRepositoryMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ public Task<ResultOfUpdate> Update(string projectId, Project project)

public Task<bool> CanImportLift(string projectId)
{
return Task.FromResult(true);
var project = _projects.Find(p => p.Id == projectId);
return Task.FromResult(project?.LiftImported != true);
}
}
}
79 changes: 36 additions & 43 deletions Backend/Repositories/WordRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,43 @@ public WordRepository(IWordContext collectionSettings)
_wordDatabase = collectionSettings;
}

/// <summary> Finds all <see cref="Word"/>s with specified projectId </summary>
/// <summary>
/// Creates a mongo filter for all words in a specified project (and optionally with specified vernacular).
/// Since a variant in FLEx can export as an entry without any senses, filters out 0-sense words.
/// </summary>
private static FilterDefinition<Word> GetAllProjectWordsFilter(string projectId, string? vernacular = null)
{
var filterDef = new FilterDefinitionBuilder<Word>();
return (vernacular is null)
? filterDef.And(filterDef.Eq(w => w.ProjectId, projectId), filterDef.SizeGt(w => w.Senses, 0))
: filterDef.And(filterDef.Eq(w => w.ProjectId, projectId), filterDef.SizeGt(w => w.Senses, 0),
filterDef.Eq(w => w.Vernacular, vernacular));
}

/// <summary> Creates a mongo filter for words in a specified project with specified wordId. </summary>
private static FilterDefinition<Word> GetProjectWordFilter(string projectId, string wordId)
{
var filterDef = new FilterDefinitionBuilder<Word>();
return filterDef.And(filterDef.Eq(w => w.ProjectId, projectId), filterDef.Eq(w => w.Id, wordId));
}

/// <summary> Creates a mongo filter for words in a specified project with specified wordIds. </summary>
private static FilterDefinition<Word> GetProjectWordsFilter(string projectId, List<string> wordIds)
{
var filterDef = new FilterDefinitionBuilder<Word>();
return filterDef.And(filterDef.Eq(w => w.ProjectId, projectId), filterDef.In(w => w.Id, wordIds));
}

/// <summary> Finds all <see cref="Word"/>s with specified projectId </summary>
public async Task<List<Word>> GetAllWords(string projectId)
{
return await _wordDatabase.Words.Find(w => w.ProjectId == projectId).ToListAsync();
return await _wordDatabase.Words.Find(GetAllProjectWordsFilter(projectId)).ToListAsync();
}

/// <summary> Finds <see cref="Word"/> with specified wordId and projectId </summary>
public async Task<Word?> GetWord(string projectId, string wordId)
{
var filterDef = new FilterDefinitionBuilder<Word>();
var filter = filterDef.And(
filterDef.Eq(x => x.ProjectId, projectId),
filterDef.Eq(x => x.Id, wordId));

var wordList = await _wordDatabase.Words.FindAsync(filter);
var wordList = await _wordDatabase.Words.FindAsync(GetProjectWordFilter(projectId, wordId));
try
{
return await wordList.FirstAsync();
Expand Down Expand Up @@ -127,29 +149,26 @@ public async Task<Word> Add(Word word)
/// <summary> Checks if Frontier is nonempty for specified <see cref="Project"/> </summary>
public async Task<bool> IsFrontierNonempty(string projectId)
{
var word = await _wordDatabase.Frontier.Find(w => w.ProjectId == projectId).FirstOrDefaultAsync();
var word = await _wordDatabase.Frontier.Find(GetAllProjectWordsFilter(projectId)).FirstOrDefaultAsync();
return word is not null;
}

/// <summary> Checks if specified word is in Frontier for specified <see cref="Project"/> </summary>
public async Task<bool> IsInFrontier(string projectId, string wordId)
{
var word = await _wordDatabase.Frontier
.Find(w => w.ProjectId == projectId && w.Id == wordId).FirstOrDefaultAsync();
return word is not null;
return (await _wordDatabase.Frontier.CountDocumentsAsync(GetProjectWordFilter(projectId, wordId))) > 0;
}

/// <summary> Finds all <see cref="Word"/>s in the Frontier for specified <see cref="Project"/> </summary>
public async Task<List<Word>> GetFrontier(string projectId)
{
return await _wordDatabase.Frontier.Find(w => w.ProjectId == projectId).ToListAsync();
return await _wordDatabase.Frontier.Find(GetAllProjectWordsFilter(projectId)).ToListAsync();
}

/// <summary> Finds all <see cref="Word"/>s in Frontier of specified project with specified vern </summary>
public async Task<List<Word>> GetFrontierWithVernacular(string projectId, string vernacular)
{
return await _wordDatabase.Frontier.Find(
w => w.ProjectId == projectId && w.Vernacular == vernacular).ToListAsync();
return await _wordDatabase.Frontier.Find(GetAllProjectWordsFilter(projectId, vernacular)).ToListAsync();
}

/// <summary> Adds a <see cref="Word"/> only to the Frontier </summary>
Expand All @@ -174,42 +193,16 @@ public async Task<List<Word>> AddFrontier(List<Word> words)
/// <returns> A bool: success of operation </returns>
public async Task<bool> DeleteFrontier(string projectId, string wordId)
{
var filterDef = new FilterDefinitionBuilder<Word>();
var filter = filterDef.And(
filterDef.Eq(x => x.ProjectId, projectId),
filterDef.Eq(x => x.Id, wordId));

var deleted = await _wordDatabase.Frontier.DeleteOneAsync(filter);
var deleted = await _wordDatabase.Frontier.DeleteOneAsync(GetProjectWordFilter(projectId, wordId));
return deleted.DeletedCount > 0;
}

/// <summary> Removes <see cref="Word"/>s from the Frontier with specified wordIds and projectId </summary>
/// <returns> Number of words deleted </returns>
public async Task<long> DeleteFrontier(string projectId, List<string> wordIds)
{
var filterDef = new FilterDefinitionBuilder<Word>();
var filter = filterDef.And(
filterDef.Eq(x => x.ProjectId, projectId),
filterDef.In(x => x.Id, wordIds));
var deleted = await _wordDatabase.Frontier.DeleteManyAsync(filter);
var deleted = await _wordDatabase.Frontier.DeleteManyAsync(GetProjectWordsFilter(projectId, wordIds));
return deleted.DeletedCount;
}

/// <summary> Updates <see cref="Word"/> in the Frontier collection with same wordId and projectId </summary>
/// <returns> A bool: success of operation </returns>
public async Task<bool> UpdateFrontier(Word word)
{
var filterDef = new FilterDefinitionBuilder<Word>();
var filter = filterDef.And(
filterDef.Eq(x => x.ProjectId, word.ProjectId),
filterDef.Eq(x => x.Id, word.Id));

var deleted = (await _wordDatabase.Frontier.DeleteOneAsync(filter)).DeletedCount > 0;
if (deleted)
{
await AddFrontier(word);
}
return deleted;
}
}
}
Loading

0 comments on commit a69a7b1

Please sign in to comment.