Skip to content

Commit

Permalink
Support custom semantic domains ending in 0 (#3081)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Jul 1, 2024
1 parent 683ebac commit 755fbe0
Show file tree
Hide file tree
Showing 34 changed files with 1,138 additions and 210 deletions.
6 changes: 0 additions & 6 deletions Backend.Tests/Controllers/LiftControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -587,12 +587,6 @@ public void TestRoundtrip(RoundTripObj roundTripObj)
}
}

// Assert that the first SemanticDomain doesn't have an empty MongoId.
if (allWords[0].Senses.Count > 0 && allWords[0].Senses[0].SemanticDomains.Count > 0)
{
Assert.That(allWords[0].Senses[0].SemanticDomains[0].MongoId, Is.Not.Empty);
}

// Export.
var exportedFilePath = _liftController.CreateLiftExport(proj1.Id).Result;
var exportedDirectory = FileOperations.ExtractZipFile(exportedFilePath, null);
Expand Down
4 changes: 2 additions & 2 deletions Backend.Tests/Controllers/SemanticDomainControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected virtual void Dispose(bool disposing)
private const string Id = "1";
private const string Lang = "en";
private const string Name = "Universe";
private readonly SemanticDomainFull _semDom = new() { Id = Id, Lang = Lang, Name = Name };
private readonly SemanticDomain _semDom = new() { Id = Id, Lang = Lang, Name = Name };

[SetUp]
public void Setup()
Expand Down Expand Up @@ -60,7 +60,7 @@ public void GetAllSemanticDomainNamesNotFound()
[Test]
public void GetSemanticDomainFullDomainFound()
{
((SemanticDomainRepositoryMock)_semDomRepository).SetNextResponse(_semDom);
((SemanticDomainRepositoryMock)_semDomRepository).SetNextResponse(new SemanticDomainFull(_semDom));
var domain = (SemanticDomainFull?)(
(ObjectResult)_semDomController.GetSemanticDomainFull(Id, Lang).Result).Value;
Assert.That(domain?.Id, Is.EqualTo(Id));
Expand Down
37 changes: 24 additions & 13 deletions Backend.Tests/Models/ProjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ public void TestNotEquals()
Assert.That(project.Equals(project2), Is.False);

project2 = project.Clone();
project2.AnalysisWritingSystems.Add(new WritingSystem());
project2.AnalysisWritingSystems.Add(new());
Assert.That(project.Equals(project2), Is.False);

project2 = project.Clone();
project2.SemanticDomains.Add(new SemanticDomain());
project2.SemanticDomains.Add(new());
Assert.That(project.Equals(project2), Is.False);

project2 = project.Clone();
Expand All @@ -74,7 +74,7 @@ public void TestNotEquals()
Assert.That(project.Equals(project2), Is.False);

project2 = project.Clone();
project2.CustomFields.Add(new CustomField());
project2.CustomFields.Add(new());
Assert.That(project.Equals(project2), Is.False);

project2 = project.Clone();
Expand All @@ -99,16 +99,27 @@ public void TestNotEquals()
[Test]
public void TestClone()
{
var system = new WritingSystem("en", "WritingSystemName", "calibri");
var project = new Project { Name = "ProjectName", VernacularWritingSystem = system };
var domain = new SemanticDomain { Name = "SemanticDomainName", Id = "1" };
project.SemanticDomains.Add(domain);

var customField = new CustomField { Name = "CustomFieldName", Type = "type" };
project.CustomFields.Add(customField);

var emailInvite = new EmailInvite(10, "user@combine.org", Role.Harvester);
project.InviteTokens.Add(emailInvite);
var project = new Project
{
Id = "ProjectId",
Name = "ProjectName",
IsActive = true,
LiftImported = true,
DefinitionsEnabled = true,
GrammaticalInfoEnabled = true,
AutocompleteSetting = AutocompleteSetting.On,
SemDomWritingSystem = new("fr", "Français"),
VernacularWritingSystem = new("en", "English", "Calibri"),
AnalysisWritingSystems = new() { new("es", "Español") },
SemanticDomains = new() { new() { Name = "SemanticDomainName", Id = "1" } },
ValidCharacters = new() { "a", "b", "c" },
RejectedCharacters = new() { "X", "Y", "Z" },
CustomFields = new() { new() { Name = "CustomFieldName", Type = "type" } },
WordFields = new() { "some field string" },
PartsOfSpeech = new() { "noun", "verb" },
InviteTokens = new() { new(10, "user@combine.org", Role.Harvester) },
WorkshopSchedule = new() { new(2222, 2, 22), },
};

var project2 = project.Clone();
Assert.That(project, Is.EqualTo(project2));
Expand Down
48 changes: 48 additions & 0 deletions Backend.Tests/Models/SemanticDomainTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,54 @@ public void TestHashCode()
Is.Not.EqualTo(new SemanticDomain { Name = Guid.NewGuid().ToString() }.GetHashCode())
);
}

