Commit c931ac7b authored by Konstantin Schulz's avatar Konstantin Schulz

Merge branch 'master' into develop

parents 6e2611b1 b98bed42
x=$(git tag --points-at HEAD)
echo "export const version = '${x}';" > mc_frontend/src/version.ts
python ./mc_frontend/update_version.py
docker-compose build
docker-compose down
docker-compose up -d
......@@ -17871,6 +17871,14 @@ Heluios
Heluium
Heluius
Heluorum
Helvetia
Helvetiae
Helvetico
Helvetii
Helvetiis
Helvetiorum
Helvetios
Helvetium
Helymi
Helymum
Helymus
......
......@@ -369,6 +369,7 @@ class CorpusService:
return []
xml: etree.Element = etree.fromstring(resp)
XMLservice.strip_name_spaces(xml)
XMLservice.remove_notes(xml)
return XMLservice.get_text_parts_by_urn(cts_urn_raw, xml)
@staticmethod
......
......@@ -105,32 +105,44 @@ class XMLservice:
""" Parses an XML file for the various text parts and maps them to their respective URN. """
text_list: List[ReferenceableText] = []
base_urn: str = ":".join(cts_urn_raw.split(":")[:-1])
target_elements_string: str = "*[@n]"
n_attribute_xpath: str = "@n"
target_elements_string: str = f"*[{n_attribute_xpath}]"
level1_parts: List[etree._Element] = xml.xpath(
f"/GetPassage/reply/passage/TEI/text/body/div/{target_elements_string}")
text_xpath: str = ".//text()"
for l1p in level1_parts:
level2_parts: List[etree._Element] = l1p.xpath(f"./{target_elements_string}")
l1p_value: _ElementUnicodeResult = l1p.xpath("@n")[0]
l1p_value: _ElementUnicodeResult = l1p.xpath(n_attribute_xpath)[0]
if level2_parts:
for l2p in level2_parts:
l2p_value: _ElementUnicodeResult = l2p.xpath("@n")[0]
l2p_value: _ElementUnicodeResult = l2p.xpath(n_attribute_xpath)[0]
level3_parts: List[etree._Element] = l2p.xpath(f"./{target_elements_string}")
if level3_parts:
for l3p in level3_parts:
l3p_value: _ElementUnicodeResult = l3p.xpath("@n")[0]
text_values: List[str] = l3p.xpath(".//text()")
l3p_value: _ElementUnicodeResult = l3p.xpath(n_attribute_xpath)[0]
text_values: List[str] = l3p.xpath(text_xpath)
urn: str = f"{base_urn}:{str(l1p_value)}.{str(l2p_value)}.{str(l3p_value)}"
text_list.append(ReferenceableText(" ".join(" ".join(text_values).split()), urn))
else:
text_values: List[str] = l2p.xpath(".//text()")
text_values: List[str] = l2p.xpath(text_xpath)
urn: str = f"{base_urn}:{str(l1p_value)}.{str(l2p_value)}"
text_list.append(ReferenceableText(" ".join(" ".join(text_values).split()), urn))
else:
text_values: List[str] = l1p.xpath(".//text()")
text_values: List[str] = l1p.xpath(text_xpath)
urn: str = f"{base_urn}:{str(l1p_value)}"
text_list.append(ReferenceableText(" ".join(" ".join(text_values).split()), urn))
return text_list
@staticmethod
def remove_notes(parent: etree._Element):
"""Removes all notes from an XML document, including textual criticism."""
for child in parent:
if child.tag == "note":
parent.remove(child)
elif len([x for x in child]) > 0:
XMLservice.remove_notes(child)
pass
@staticmethod
def strip_name_spaces(xml: etree._Element) -> None:
"""Removes all namespaces from an XML document for easier parsing, e.g. with XPath."""
......
......@@ -30,6 +30,7 @@ class Config(object):
-1] == "mcserver" else MC_SERVER_DIRECTORY
IS_DOCKER = os.environ.get("IS_THIS_A_DOCKER_CONTAINER", False)
MC_FRONTEND_DIRECTORY = os.path.join(Path(MC_SERVER_DIRECTORY).parent.parent, "mc_frontend")
MC_FRONTEND_SRC_DIRECTORY = os.path.join(MC_FRONTEND_DIRECTORY, "src")
ASSETS_DIRECTORY = os.path.join(MC_SERVER_APP_DIRECTORY, "assets")
FILES_DIRECTORY = os.path.join(MC_SERVER_APP_DIRECTORY, "files")
TMP_DIRECTORY = os.path.join(FILES_DIRECTORY, "tmp")
......@@ -97,7 +98,7 @@ class Config(object):
FLASK_MIGRATE = "migrate"
GRAPHANNIS_DEPENDENCY_LINK = "dep"
GRAPHANNIS_LOG_PATH = os.path.join(os.getcwd(), "graphannis.log")
H5P_DIRECTORY = "/home/mc/h5p" if IS_DOCKER else os.path.join(MC_FRONTEND_DIRECTORY, "src", "assets", "h5p")
H5P_DIRECTORY = "/home/mc/h5p" if IS_DOCKER else os.path.join(MC_FRONTEND_SRC_DIRECTORY, "assets", "h5p")
# Windows: use 127.0.0.1 as host IP fallback
HOST_IP_FALLBACK = "0.0.0.0"
HOST_IP_CSM = DOCKER_SERVICE_NAME_CSM if IS_DOCKER else HOST_IP_FALLBACK
......
......@@ -676,7 +676,7 @@ class Mocks:
citation_level_1=CitationLevel.default.value)]
cts_capabilities_xml: str = '<GetCapabilities xmlns="http://chs.harvard.edu/xmlns/cts"><request><requestName>GetInventory</requestName><requestFilters>urn=urn:cts:latinLit</requestFilters></request><reply><ti:TextInventory xmlns:ti=\'http://chs.harvard.edu/xmlns/cts\'><ti:textgroup urn=\'urn:cts:latinLit:phi0660\' xmlns:ti=\'http://chs.harvard.edu/xmlns/cts\'><ti:groupname xml:lang=\'eng\'>Tibullus</ti:groupname><ti:groupname xml:lang=\'lat\'>Corpus Tibullianum</ti:groupname><ti:work xml:lang="lat" urn=\'urn:cts:latinLit:phi0660.phi001\' groupUrn=\'urn:cts:latinLit:phi0660\' xmlns:ti=\'http://chs.harvard.edu/xmlns/cts\'><ti:title xml:lang=\'lat\'>Elegiae</ti:title><ti:edition urn=\'urn:cts:latinLit:phi0660.phi001.perseus-lat2\' workUrn=\'urn:cts:latinLit:phi0660.phi001\' xmlns:ti=\'http://chs.harvard.edu/xmlns/cts\'><ti:label xml:lang=\'eng\'>Elegiae, Aliorumque carminum libri tres</ti:label><ti:description xml:lang=\'eng\'>Tibullus, creator; Postgate, J. P. (John Percival), 1853- 1926, editor </ti:description><ti:online><ti:citationMapping><ti:citation label="book" xpath="/tei:div[@n=\'?\']" scope="/tei:TEI/tei:text/tei:body/tei:div"><ti:citation label="poem" xpath="/tei:div[@n=\'?\']" scope="/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n=\'?\']"><ti:citation label="line" xpath="//tei:l[@n=\'?\']" scope="/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n=\'?\']/tei:div[@n=\'?\']"></ti:citation></ti:citation></ti:citation></ti:citationMapping></ti:online></ti:edition></ti:work><ti:work xml:lang="lat" urn=\'urn:cts:latinLit:phi0660.phi003\' groupUrn=\'urn:cts:latinLit:phi0660\' xmlns:ti=\'http://chs.harvard.edu/xmlns/cts\'> </ti:work></ti:textgroup></ti:TextInventory></reply></GetCapabilities>'
cts_passage_xml: str = '<GetPassage xmlns:tei="http://www.tei-c.org/ns/1.0" xmlns="http://chs.harvard.edu/xmlns/cts"><request><requestName>GetPassage</requestName><requestUrn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.1-1.1.2</requestUrn></request><reply><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.1-1.1.2</urn><passage><TEI xmlns="http://www.tei-c.org/ns/1.0"><text><body><div type="edition" xml:lang="lat" n="urn:cts:latinLit:phi0448.phi001.perseus-lat2"><div n="1" type="textpart" subtype="book"><div type="textpart" subtype="chapter" n="1"><div type="textpart" subtype="section" n="1"><p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.</p></div><div type="textpart" subtype="section" n="2"><p>Hi omnes lingua, institutis, legibus inter se differunt. Gallos ab Aquitanis Garumna flumen, a Belgis Matrona et Sequana dividit.</p></div></div></div></div></body></text></TEI></passage></reply></GetPassage>'
cts_passage_xml_1_level: str = '<GetPassage xmlns:tei="http://www.tei-c.org/ns/1.0" xmlns="http://chs.harvard.edu/xmlns/cts"><request><requestName>GetPassage</requestName><requestUrn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1-1.2</requestUrn></request><reply><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1-1.2</urn><passage><TEI xmlns="http://www.tei-c.org/ns/1.0"><text><body><div type="edition" xml:lang="lat" n="urn:cts:latinLit:phi0448.phi001.perseus-lat2"><div n="1" type="textpart" subtype="book"><p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.</p></div><div n="2" type="textpart" subtype="book"><p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.</p></div><div n="3" type="textpart" subtype="book"><p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.</p></div></div></body></text></TEI></passage></reply></GetPassage>'
cts_passage_xml_1_level: str = '<GetPassage xmlns:tei="http://www.tei-c.org/ns/1.0" xmlns="http://chs.harvard.edu/xmlns/cts"><request><requestName>GetPassage</requestName><requestUrn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1-1.2</requestUrn></request><reply><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1-1.2</urn><passage><TEI xmlns="http://www.tei-c.org/ns/1.0"><text><body><div type="edition" xml:lang="lat" n="urn:cts:latinLit:phi0448.phi001.perseus-lat2"><div n="1" type="textpart" subtype="book"><note>fake textual criticism</note><p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.</p></div><div n="2" type="textpart" subtype="book"><p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.</p></div><div n="3" type="textpart" subtype="book"><p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.</p></div></div></body></text></TEI></passage></reply></GetPassage>'
cts_passage_xml_2_levels: str = '<GetPassage xmlns:tei="http://www.tei-c.org/ns/1.0" xmlns="http://chs.harvard.edu/xmlns/cts"><request><requestName>GetPassage</requestName><requestUrn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1-1.2</requestUrn></request><reply><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1-1.2</urn><passage><TEI xmlns="http://www.tei-c.org/ns/1.0"><text><body><div type="edition" xml:lang="lat" n="urn:cts:latinLit:phi0448.phi001.perseus-lat2"><div n="1" type="textpart" subtype="book"><div type="textpart" subtype="section" n="1"><p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.</p></div></div></div></body></text></TEI></passage></reply></GetPassage>'
cts_reff_xml: str = '<GetValidReff xmlns:tei="http://www.tei-c.org/ns/1.0" xmlns="http://chs.harvard.edu/xmlns/cts"><request><requestName>GetValidReff</requestName><requestUrn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1</requestUrn><requestLevel>3</requestLevel></request><reply><reff><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.1</urn><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.2</urn><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.3</urn><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.4</urn><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.5</urn><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.6</urn><urn>urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.7</urn></reff></reply></GetValidReff>'
exercise: Exercise = ExerciseMC.from_dict(
......
......@@ -40,7 +40,7 @@ jmespath==0.9.5
jsonschema==3.2.0
kiwisolver==1.2.0
LinkHeader==0.4.3
lxml==4.5.0
lxml==4.6.2
Mako==1.1.2
MarkupSafe==1.1.1
matplotlib==3.2.1
......
......@@ -23,7 +23,6 @@ from typing import Dict, List, Tuple, Type, Any
from conllu import TokenList
from flask import Flask
from gensim.models import Word2Vec
from graphannis.graph import GraphUpdate
from lxml import etree
from networkx import MultiDiGraph, Graph
from requests import HTTPError
......@@ -38,15 +37,16 @@ from mcserver.app import create_app, db, start_updater, full_init
from mcserver.app.api.exerciseAPI import map_exercise_data_to_database
from mcserver.app.models import ResourceType, FileType, ExerciseType, ExerciseData, \
NodeMC, LinkMC, GraphData, Phenomenon, CustomCorpus, AnnisResponse, Solution, DownloadableFile, Language, \
VocabularyCorpus, TextComplexityMeasure, CitationLevel, FrequencyItem, TextComplexity, Dependency, PartOfSpeech, \
Choice, XapiStatement, ExerciseMC, CorpusMC, make_solution_element_from_salt_id, Sentence, ReferenceableText
from mcserver.app.services import AnnotationService, CorpusService, FileService, CustomCorpusService, DatabaseService, \
XMLservice, TextService, FrequencyService, ExerciseService
VocabularyCorpus, TextComplexityMeasure, CitationLevel, FrequencyItem, TextComplexity, Dependency, \
PartOfSpeech, Choice, XapiStatement, ExerciseMC, CorpusMC, make_solution_element_from_salt_id, Sentence, \
ReferenceableText
from mcserver.app.services import AnnotationService, CorpusService, FileService, CustomCorpusService, \
DatabaseService, XMLservice, TextService, FrequencyService, ExerciseService
from mcserver.config import TestingConfig, Config
from mcserver.models_auto import Corpus, Exercise, UpdateInfo, LearningResult
from mocks import Mocks, MockResponse, MockW2V, MockQuery, TestHelper
from openapi.openapi_server.models import VocabularyForm, VocabularyMC, TextComplexityForm, ExerciseForm, KwicForm, \
VectorNetworkForm, MatchingExercise, ExerciseTypePath, H5PForm
from openapi.openapi_server.models import VocabularyForm, VocabularyMC, TextComplexityForm, ExerciseForm, \
KwicForm, VectorNetworkForm, MatchingExercise, ExerciseTypePath, H5PForm
class McTestCase(unittest.TestCase):
......
......@@ -5,7 +5,7 @@
"homepage": "https://ionicframework.com/",
"scripts": {
"ng": "ng",
"start": "ng serve --port 8100",
"start": "python update_version.py && ng serve --port 8100",
"build": "ng build",
"test-ci": "ng test --code-coverage --watch=false",
"test-cov": "ng test --code-coverage --watch=true",
......
......@@ -8,7 +8,7 @@
</div>
</ion-buttons>
<ion-spinner *ngIf="helperService.openRequests.length"></ion-spinner>
<ion-title *ngIf="helperService.applicationState | async as state">{{ state.currentSetup.currentAuthor?.name }}</ion-title>
<ion-title *ngIf="helperService.applicationState | async as state">{{ state.mostRecentSetup.currentAuthor?.name }}</ion-title>
<ion-buttons slot="end">
<ion-menu-button autoHide="false">
<ion-icon name="menu"></ion-icon>
......@@ -20,7 +20,7 @@
<ion-content class="ion-padding">
<ion-list *ngIf="helperService.applicationState | async as state">
<ion-item *ngFor="let corpus of state.currentSetup.currentAuthor?.corpora">
<ion-item *ngFor="let corpus of state.mostRecentSetup.currentAuthor?.corpora">
<button (click)="showPossibleReferences(corpus)">
<span>{{corpus.title}}</span>
</button>
......
......@@ -95,7 +95,7 @@ export class AuthorPage implements OnInit {
showCorpora(author: Author): void {
this.corpusService.currentAuthor = author;
this.helperService.applicationState.pipe(take(1)).subscribe((as: ApplicationState) => {
as.currentSetup.currentAuthor = author;
as.mostRecentSetup.currentAuthor = author;
this.helperService.saveApplicationState(as).then();
this.helperService.goToPage(this.navCtrl, configMC.pageUrlAuthorDetail).then();
});
......
import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
import {TestBed} from '@angular/core/testing';
import {CorpusService} from './corpus.service';
import {IonicStorageModule} from '@ionic/storage';
......@@ -19,10 +19,9 @@ import {CorpusMC} from './models/corpusMC';
import {Author} from './models/author';
import {take} from 'rxjs/operators';
import {TextRange} from './models/textRange';
import Spy = jasmine.Spy;
import {AnnisResponse, NodeMC} from '../../openapi';
import {Phenomenon} from '../../openapi';
import {AnnisResponse, NodeMC, Phenomenon} from '../../openapi';
import {ReplaySubject, Subscription} from 'rxjs';
import Spy = jasmine.Spy;
describe('CorpusService', () => {
let httpClient: HttpClient;
......@@ -51,7 +50,9 @@ describe('CorpusService', () => {
it('should adjust translations', (done) => {
spyOn(corpusService.helperService, 'makeGetRequest').and.returnValue(Promise.resolve(MockMC.apiResponseFrequencyAnalysisGet));
spyOn(corpusService, 'getSortedQueryValues').and.returnValue([PartOfSpeechValue.adjective.toString()]);
spyOn(corpusService, 'getSortedQueryValues').and.callFake((query: QueryMC, queryIndex: number) => {
query.availableValues = [PartOfSpeechValue.adjective];
});
corpusService.helperService.applicationState.next(
corpusService.helperService.deepCopy(MockMC.applicationState) as ApplicationState);
corpusService.exercise.type = ExerciseType.matching;
......@@ -79,22 +80,16 @@ describe('CorpusService', () => {
stateSpy.and.returnValue(state.pipe(take(1)));
corpusService.annisResponse = undefined;
corpusService.checkAnnisResponse().then(() => {
}, () => {
}, async () => {
expect(corpusService.annisResponse).toBeFalsy();
state = new ReplaySubject<ApplicationState>();
state.next(corpusService.helperService.deepCopy(MockMC.applicationState) as ApplicationState);
stateSpy.and.returnValue(state.pipe(take(1)));
corpusService.checkAnnisResponse().then(() => {
expect(corpusService.annisResponse).toBeTruthy();
corpusService.checkAnnisResponse().then(() => {
expect(corpusService.annisResponse).toBeTruthy();
done();
}, () => {
console.log('FOURTH CHECK FAILED');
});
}, () => {
console.log('THIRD CHECK FAILED');
});
await corpusService.checkAnnisResponse();
expect(corpusService.annisResponse).toBeTruthy();
await corpusService.checkAnnisResponse();
expect(corpusService.annisResponse).toBeTruthy();
done();
});
});
});
......@@ -193,13 +188,13 @@ describe('CorpusService', () => {
it('should get sorted query values', () => {
corpusService.exercise.type = ExerciseType.cloze;
const query: QueryMC = new QueryMC({phenomenon: Phenomenon.Upostag});
let result: string[] = corpusService.getSortedQueryValues(query, 0);
expect(result.length).toBe(0);
corpusService.getSortedQueryValues(query, 0);
expect(query.availableValues.length).toBe(0);
const pmc: PhenomenonMapContent = corpusService.phenomenonMap[query.phenomenon];
pmc.specificValues = {a: 0, b: 1, c: 2};
pmc.translationValues = {a: 'a', b: 'c', c: 'b'};
result = corpusService.getSortedQueryValues(query, 0);
expect(result.length).toBe(3);
corpusService.getSortedQueryValues(query, 0);
expect(query.availableValues.length).toBe(3);
corpusService.exercise.type = ExerciseType.matching;
corpusService.annisResponse = {
frequency_analysis: [{
......@@ -213,14 +208,14 @@ describe('CorpusService', () => {
phenomena: [Phenomenon.Upostag, Phenomenon.Upostag]
}]
};
result = corpusService.getSortedQueryValues(query, 1);
expect(result.length).toBe(3);
corpusService.getSortedQueryValues(query, 1);
expect(query.availableValues.length).toBe(3);
corpusService.annisResponse.frequency_analysis.forEach(fi => fi.phenomena = [Phenomenon.Upostag]);
corpusService.annisResponse.frequency_analysis[0].values[0] = 'a';
corpusService.annisResponse.frequency_analysis[1].values[0] = 'b';
corpusService.annisResponse.frequency_analysis[2].values[0] = 'c';
result = corpusService.getSortedQueryValues(query, 0);
expect(result.length).toBe(3);
corpusService.getSortedQueryValues(query, 0);
expect(query.availableValues.length).toBe(3);
});
it('should get text', (done) => {
......@@ -267,33 +262,38 @@ describe('CorpusService', () => {
});
});
it('should initialize the current corpus', fakeAsync(() => {
corpusService.helperService.applicationState.next(new ApplicationState());
it('should initialize the current corpus', ((done) => {
corpusService.helperService.applicationState.next(new ApplicationState({mostRecentSetup: new TextData()}));
let corpus: CorpusMC = {source_urn: ''};
const subscriptions: Subscription[] = [];
function initCorpus(): void {
subscriptions.forEach((sub: Subscription, idx: number, subList: Subscription[]) => {
sub.unsubscribe();
delete subList[idx];
});
corpusService.initCurrentCorpus().then(() => {
subscriptions.push(corpusService.currentCorpus.subscribe((cc: CorpusMC) => {
corpus = cc;
}));
function initCorpus(): Promise<void> {
return new Promise<void>(resolve => {
subscriptions.forEach((sub: Subscription, idx: number, subList: Subscription[]) => {
sub.unsubscribe();
delete subList[idx];
});
corpusService.initCurrentCorpus().then(() => {
subscriptions.push(corpusService.currentCorpus.subscribe((cc: CorpusMC) => {
corpus = cc;
return resolve();
}));
});
});
flushMicrotasks();
}
initCorpus();
// first subscription will not resolve because the current corpus is null
initCorpus().then();
expect(corpus.source_urn).toBeFalsy();
corpusService.helperService.applicationState.next(
corpusService.helperService.deepCopy(MockMC.applicationState) as ApplicationState);
initCorpus();
expect(corpus.source_urn).toBeTruthy();
corpus = undefined;
initCorpus();
expect(corpus).toBeTruthy();
initCorpus().then(async () => {
expect(corpus.source_urn).toBeTruthy();
corpus = undefined;
await initCorpus();
expect(corpus).toBeTruthy();
done();
});
}));
it('should load corpora from local storage', (done) => {
......@@ -417,14 +417,14 @@ describe('CorpusService', () => {
it('should update the base word', () => {
const adjustSpy: Spy = spyOn(corpusService, 'adjustQueryValue');
const queryValuesSpy: Spy = spyOn(corpusService, 'getSortedQueryValues').and.returnValue([]);
const queryValuesSpy: Spy = spyOn(corpusService, 'getSortedQueryValues');
corpusService.annisResponse = {
frequency_analysis: corpusService.helperService.deepCopy(MockMC.apiResponseFrequencyAnalysisGet)
};
corpusService.annisResponse.frequency_analysis[0].phenomena.push(Phenomenon.Feats);
corpusService.exercise.type = ExerciseType.matching;
corpusService.exercise.queryItems.push(new QueryMC());
corpusService.updateBaseWord(new QueryMC(), 0);
corpusService.exercise.queryItems.push(new QueryMC({selectedValues: []}));
corpusService.updateBaseWord(corpusService.exercise.queryItems.slice(-1)[0], 0);
expect(adjustSpy).toHaveBeenCalledTimes(1);
expect(queryValuesSpy).toHaveBeenCalledTimes(1);
expect(corpusService.exercise.queryItems[1].phenomenon).toBe(Phenomenon.Feats);
......
......@@ -25,7 +25,6 @@ import {PhenomenonMap, PhenomenonMapContent} from 'src/app/models/phenomenonMap'
import {ReplaySubject} from 'rxjs';
import {ApplicationState} from './models/applicationState';
import {take} from 'rxjs/operators';
import {TextData} from './models/textData';
import {Storage} from '@ionic/storage';
import {UpdateInfo} from './models/updateInfo';
import configMC from '../configMC';
......@@ -58,8 +57,9 @@ export class CorpusService {
type: ExerciseType.cloze,
typeTranslation: '',
queryItems: [new QueryMC({
availableValues: [],
phenomenon: Phenomenon.Upostag,
values: [PartOfSpeechValue.adjective],
selectedValues: [PartOfSpeechValue.adjective],
})],
feedback: new Feedback({general: '', incorrect: '', partiallyCorrect: '', correct: ''}),
instructionsTranslation: ''
......@@ -75,6 +75,7 @@ export class CorpusService {
lemma: new PhenomenonMapContent({translationObject: null}),
upostag: new PhenomenonMapContent({translationObject: PartOfSpeechTranslation})
});
public previewAnnisResponse: AnnisResponse;
public searchRegexMissingString: string;
public shareLinkCopiedString: string;
public textTooLongString: string;
......@@ -89,8 +90,9 @@ export class CorpusService {
}
adjustQueryValue(query: QueryMC, queryIndex: number): void {
this.getSortedQueryValues(query, queryIndex);
// when the phenomenon changes, choose the first value from the translated list as the default
query.values = [this.getSortedQueryValues(query, queryIndex)[0]];
query.selectedValues = query.availableValues.slice(0, 1);
this.updateBaseWord(query, queryIndex);
}
......@@ -103,8 +105,8 @@ export class CorpusService {
value => this.exercise.instructionsTranslation = value);
}
if (this.exercise.type === ExerciseType.matching) {
this.exercise.queryItems = [new QueryMC({phenomenon: Phenomenon.Upostag, values: []}),
new QueryMC({phenomenon: Phenomenon.Upostag, values: []})];
this.exercise.queryItems = [new QueryMC({phenomenon: Phenomenon.Upostag, selectedValues: []}),
new QueryMC({phenomenon: Phenomenon.Upostag, selectedValues: []})];
this.getFrequencyAnalysis().then(() => {
this.adjustQueryValue(this.exercise.queryItems[0], 0);
this.adjustQueryValue(this.exercise.queryItems[1], 1);
......@@ -119,10 +121,11 @@ export class CorpusService {
checkAnnisResponse(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.annisResponse) {
if (this.annisResponse && this.previewAnnisResponse) {
return resolve();
}
this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => {
this.previewAnnisResponse = state.previewAnnisResponse;
this.annisResponse = state.mostRecentSetup.annisResponse;
this.currentAuthor = state.mostRecentSetup.currentAuthor;
this.currentUrn = state.mostRecentSetup.currentUrn;
......@@ -215,7 +218,7 @@ export class CorpusService {
getFrequencyAnalysis(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.annisResponse.frequency_analysis.length) {
if (this.annisResponse.frequency_analysis && this.annisResponse.frequency_analysis.length) {
return resolve();
} else {
const url: string = configMC.backendBaseUrl + configMC.backendApiFrequencyPath;
......@@ -234,30 +237,36 @@ export class CorpusService {
});
}
getSortedQueryValues(query: QueryMC, queryIndex: number): string[] {
getSortedQueryValues(query: QueryMC, queryIndex: number): void {
const pmc: PhenomenonMapContent = this.phenomenonMap[query.phenomenon];
if (this.exercise.type === ExerciseType.matching) {
if (queryIndex) {
const relevantFIs: FrequencyItem[] = this.annisResponse.frequency_analysis.filter(
x => x.values[0] === this.exercise.queryItems[0].values[0] &&
x => x.values[0] === this.exercise.queryItems[0].selectedValues[0] &&
x.phenomena[1] === query.phenomenon);
return Array.from(new Set<string>(relevantFIs.map(x => x.values[1]))).sort((a, b) => {
return pmc.translationValues[a] < pmc.translationValues[b] ? -1 : 1;
});
query.availableValues = Array.from(new Set<string>(relevantFIs.map(x => x.values[1])))
.sort((a, b) => {
return pmc.translationValues[a] < pmc.translationValues[b] ? -1 : 1;
});
return;
} else {
const relevantFIs: FrequencyItem[] = this.annisResponse.frequency_analysis.filter(
x => x.phenomena[0] === query.phenomenon);
return Array.from(new Set<string>(relevantFIs.map(x => x.values[0]))).sort((a, b) => {
return pmc.translationValues[a] < pmc.translationValues[b] ? -1 : 1;
});
query.availableValues = Array.from(new Set<string>(relevantFIs.map(x => x.values[0])))
.sort((a, b) => {
return pmc.translationValues[a] < pmc.translationValues[b] ? -1 : 1;
});
return;
}
}
if (!pmc.specificValues) {
return [];
query.availableValues = [];
return;
}
return Object.keys(pmc.specificValues).sort((a, b) => {
query.availableValues = Object.keys(pmc.specificValues).sort((a, b) => {
return pmc.translationValues[a] < pmc.translationValues[b] ? -1 : 1;
});
return;
}
getText(saveToCache: boolean = true): Promise<void> {
......@@ -332,8 +341,8 @@ export class CorpusService {
this.currentCorpus = new ReplaySubject<CorpusMC>(1);
if (!this.currentCorpusCache) {
this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => {
const textData: TextData = state.currentSetup ? state.currentSetup : state.mostRecentSetup;
this.currentCorpusCache = textData ? textData.currentCorpus : null;
this.currentCorpusCache = state.mostRecentSetup.currentCorpus ?
state.mostRecentSetup.currentCorpus : null;
if (this.currentCorpusCache) {
this.currentCorpus.next(this.currentCorpusCache);
}
......@@ -349,7 +358,7 @@ export class CorpusService {
initCurrentTextRange(): void {
this.currentTextRange = new ReplaySubject<TextRange>(1);
this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => {
this.currentTextRangeCache = state.currentSetup.currentTextRange;
this.currentTextRangeCache = state.mostRecentSetup.currentTextRange;
this.currentTextRange.next(this.currentTextRangeCache);
});
}
......@@ -422,7 +431,6 @@ export class CorpusService {
this.annisResponse = ar;
if (saveToCache) {
this.helperService.applicationState.pipe(take(1)).subscribe((as: ApplicationState) => {
as.currentSetup.annisResponse = null;
as.mostRecentSetup = {
annisResponse: ar,
currentUrn: this.currentUrn,
......@@ -430,10 +438,7 @@ export class CorpusService {
currentCorpus: this.currentCorpusCache,
currentTextRange: this.currentTextRangeCache
};
this.helperService.saveApplicationState(as).then(() => {
as.currentSetup.currentUrn = this.currentUrn;
as.currentSetup.annisResponse = ar;
});
this.helperService.saveApplicationState(as).then();
});
}
}
......@@ -530,7 +535,7 @@ export class CorpusService {
this.currentCorpus.next(this.currentCorpusCache);
this.setCurrentTextRange(-1, null, new TextRange({start: ['', '', ''], end: ['', '', '']}));
this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => {
state.currentSetup.currentCorpus = corpus;
state.mostRecentSetup.currentCorpus = corpus;
this.helperService.applicationState.next(state);
});
}
......@@ -556,9 +561,10 @@ export class CorpusService {
return;
}
if (!queryIndex && this.exercise.type === ExerciseType.matching) {
if (!this.getSortedQueryValues(this.exercise.queryItems[1], 1).length) {
this.getSortedQueryValues(this.exercise.queryItems[1], 1);
if (!query.selectedValues.length) {
const fi: FrequencyItem = this.annisResponse.frequency_analysis.find(
x => x.values[0] === this.exercise.queryItems[0].values[0]);
x => x.values[0] === this.exercise.queryItems[0].selectedValues[0]);
this.exercise.queryItems[1].phenomenon = fi.phenomena[1];
}
this.adjustQueryValue(this.exercise.queryItems[1], 1);
......
......@@ -65,9 +65,9 @@
*ngIf="corpusService.exercise.type === ExerciseType.matching; else notMatching">
<label>
<h2 class="label">{{ 'QUERY_VALUE' | translate }}</h2>
<select [(ngModel)]="query.values[0]" name="queryValue"
<select [(ngModel)]="query.selectedValues[0]" name="queryValue"
(change)="corpusService.updateBaseWord(query, i)">
<option *ngFor="let key of corpusService.getSortedQueryValues(query, i)"
<option *ngFor="let key of query.availableValues"
[value]=key>{{ getDisplayValue(query, key, i) }}
</option>
</select>
......@@ -76,9 +76,9 @@
<ng-template #notMatching>
<label>
<h2 class="label">{{ 'QUERY_VALUE' | translate }}</h2>
<select [(ngModel)]="query.values" name="queryValue" multiple
size="{{Math.min(corpusService.getSortedQueryValues(query, i).length, 20)}}">
<option *ngFor="let key of corpusService.getSortedQueryValues(query, i)"
<select [(ngModel)]="query.selectedValues" name="queryValue" multiple
size="{{Math.min(query.availableValues.length, 20)}}">
<option *ngFor="let key of query.availableValues"