Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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 TestGetDomainSenseCountNoPermission()
{
_statsController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();

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

[Test]
public async Task TestGetDomainSenseCount()
{
var result = await _statsController.GetDomainSenseCount(_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", "en");
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public async Task TestGetDomainProgressProportion()
{
var result = await _statsController.GetDomainProgressProportion(_projId, "1", "en");
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> GetDomainSenseCount(string projectId, string domainId)
{
return Task.FromResult(0);
}
public Task<double> GetDomainProgressProportion(string projectId, string domainId, string lang)
{
return Task.FromResult(0.0);
}
}
}
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 senses in a specific semantic domain </summary>
/// <returns> An integer count </returns>
[HttpGet("GetDomainSenseCount", Name = "GetDomainSenseCount")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(int))]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> GetDomainSenseCount(string projectId, string domainId)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain sense count");

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

return Ok(await _statService.GetDomainSenseCount(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, string lang)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain progress proportion");

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

return Ok(await _statService.GetDomainProgressProportion(projectId, domainId, lang));
}
}
}
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> GetDomainSenseCount(string projectId, string domainId);
Task<double> GetDomainProgressProportion(string projectId, string domainId, string lang);
}

}
87 changes: 87 additions & 0 deletions Backend/Services/StatisticsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,5 +355,92 @@ 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> GetDomainSenseCount(string projectId, string domainId)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain sense count");

var wordList = await _wordRepo.GetFrontier(projectId);
var count = 0;

foreach (var word in wordList)
{
foreach (var sense in word.Senses)
{
if (sense.SemanticDomains.Any(sd => sd.Id == domainId))
{
count++;
}
}
}

return count;
}

/// <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>
/// <param name="lang"> The language code </param>
/// <returns> A proportion value between 0 and 1 </returns>
public async Task<double> GetDomainProgressProportion(string projectId, string domainId, string lang)
{
using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain progress proportion");

var domainTreeNodeList = await _domainRepo.GetAllSemanticDomainTreeNodes(lang);
if (domainTreeNodeList is null || domainTreeNodeList.Count == 0)
{
return 0.0;
}

// Get all descendant domain IDs
var descendantIds = new List<string>();
foreach (var node in domainTreeNodeList)
{
if (node.Id.StartsWith(domainId, StringComparison.Ordinal) &&
node.Id.Length > domainId.Length &&
node.Id != domainId)
{
// Check if it's a direct or indirect child (not just a string prefix match)
var relativePart = node.Id.Substring(domainId.Length);
if (relativePart.StartsWith('.'))
{
descendantIds.Add(node.Id);
}
}
}

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

// Get word list and count which descendants have at least one entry
var wordList = await _wordRepo.GetFrontier(projectId);
var domainsWithEntries = new HashSet<string>();

foreach (var word in wordList)
{
foreach (var sense in word.Senses)
{
foreach (var sd in sense.SemanticDomains)
{
if (descendantIds.Contains(sd.Id))
{
domainsWithEntries.Add(sd.Id);
}
}
}
}

return (double)domainsWithEntries.Count / descendantIds.Count;
}
}
}
3 changes: 2 additions & 1 deletion public/locales/ar/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"treeView": {
"findDomain": "البحث عن نطاق",
"domainNotFound": "لم يتم العثور على المجال",
"returnToTop": "الرجوع إلى الجزء العلوي من شجرة النطاق."
"returnToTop": "الرجوع إلى الجزء العلوي من شجرة النطاق.",
"senseCountTooltip": "عدد الكلمات المجمعة في هذا المجال"
},
"addWords": {
"selectEntry": "حدد إدخالاً",
Expand Down
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
3 changes: 2 additions & 1 deletion public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"treeView": {
"findDomain": "Buscar un dominio",
"domainNotFound": "Dominio no encontrado",
"returnToTop": "Volver a la parte superior del árbol de dominios."
"returnToTop": "Volver a la parte superior del árbol de dominios.",
"senseCountTooltip": "Número de palabras recopiladas en este dominio"
},
"addWords": {
"selectEntry": "Seleccionar una entrada",
Expand Down
3 changes: 2 additions & 1 deletion public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
},
"treeView": {
"findDomain": "Chercher un domaine",
"domainNotFound": "Champ sémantique introuvable"
"domainNotFound": "Champ sémantique introuvable",
"senseCountTooltip": "Nombre de mots recueillis dans ce domaine"
},
"addWords": {
"selectEntry": "Choisissez une entrée",
Expand Down
3 changes: 2 additions & 1 deletion public/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
},
"treeView": {
"findDomain": "Encontrar um domínio",
"domainNotFound": "Domínio não encontrado"
"domainNotFound": "Domínio não encontrado",
"senseCountTooltip": "Número de palavras coletadas neste domínio"
},
"addWords": {
"selectEntry": "Selecione uma entrada",
Expand Down
3 changes: 2 additions & 1 deletion public/locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"treeView": {
"findDomain": "查找语义域",
"domainNotFound": "未找到语义域",
"returnToTop": "返回语义域树顶部"
"returnToTop": "返回语义域树顶部",
"senseCountTooltip": "该语义域中收集的词数"
},
"addWords": {
"selectEntry": "选择一个词条",
Expand Down
Loading