private static readonly List<string> _invalidIds = new()
{
"",
"a",
"123",
"1.42.9",
".1.3.6",
"9.9.r",
"1.2.3..4"
};

[TestCaseSource(nameof(_invalidIds))]
public void TestIsValidIdInvalid(string id)
{
Assert.That(SemanticDomain.IsValidId(id), Is.False);
Assert.That(SemanticDomain.IsValidId(id, true), Is.False);
}

private static readonly List<string> _validIds = new()
{
"6",
"3.7",
"1.2.9",
"9.9.9.1",
};

[TestCaseSource(nameof(_validIds))]
public void TestIsValidIdValid(string id)
{
Assert.That(SemanticDomain.IsValidId(id), Is.True);
Assert.That(SemanticDomain.IsValidId(id, true), Is.True);
}

private static readonly List<string> _customIds = new()
{
"0",
"3.0",
"1.2.0",
"9.9.9.0",
};

[TestCaseSource(nameof(_customIds))]
public void TestIsValidIdCustom(string id)
{
Assert.That(SemanticDomain.IsValidId(id), Is.False);
Assert.That(SemanticDomain.IsValidId(id, true), Is.True);
}
}

public class SemanticDomainFullTests
Expand Down
10 changes: 5 additions & 5 deletions Backend.Tests/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,20 @@ public static Project RandomProject()
{
Name = RandString(),
VernacularWritingSystem = RandomWritingSystem(),
AnalysisWritingSystems = new List<WritingSystem> { RandomWritingSystem() },
SemanticDomains = new List<SemanticDomain>()
AnalysisWritingSystems = new() { RandomWritingSystem() },
SemanticDomains = new()
};

