Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,4 @@ dev_*.yaml

# JetBrains Rider
.idea

32 changes: 32 additions & 0 deletions Backend.Tests/Controllers/StatisticsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,37 @@ public async Task TestGetSemanticDomainUserCounts()
var result = await _statsController.GetSemanticDomainUserCounts(_projId);
Assert.That(result, Is.InstanceOf<OkObjectResult>());
}

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

var result = await _statsController.GetDomainWordCount(_projId, "1");
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public async Task TestGetDomainWordCount()
{
var result = await _statsController.GetDomainWordCount(_projId, "1");
Assert.That(result, Is.InstanceOf<OkObjectResult>());
}

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

var result = await _statsController.GetDomainProgressProportion(_projId, "1");
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public async Task TestGetDomainProgressProportion()
{
var result = await _statsController.GetDomainProgressProportion(_projId, "1");
Assert.That(result, Is.InstanceOf<OkObjectResult>());
}
}
}
8 changes: 8 additions & 0 deletions Backend.Tests/Mocks/StatisticsServiceMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,13 @@ public Task<List<SemanticDomainUserCount>> GetSemanticDomainUserCounts(string pr
{
return Task.FromResult(new List<SemanticDomainUserCount>());
}
public Task<int> GetDomainWordCount(string projectId, string domainId)
{
return Task.FromResult(0);
}
public Task<double> GetDomainProgressProportion(string projectId, string domainId)
{
return Task.FromResult(0.0);
}
}
}
17 changes: 17 additions & 0 deletions Backend.Tests/Mocks/WordRepositoryMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,22 @@ public Task<Word> Add(Word word)
_words.Add(word.Clone());
return Task.FromResult(word);
}

public Task<int> CountFrontierWordsWithDomain(string projectId, string domainId, int? maxCount = null)
{
var count = 0;
foreach (var word in _frontier.Where(w => w.ProjectId == projectId))
{
if (word.Senses.Any(s => s.SemanticDomains.Any(sd => sd.Id == domainId)))
{
count++;
if (maxCount is not null && count >= maxCount)
{
return Task.FromResult(maxCount.Value);
}
}
}
return Task.FromResult(count);
}
}
}
34 changes: 34 additions & 0 deletions Backend/Controllers/StatisticsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,39 @@ public async Task<IActionResult> GetSemanticDomainUserCounts(string projectId)

return Ok(await _statService.GetSemanticDomainUserCounts(projectId));
}

/// <summary> Get the count of entries in a specific semantic domain </summary>
/// <returns> An integer count </returns>
[HttpGet("GetDomainWordCount", Name = "GetDomainWordCount")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(int))]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> GetDomainWordCount(string projectId, string domainId)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain word count");

if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId))
{
return Forbid();
}

return Ok(await _statService.GetDomainWordCount(projectId, domainId));
}

/// <summary> Get the proportion of descendant domains that have at least one entry </summary>
/// <returns> A double value between 0 and 1 </returns>
[HttpGet("GetDomainProgressProportion", Name = "GetDomainProgressProportion")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(double))]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> GetDomainProgressProportion(string projectId, string domainId)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain progress proportion");

if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId))
{
return Forbid();
}

