Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protect words/senses imported with data The Combine doesn't handle #2019

Merged
merged 19 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 18 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
2 changes: 1 addition & 1 deletion Backend.Tests/Controllers/WordControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public async Task TestDeleteFrontierWord()
updatedWords.ForEach(w => Assert.That(
w.Id == wordToDelete.Id ||
w.Id == otherWord.Id ||
w.Accessibility == State.Deleted));
w.Accessibility == Status.Deleted));
var updatedFrontier = await _wordRepo.GetFrontier(_projId);
Assert.That(updatedFrontier, Has.Count.EqualTo(1));
Assert.That(updatedFrontier.First().Id, Is.EqualTo(otherWord.Id));
Expand Down
8 changes: 4 additions & 4 deletions Backend.Tests/Models/WordTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ public void TestAppendDiffLanguage()

public class SenseTests
{
private const State Accessibility = State.Duplicate;
private const Status Accessibility = Status.Duplicate;

/// <summary> Words create a unique Guid by default. Use a common GUID to ensure equality in tests. </summary>
private readonly Guid _commonGuid = Guid.NewGuid();
Expand All @@ -286,16 +286,16 @@ public void TestEqualsNull()
[Test]
public void TestClone()
{
var sense = new Sense { Accessibility = State.Deleted };
var sense = new Sense { Accessibility = Status.Deleted };
Assert.AreEqual(sense, sense.Clone());
}

[Test]
public void TestHashCode()
{
Assert.AreNotEqual(
new Sense { Guid = _commonGuid, Accessibility = State.Active }.GetHashCode(),
new Sense { Guid = _commonGuid, Accessibility = State.Deleted }.GetHashCode());
new Sense { Guid = _commonGuid, Accessibility = Status.Active }.GetHashCode(),
new Sense { Guid = _commonGuid, Accessibility = Status.Deleted }.GetHashCode());
}