const int numSemanticDomains = 3;
foreach (var i in Range(1, numSemanticDomains))
{
project.SemanticDomains.Add(RandomSemanticDomain($"{i}"));
project.SemanticDomains.Add(new(RandomSemanticDomain($"{i}")));
foreach (var j in Range(1, numSemanticDomains))
{
project.SemanticDomains.Add(RandomSemanticDomain($"{i}.{j}"));
project.SemanticDomains.Add(new(RandomSemanticDomain($"{i}.{j}")));
foreach (var k in Range(1, numSemanticDomains))
{
project.SemanticDomains.Add(RandomSemanticDomain($"{i}.{j}.{k}"));
project.SemanticDomains.Add(new(RandomSemanticDomain($"{i}.{j}.{k}")));
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions Backend/Controllers/LiftController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,15 @@ private async Task<IActionResult> AddImportToProject(string liftStoragePath, str
project.DefinitionsEnabled = doesImportHaveDefinitions;
project.GrammaticalInfoEnabled = doesImportHaveGrammaticalInfo;

// Add new custom domains to the project
liftMerger.GetCustomSemanticDomains().ForEach(customDom =>
{
if (!project.SemanticDomains.Any(dom => dom.Id == customDom.Id && dom.Lang == customDom.Lang))
{
project.SemanticDomains.Add(customDom);
}
});

// Store that we have imported LIFT data already for this project
// to signal the frontend not to attempt to import again in this project.
project.LiftImported = true;
Expand Down
3 changes: 2 additions & 1 deletion Backend/Interfaces/ILiftService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public interface ILiftService
ILiftMerger GetLiftImporterExporter(string projectId, string vernLang, IWordRepository wordRepo);
Task<bool> LdmlImport(string dirPath, IProjectRepository projRepo, Project project);
Task<string> LiftExport(string projectId, IWordRepository wordRepo, IProjectRepository projRepo);
Task CreateLiftRanges(List<SemanticDomain> projDoms, string rangesDest);
Task CreateLiftRanges(List<SemanticDomainFull> projDoms, string rangesDest);

// Methods to store, retrieve, and delete an export string in a common dictionary.
void StoreExport(string userId, string filePath);
Expand All @@ -27,6 +27,7 @@ public interface ILiftMerger : ILexiconMerger<LiftObject, LiftEntry, LiftSense,
{
bool DoesImportHaveDefinitions();
bool DoesImportHaveGrammaticalInfo();
List<SemanticDomainFull> GetCustomSemanticDomains();
List<WritingSystem> GetImportAnalysisWritingSystems();
Task<List<Word>> SaveImportEntries();
}
Expand Down
3 changes: 2 additions & 1 deletion Backend/Models/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ public class Project
[BsonElement("analysisWritingSystems")]
public List<WritingSystem> AnalysisWritingSystems { get; set; }

/// <summary> Custom, project-specific semantic domains. </summary>
[Required]
[BsonElement("semanticDomains")]
public List<SemanticDomain> SemanticDomains { get; set; }
public List<SemanticDomainFull> SemanticDomains { get; set; }

[Required]
[BsonElement("validCharacters")]
Expand Down
80 changes: 67 additions & 13 deletions Backend/Models/SemanticDomain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,22 @@ public class SemanticDomain
[BsonId]
[BsonElement("_id")]
[BsonRepresentation(BsonType.ObjectId)]
public string MongoId { get; set; }
public string? MongoId { get; set; }

[Required]
[BsonElement("guid")]
#pragma warning disable CA1720
public string Guid { get; set; }
#pragma warning restore CA1720

[Required]
[BsonElement("name")]
public string Name { get; set; }

[Required]
[BsonElement("id")]
public string Id { get; set; }

[Required]
[BsonElement("lang")]
public string Lang { get; set; }
Expand All @@ -37,7 +41,6 @@ public class SemanticDomain

public SemanticDomain()
{
MongoId = "";
Guid = System.Guid.Empty.ToString();
Name = "";
Id = "";
Expand Down Expand Up @@ -78,32 +81,75 @@ public override int GetHashCode()
{
return HashCode.Combine(Name, Id, Lang, Guid, UserId, Created);
}

/// <summary>
/// Check if given id string is a valid id: single non-0 digits divided by periods.
/// If allowCustom is set to true, allow the final digit to be 0.
/// </summary>
public static bool IsValidId(string id, bool allowCustom = false)
{
// Ensure the id is nonempty and composed of digits and periods
if (string.IsNullOrEmpty(id) || !id.All(c => char.IsDigit(c) || c == '.'))
{
return false;
}

// Check that each number between periods is a single non-zero digit
var parts = id.Split(".");
var allSingleDigit = parts.All(d => d.Length == 1);
if (allowCustom)
{
// Custom domains may have 0 as the final digit
parts = parts.Take(parts.Length - 1).ToArray();
}
return allSingleDigit && parts.All(d => d != "0");
}
}

public class SemanticDomainFull : SemanticDomain
{
[Required]
[BsonElement("description")]
public string Description { get; set; }

[Required]
[BsonElement("parentId")]
public string ParentId { get; set; }

[Required]
[BsonElement("questions")]
public List<string> Questions { get; set; }

public SemanticDomainFull()
public SemanticDomainFull() : base()
{
Name = "";
Id = "";
Description = "";
ParentId = "";
Questions = new();
}

public SemanticDomainFull(SemanticDomain semDom)
{
MongoId = semDom.MongoId;
Guid = semDom.Guid;
Name = semDom.Name;
Id = semDom.Id;
Lang = semDom.Lang;
UserId = semDom.UserId;
Created = semDom.Created;

Description = "";
ParentId = "";
Questions = new();
Lang = "";
}

public new SemanticDomainFull Clone()
{
var clone = (SemanticDomainFull)base.Clone();
clone.Description = Description;
clone.Questions = Questions.Select(q => q).ToList();
return clone;
return new(base.Clone())
{
Description = Description,
ParentId = ParentId,
Questions = Questions.Select(q => q).ToList()
};
}

public override bool Equals(object? obj)
Expand All @@ -116,13 +162,14 @@ public override bool Equals(object? obj)
return
base.Equals(other) &&
Description.Equals(other.Description, StringComparison.Ordinal) &&
ParentId.Equals(other.ParentId, StringComparison.Ordinal) &&
Questions.Count == other.Questions.Count &&
Questions.All(other.Questions.Contains);
}

public override int GetHashCode()
{
return HashCode.Combine(Name, Id, Description, Questions);
return HashCode.Combine(Name, Id, Description, ParentId, Questions);
}
}

Expand All @@ -134,36 +181,43 @@ public class SemanticDomainTreeNode
[BsonId]
[BsonElement("_id")]
[BsonRepresentation(BsonType.ObjectId)]
public string MongoId { get; set; }
public string? MongoId { get; set; }

[Required]
[BsonElement("lang")]
public string Lang { get; set; }

[Required]
[BsonElement("guid")]
#pragma warning disable CA1720
public string Guid { get; set; }
#pragma warning restore CA1720

[Required]
[BsonElement("name")]
public string Name { get; set; }

[Required]
[BsonElement("id")]
public string Id { get; set; }

[BsonElement("prev")]
public SemanticDomain? Previous { get; set; }

[BsonElement("next")]
public SemanticDomain? Next { get; set; }

[BsonElement("parent")]
public SemanticDomain? Parent { get; set; }

[Required]
[BsonElement("children")]
public List<SemanticDomain> Children { get; set; }

public SemanticDomainTreeNode(SemanticDomain sd)
{
Guid = sd.Guid;
MongoId = sd.MongoId;
Guid = sd.Guid;
Lang = sd.Lang;
Name = sd.Name;
Id = sd.Id;
Expand Down
Loading

0 comments on commit 755fbe0

Please sign in to comment.