return Ok(await _statService.GetDomainProgressProportion(projectId, domainId));
}
}
}
2 changes: 2 additions & 0 deletions Backend/Interfaces/IStatisticsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public interface IStatisticsService
Task<ChartRootData> GetProgressEstimationLineChartRoot(string projectId, List<DateTime> schedule);
Task<ChartRootData> GetLineChartRootData(string projectId);
Task<List<SemanticDomainUserCount>> GetSemanticDomainUserCounts(string projectId);
Task<int> GetDomainWordCount(string projectId, string domainId);
Task<double> GetDomainProgressProportion(string projectId, string domainId);
}

}
1 change: 1 addition & 0 deletions Backend/Interfaces/IWordRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ public interface IWordRepository
Task<List<Word>> AddFrontier(List<Word> words);
Task<bool> DeleteFrontier(string projectId, string wordId);
Task<long> DeleteFrontier(string projectId, List<string> wordIds);
Task<int> CountFrontierWordsWithDomain(string projectId, string domainId, int? maxCount = null);
}
}
20 changes: 20 additions & 0 deletions Backend/Repositories/WordRepository.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using BackendFramework.Helper;
using BackendFramework.Interfaces;
Expand Down Expand Up @@ -268,5 +269,24 @@ public async Task<long> DeleteFrontier(string projectId, List<string> wordIds)
var deleted = await _frontier.DeleteManyAsync(GetProjectWordsFilter(projectId, wordIds));
return deleted.DeletedCount;
}

/// <summary>
/// Counts the number of Frontier words that have the specified semantic domain.
/// </summary>
/// <param name="projectId"> The project id </param>
/// <param name="domainId"> The semantic domain id </param>
/// <param name="maxCount"> Optional maximum count to return </param>
/// <returns> The count of senses with the specified domain, capped at maxCount if provided </returns>
public async Task<int> CountFrontierWordsWithDomain(string projectId, string domainId, int? maxCount = null)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "counting frontier words with domain");

var filterDef = new FilterDefinitionBuilder<Word>();
var filter = filterDef.And(
filterDef.Eq(w => w.ProjectId, projectId),
filterDef.ElemMatch(w => w.Senses, s => s.SemanticDomains.Any(sd => sd.Id == domainId)));

return (await _frontier.Find(filter).Limit(maxCount).ToListAsync()).Count;
}
}
}
54 changes: 54 additions & 0 deletions Backend/Services/StatisticsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,5 +367,59 @@ public async Task<List<SemanticDomainUserCount>> GetSemanticDomainUserCounts(str
// return descending order by senseCount
return resUserMap.Values.ToList().OrderByDescending(t => t.WordCount).ToList();
}

/// <summary>
/// Get the count of senses in a specific semantic domain
/// </summary>
/// <param name="projectId"> The project id </param>
/// <param name="domainId"> The semantic domain id </param>
/// <returns> The count of senses with the specified domain </returns>
public async Task<int> GetDomainWordCount(string projectId, string domainId)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain word count");

return await _wordRepo.CountFrontierWordsWithDomain(projectId, domainId);
}

/// <summary>
/// Get the proportion of descendant domains that have at least one entry
/// </summary>
/// <param name="projectId"> The project id </param>
/// <param name="domainId"> The semantic domain id </param>
/// <returns> A proportion value between 0 and 1 </returns>
public async Task<double> GetDomainProgressProportion(string projectId, string domainId)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain progress proportion");

if (string.IsNullOrEmpty(projectId) || string.IsNullOrEmpty(domainId) || !char.IsDigit(domainId[0]))
{
return 0.0;
}

var domains = await _domainRepo.GetAllSemanticDomainTreeNodes("en");
if (domains is null || domains.Count == 0)
{
return 0.0;
}

var domainAndDescendants = domains
.Where(dom => dom.Id.StartsWith(domainId, StringComparison.Ordinal)).ToList();

if (domainAndDescendants.Count == 0)
{
return 0.0;
}

var count = 0.0;
foreach (var dom in domainAndDescendants)
{
if (await _wordRepo.CountFrontierWordsWithDomain(projectId, dom.Id, 1) > 0)
{
count++;
}
}

return count / domainAndDescendants.Count;
}
}
}
3 changes: 2 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"treeView": {
"findDomain": "Find a domain",
"domainNotFound": "Domain not found",
"returnToTop": "Return to the top of the domain tree."
"returnToTop": "Return to the top of the domain tree.",
"senseCountTooltip": "Number of words gathered in this domain"
},
"addWords": {
"selectEntry": "Select an entry",
Expand Down
Loading
Loading