[Test]
Expand Down
2 changes: 1 addition & 1 deletion Backend.Tests/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static Sense RandomSense()
{
return new Sense
{
Accessibility = State.Active,
Accessibility = Status.Active,
Glosses = new List<Gloss> { RandomGloss(), RandomGloss(), RandomGloss() },
SemanticDomains = new List<SemanticDomain>
{
Expand Down
31 changes: 31 additions & 0 deletions Backend/Helper/LiftHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Linq;
using SIL.Lift.Parsing;

namespace BackendFramework.Helper
{
public class LiftHelper
{
/// <summary>
/// Determine if a <see cref="LiftEntry"/> has any data not handled by The Combine.
/// </summary>
public static bool IsProtected(LiftEntry entry)
{
return entry.Annotations.Count > 0 || entry.Etymologies.Count > 0 || entry.Fields.Count > 0 ||
(entry.Notes.Count == 1 && !String.IsNullOrEmpty(entry.Notes.First().Type)) ||
entry.Notes.Count > 1 || entry.Pronunciations.Count > 0 || entry.Relations.Count > 0 ||
entry.Traits.Any(t => !t.Value.Equals("stem")) || entry.Variants.Count > 0;
}

/// <summary>
/// Determine if a <see cref="LiftSense"/> has any data not handled by The Combine.
/// </summary>
public static bool IsProtected(LiftSense sense)
{
return sense.Examples.Count > 0 || sense.Fields.Count > 0 || sense.GramInfo != null ||
sense.Illustrations.Count > 0 || sense.Notes.Count > 0 || sense.Relations.Count > 0 ||
sense.Reversals.Count > 0 || sense.Subsenses.Count > 0 ||
(sense.Traits.Any(t => !t.Name.StartsWith("semantic-domain")));
}
}
}
13 changes: 7 additions & 6 deletions Backend/Models/Word.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class Word
[Required]
[BsonElement("accessibility")]
[BsonRepresentation(BsonType.String)]
public State Accessibility { get; set; }
public Status Accessibility { get; set; }

[Required]
[BsonElement("history")]
Expand Down Expand Up @@ -93,7 +93,7 @@ public Word()
PartOfSpeech = "";
OtherField = "";
ProjectId = "";
Accessibility = State.Active;
Accessibility = Status.Active;
Audio = new List<string>();
EditedBy = new List<string>();
History = new List<string>();
Expand Down Expand Up @@ -359,13 +359,13 @@ public class Sense
[Required]
[BsonElement("accessibility")]
[BsonRepresentation(BsonType.String)]
public State Accessibility { get; set; }
public Status Accessibility { get; set; }

public Sense()
{
// By default generate a new, unique Guid for each new Sense.
Guid = Guid.NewGuid();
Accessibility = State.Active;
Accessibility = Status.Active;
Definitions = new List<Definition>();
Glosses = new List<Gloss>();
SemanticDomains = new List<SemanticDomain>();
Expand Down Expand Up @@ -616,12 +616,13 @@ public FileUpload()
}
}

/// <summary> Information about the state of the word or sense used for merging. </summary>
public enum State
/// <summary> Information about the status of the word or sense used for merging. </summary>
public enum Status
{
Active,
Deleted,
Duplicate,
Protected,
Separate
}
}
15 changes: 13 additions & 2 deletions Backend/Services/LiftService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ public async Task<string> LiftExport(
var allWords = await wordRepo.GetAllWords(projectId);
var frontier = await wordRepo.GetFrontier(projectId);
var activeWords = frontier.Where(
x => x.Senses.Any(s => s.Accessibility == State.Active)).ToList();
x => x.Senses.Any(s => s.Accessibility == Status.Active || s.Accessibility == Status.Protected)).ToList();

// All words in the frontier with any senses are considered current.
// The Combine does not import senseless entries and the interface is supposed to prevent creating them.
Expand Down Expand Up @@ -390,7 +390,8 @@ private static void AddVern(LexEntry entry, Word wordEntry, string vernacularBcp
/// <summary> Adds each <see cref="Sense"/> of a word to be written out to lift </summary>
private static void AddSenses(LexEntry entry, Word wordEntry)
{
var activeSenses = wordEntry.Senses.Where(s => s.Accessibility == State.Active).ToList();
var activeSenses = wordEntry.Senses.Where(
s => s.Accessibility == Status.Active || s.Accessibility == Status.Protected).ToList();
foreach (var currentSense in activeSenses)
{
// Merge in senses
Expand Down Expand Up @@ -571,6 +572,11 @@ public void FinishEntry(LiftEntry entry)
Modified = Time.ToUtcIso8601(entry.DateModified)
};

if (LiftHelper.IsProtected(entry))
{
newWord.Accessibility = Status.Protected;
}

// Add Note if one exists.
// Note: Currently only support for a single note is included.
if (entry.Notes.Count > 0)
Expand Down Expand Up @@ -613,6 +619,11 @@ public void FinishEntry(LiftEntry entry)
Guid = new Guid(sense.Id)
};

if (LiftHelper.IsProtected(sense))
{
newSense.Accessibility = Status.Protected;
}

// Add definitions
foreach (var (key, value) in sense.Definition)
{
Expand Down
6 changes: 3 additions & 3 deletions Backend/Services/WordService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ public async Task<bool> Delete(string projectId, string wordId)
wordToDelete.Id = "";
wordToDelete.Modified = "";
wordToDelete.History = new List<string> { wordId };
wordToDelete.Accessibility = State.Deleted;
wordToDelete.Accessibility = Status.Deleted;

foreach (var senseAcc in wordToDelete.Senses)
{
senseAcc.Accessibility = State.Deleted;
senseAcc.Accessibility = Status.Deleted;
}

await _wordRepo.Create(wordToDelete);
Expand Down Expand Up @@ -98,7 +98,7 @@ public async Task<bool> Delete(string projectId, string wordId)
word.Id = "";
word.Modified = "";
word.ProjectId = projectId;
word.Accessibility = State.Deleted;
word.Accessibility = Status.Deleted;

// Keep track of the old word, adding it to the history.
word.History.Add(wordId);
Expand Down
5 changes: 4 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"noDomain": "No domain selected",
"noNote": "No note",
"deleteWordWarning": "This word will be permanently deleted!",
"deleteDisabled": "This was imported with data that The Combine doesn't handle, so it may not be deleted.",
"error": {
"gloss": "Glosses cannot be left blank",
"domain": "Domains cannot be left blank",
Expand Down Expand Up @@ -309,7 +310,9 @@
"list": "Drag this word to the right to start merging it with other words",
"noDups": "Nothing to merge.",
"delete": "Delete sense",
"deleteDialog": "Delete this sense?"
"deleteDialog": "Delete this sense?",
"protectedSense": "This sense was imported with data that The Combine doesn't handle, so to prevent deletion, it cannot be moved. You may still drop other senses into this one to merge them.",
"protectedWord": "This word was imported with data that The Combine doesn't handle, so to prevent deletion, its final sense cannot be removed."
},
"completed": {
"number": "Number of merges completed: "
Expand Down
2 changes: 1 addition & 1 deletion src/api/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export * from "./semantic-domain-tree-node";
export * from "./semantic-domain-user-count";
export * from "./sense";
export * from "./site-banner";
export * from "./state";
export * from "./status";
export * from "./user";
export * from "./user-created-project";
export * from "./user-edit";
Expand Down
6 changes: 3 additions & 3 deletions src/api/models/sense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { Definition } from "./definition";
import { Gloss } from "./gloss";
import { SemanticDomain } from "./semantic-domain";
import { State } from "./state";
import { Status } from "./status";

/**
*
Expand Down Expand Up @@ -49,8 +49,8 @@ export interface Sense {
semanticDomains: Array<SemanticDomain>;
/**
*
* @type {State}
* @type {Status}
* @memberof Sense
*/
accessibility: State;
accessibility: Status;
}
3 changes: 2 additions & 1 deletion src/api/models/state.ts → src/api/models/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
* @export
* @enum {string}
*/
export enum State {
export enum Status {
Active = "Active",
Deleted = "Deleted",
Duplicate = "Duplicate",
Protected = "Protected",
Separate = "Separate",
}
6 changes: 3 additions & 3 deletions src/api/models/word.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { Flag } from "./flag";
import { Note } from "./note";
import { Sense } from "./sense";
import { State } from "./state";
import { Status } from "./status";

/**
*
Expand Down Expand Up @@ -73,10 +73,10 @@ export interface Word {
modified: string;
/**
*
* @type {State}
* @type {Status}
* @memberof Word
*/
accessibility: State;
accessibility: Status;
/**
*
* @type {Array<string>}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Buttons/FlagButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function FlagButton(props: FlagButtonProps): ReactElement {
props.updateFlag ? () => setOpen(true) : active ? () => {} : undefined
}
buttonId={props.buttonId}
side="left"
side="top"
/>
{props.updateFlag ? (
<DeleteEditTextDialog
Expand Down
19 changes: 10 additions & 9 deletions src/components/DataEntry/DataEntryComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SemanticDomain,
SemanticDomainFull,
SemanticDomainTreeNode,
State,
Status,
Word,
} from "api/models";
import { getFrontierWords, getSemanticDomainFull } from "backend";
Expand Down Expand Up @@ -44,7 +44,9 @@ const paperStyle = {
/** Filter out words that do not have at least 1 active sense */
export function filterWords(words: Word[]): Word[] {
return words.filter((w) =>
w.senses.find((s) => s.accessibility === State.Active)
w.senses.find((s) =>
[Status.Active, Status.Protected].includes(s.accessibility)
)
);
}

Expand All @@ -54,13 +56,12 @@ export function filterWordsByDomain(
): DomainWord[] {
const domainWords: DomainWord[] = [];
for (const currentWord of words) {
for (const sense of currentWord.senses) {
if (
// This is for States created before .accessibility was required in the frontend.
(sense.accessibility === undefined ||
sense.accessibility === State.Active) &&
sense.semanticDomains.map((dom) => dom.id).includes(domain.id)
) {
const senses = currentWord.senses.filter((s) =>
// The undefined is for Statuses created before .accessibility was required in the frontend.
[Status.Active, Status.Protected, undefined].includes(s.accessibility)
);
for (const sense of senses) {
if (sense.semanticDomains.map((dom) => dom.id).includes(domain.id)) {
domainWords.push(new DomainWord({ ...currentWord, senses: [sense] }));
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/components/DataEntry/DataEntryTable/DataEntryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
SemanticDomain,
SemanticDomainTreeNode,
Sense,
State,
Status,
Word,
WritingSystem,
} from "api/models";
Expand Down Expand Up @@ -153,10 +153,12 @@ export default function DataEntryTable(
}
}, [innerGetWordsFromBackend, state.isFetchingFrontier]);

/** Filter out words that do not have at least 1 active sense */
/** Filter out words that do not have at least 1 Active/Protected sense */
function filterWords(words: Word[]): Word[] {
return words.filter((w) =>
w.senses.find((s) => s.accessibility === State.Active)
w.senses.find((s) =>
[Status.Active, Status.Protected].includes(s.accessibility)
)
);
}

Expand Down
19 changes: 13 additions & 6 deletions src/components/DataEntry/tests/DataEntryComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { State, Word } from "api/models";
import { Status, Word } from "api/models";
import {
filterWords,
filterWordsByDomain,
Expand All @@ -17,22 +17,29 @@ describe("DataEntryComponent", () => {
expect(filterWords(words)).toEqual(expectedWords);
});

it("removes words that aren't Active.", () => {
it("removes words with no Active/Protected sense.", () => {
const words: Word[] = [
{
...mockWord,
senses: [{ ...newSense(), accessibility: State.Deleted }],
senses: [{ ...newSense(), accessibility: Status.Deleted }],
},
{
...mockWord,
senses: [{ ...newSense(), accessibility: State.Duplicate }],
senses: [{ ...newSense(), accessibility: Status.Duplicate }],
},
];
expect(filterWords(words)).toHaveLength(0);
});

it("keeps words that are Active.", () => {
expect(filterWords([mockWord, mockWord])).toHaveLength(2);
it("keeps words with an Active/Protected sense.", () => {
const words: Word[] = [
mockWord,
{
...mockWord,
senses: [{ ...newSense(), accessibility: Status.Protected }],
},
];
expect(filterWords(words)).toHaveLength(2);
});
});

Expand Down
Loading