Commit e42b1b16 authored by Konstantin Schulz's avatar Konstantin Schulz

the exercise repository now uses caching, application configuration is more easily accessible

parent 871e96e9
......@@ -39,4 +39,4 @@ To change the URL for the backend, use the `ionic.config.json` file (proxies > p
## Frontend URL
Use the `--host 0.0.0.0 --disable-host-check` flag for `ng serve` if you want to use it in a production environment with an Nginx server using proxy_pass.
## Other
For all other kinds of configuration, use `src/assets/config.json`.
For all other kinds of configuration, use `src/configMC.ts`.
{
"name": "mc_frontend",
"version": "1.5.2",
"version": "1.5.4",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
......
......@@ -7,7 +7,6 @@ import {HttpClient} from '@angular/common/http';
import {HelperService} from '../helper.service';
import {ExerciseService} from '../exercise.service';
import {ApplicationState} from '../models/applicationState';
import {Observable} from 'rxjs';
import {take} from 'rxjs/operators';
/**
......@@ -30,10 +29,13 @@ export class AuthorPage {
public http: HttpClient,
public exerciseService: ExerciseService,
public helperService: HelperService) {
if (!this.corpusService.availableAuthors) {
this.corpusService.loadCorporaFromLocalStorage();
}
if (!this.corpusService.availableAuthors.length) {
this.corpusService.loadCorporaFromLocalStorage().then(() => {
this.toggleTreebankAuthors();
});
} else {
this.toggleTreebankAuthors();
}
}
public authorsDisplayed: Author[];
......
......@@ -32,6 +32,8 @@ 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';
@Injectable({
providedIn: 'root'
......@@ -39,7 +41,7 @@ import {Storage} from '@ionic/storage';
export class CorpusService {
public annisResponse: AnnisResponse;
public availableCorpora: CorpusMC[];
public availableAuthors: Author[];
public availableAuthors: Author[] = [];
public baseUrn: string;
public citationsUnavailableString: string;
public corporaUnavailableString: string;
......@@ -81,6 +83,7 @@ export class CorpusService {
this.helperService.initApplicationState();
this.initCurrentCorpus();
this.initCurrentTextRange();
this.initUpdateInfo().then(() => {
this.checkForUpdates().finally(() => {
this.checkAnnisResponse().then(() => {
this.restoreLastCorpus().then(() => {
......@@ -90,6 +93,7 @@ export class CorpusService {
this.isMostRecentSetupLoaded = true;
});
});
});
this.initPhenomenonMap();
this.getTranslations();
this.translate.onLangChange.subscribe(() => {
......@@ -147,11 +151,10 @@ export class CorpusService {
checkForUpdates(): Promise<void> {
return new Promise((resolve, reject) => {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
this.storage.get(config['localStorageKeyUpdateInfo']).then((jsonString: string) => {
this.storage.get(configMC.localStorageKeyUpdateInfo).then((jsonString: string) => {
// check local storage for necessary updates
const updateInfoJSON: object = JSON.parse(jsonString);
this.getCorpora(updateInfoJSON ? new Date(updateInfoJSON['corpora'].lastAccessTime).getTime() : 0)
const updateInfo: UpdateInfo = JSON.parse(jsonString) as UpdateInfo;
this.getCorpora(updateInfo ? updateInfo.corpora : 0)
.then(() => {
return resolve();
}, () => {
......@@ -159,61 +162,53 @@ export class CorpusService {
});
});
});
});
}
getCorpora(lastUpdateTimeMS: number = 0): Promise<void> {
return new Promise((resolve, reject) => {
this.availableCorpora = [];
this.availableAuthors = [];
HelperService.config.pipe(take(1)).subscribe((config: object) => {
// get corpora from REST API
const url: string = config['backendBaseUrl'] + config['backendApiCorporaPath'];
const url: string = configMC.backendBaseUrl + configMC.backendApiCorporaPath;
const params: HttpParams = new HttpParams().set('last_update_time', lastUpdateTimeMS.toString());
HelperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((data: object) => {
HelperService.makeGetRequest(this.http, this.toastCtrl, url, params, this.corporaUnavailableString).then((data: object) => {
if (data) {
const corpusList: CorpusMC[] = data['corpora'] as CorpusMC[];
this.storage.set(config['localStorageKeyCorpora'], JSON.stringify(corpusList)).then();
const updateInfo: object = {corpora: {lastAccessTime: new Date().getTime()}};
this.storage.set(config['localStorageKeyUpdateInfo'], JSON.stringify(updateInfo)).then();
this.storage.set(configMC.localStorageKeyCorpora, JSON.stringify(corpusList)).then();
this.storage.get(configMC.localStorageKeyUpdateInfo).then((jsonString: string) => {
const updateInfo: UpdateInfo = JSON.parse(jsonString) as UpdateInfo;
updateInfo.corpora = new Date().getTime();
this.storage.set(configMC.localStorageKeyUpdateInfo, JSON.stringify(updateInfo)).then();
});
this.processCorpora(corpusList);
return resolve();
} else {
this.loadCorporaFromLocalStorage();
this.loadCorporaFromLocalStorage().then(() => {
return resolve();
});
}
}, async (error: HttpErrorResponse) => {
this.loadCorporaFromLocalStorage();
const toast = await this.toastCtrl.create({
message: this.corporaUnavailableString,
duration: 3000,
position: 'top'
});
toast.present().then();
return reject(error);
});
});
});
}
getCTStextPassage(urn: string): Promise<AnnisResponse> {
return new Promise(((resolve, reject) => {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
const url: string = config['backendBaseUrl'] + config['backendApiRawtextPath'];
const url: string = configMC.backendBaseUrl + configMC.backendApiRawtextPath;
const params: HttpParams = new HttpParams().set('urn', urn);
HelperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((ar: AnnisResponse) => {
return resolve(ar);
}, (error: HttpErrorResponse) => {
return reject(error);
});
});
}));
}
getCTSvalidReff(urn: string): Promise<string[]> {
return new Promise((resolve, reject) => {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
const fullUrl: string = config['backendBaseUrl'] + config['backendApiValidReffPath'];
const fullUrl: string = configMC.backendBaseUrl + configMC.backendApiValidReffPath;
const params: HttpParams = new HttpParams().set('urn', urn);
HelperService.makeGetRequest(this.http, this.toastCtrl, fullUrl, params).then((reff: string[]) => {
resolve(reff);
......@@ -221,7 +216,6 @@ export class CorpusService {
reject(error);
});
});
});
}
getFrequencyAnalysis() {
......@@ -229,21 +223,19 @@ export class CorpusService {
if (this.annisResponse.frequency_analysis.length) {
return resolve();
} else {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
const url: string = config['backendBaseUrl'] + config['backendApiFrequencyPath'];
const url: string = configMC.backendBaseUrl + configMC.backendApiFrequencyPath;
const params: HttpParams = new HttpParams().set('urn', this.currentUrn);
HelperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((fis: FrequencyItem[]) => {
HelperService.isLoading = false;
this.annisResponse.frequency_analysis = fis;
HelperService.applicationState.pipe(take(1)).subscribe((as: ApplicationState) => {
as.mostRecentSetup.annisResponse = this.annisResponse;
this.helperService.saveApplicationState(as).then();
HelperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => {
state.mostRecentSetup.annisResponse = this.annisResponse;
this.helperService.saveApplicationState(state).then();
});
return resolve();
}, () => {
return reject();
});
});
}
});
}
......@@ -344,17 +336,35 @@ export class CorpusService {
});
}
initUpdateInfo(): Promise<void> {
return new Promise<void>(resolve => {
this.storage.get(configMC.localStorageKeyUpdateInfo).then((jsonString: string) => {
if (jsonString) {
return resolve();
}
const ui: UpdateInfo = new UpdateInfo({
corpora: new Date().getTime(),
exerciseList: new Date().getTime()
});
this.storage.set(configMC.localStorageKeyUpdateInfo, JSON.stringify(ui)).then(() => {
return resolve();
});
});
});
}
isTreebank(corpus: CorpusMC) {
return corpus.source_urn.includes('proiel');
}
public loadCorporaFromLocalStorage() {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
this.storage.get(config['localStorageKeyCorpora']).then((jsonString: string) => {
public loadCorporaFromLocalStorage(): Promise<void> {
return new Promise<void>(resolve => {
this.storage.get(configMC.localStorageKeyCorpora).then((jsonString: string) => {
if (jsonString) {
const corpusList: CorpusMC[] = JSON.parse(jsonString) as CorpusMC[];
this.processCorpora(corpusList);
}
return resolve();
});
});
}
......
......@@ -54,8 +54,8 @@
</ion-col>
</ion-row>
<ion-row id="vocCorpus" style="display: none">
<ion-col style="display: inline-grid">
<ion-grid style="padding: 0; text-align: left">
<ion-col>
<ion-grid style="max-width: 30em; text-align: left">
<ion-row>
<ion-col>
<label>
......@@ -107,7 +107,7 @@
</ion-row>
<ion-row>
</ion-row>
<ion-row *ngIf="exercises; else loading">
<ion-row *ngIf="exercises?.length; else loading">
<ion-grid style="padding: 0">
<ion-row *ngFor="let exercise of exercises" (click)="showExercise(exercise)" class="exercises">
<ion-grid style="padding: 0">
......
......@@ -16,7 +16,11 @@ import {TranslateService} from '@ngx-translate/core';
import {AnnisResponse} from 'src/app/models/annisResponse';
import {CorpusService} from 'src/app/corpus.service';
import {VocabularyService} from 'src/app/vocabulary.service';
import {Storage} from '@ionic/storage';
import configMC from '../../configMC';
import {UpdateInfo} from '../models/updateInfo';
import {take} from 'rxjs/operators';
import {ApplicationState} from '../models/applicationState';
@Component({
selector: 'app-exercise-list',
......@@ -28,7 +32,7 @@ export class ExerciseListPage implements OnInit {
public availableExercises: ExerciseMC[];
public currentSearchValue: string;
public currentSortingCategory: SortingCategory = SortingCategory.dateDesc;
public exercises: ExerciseMC[];
public exercises: ExerciseMC[] = [];
public ExerciseTypeTranslation = ExerciseTypeTranslation;
public hasVocChanged = false;
public HelperService = HelperService;
......@@ -61,6 +65,7 @@ export class ExerciseListPage implements OnInit {
public translateService: TranslateService,
public helperService: HelperService,
public corpusService: CorpusService,
public storage: Storage,
public toastCtrl: ToastController,
public vocService: VocabularyService) {
}
......@@ -83,27 +88,33 @@ export class ExerciseListPage implements OnInit {
}
getExerciseList(): void {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
const url: string = config['backendBaseUrl'] + config['backendApiExerciseListPath'];
const url: string = configMC.backendBaseUrl + configMC.backendApiExerciseListPath;
this.hasVocChanged = false;
let params: HttpParams = new HttpParams().set('lang', this.translateService.currentLang);
HelperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => {
this.storage.get(configMC.localStorageKeyUpdateInfo).then((jsonString: string) => {
const updateInfo: UpdateInfo = JSON.parse(jsonString) as UpdateInfo;
// if there are no exercises in the cache, force refresh
const lut: number = state.exerciseList.length ? updateInfo.exerciseList : 0;
params = params.set('last_update_time', lut.toString());
if (this.vocService.currentReferenceVocabulary) {
params = params.set('vocabulary', VocabularyCorpus[this.vocService.currentReferenceVocabulary]);
params = params.set('frequency_upper_bound', this.vocService.frequencyUpperBound.toString());
}
HelperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((exercises: ExerciseMC[]) => {
this.availableExercises = exercises;
this.exercises = exercises;
this.exercises.forEach((exercise: ExerciseMC) => {
this.translateService.get(ExerciseTypeTranslation[MoodleExerciseType[exercise.exercise_type]]).subscribe(
value => exercise.exercise_type_translation = value);
this.metadata[exercise.eid] = [exercise.work_author, exercise.exercise_type_translation, exercise.work_title]
.join(' ').toLowerCase();
});
this.filterExercises(this.currentSearchValue);
updateInfo.exerciseList = new Date().getTime();
this.storage.set(configMC.localStorageKeyUpdateInfo, JSON.stringify(updateInfo)).then();
if (exercises.length) {
state.exerciseList = this.availableExercises = this.exercises = exercises;
this.helperService.saveApplicationState(state).then();
} else {
this.availableExercises = this.exercises = state.exerciseList;
}
this.processExercises();
}, () => {
});
});
});
}
getMatchingDegree(exercise: ExerciseMC): string {
......@@ -115,9 +126,18 @@ export class ExerciseListPage implements OnInit {
this.getExerciseList();
}
public processExercises() {
this.exercises.forEach((exercise: ExerciseMC) => {
this.translateService.get(ExerciseTypeTranslation[MoodleExerciseType[exercise.exercise_type]]).subscribe(
value => exercise.exercise_type_translation = value);
this.metadata[exercise.eid] = [exercise.work_author, exercise.exercise_type_translation, exercise.work_title]
.join(' ').toLowerCase();
});
this.filterExercises(this.currentSearchValue);
}
showExercise(exercise: ExerciseMC) {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
const url: string = config['backendBaseUrl'] + config['backendApiExercisePath'];
const url: string = configMC.backendBaseUrl + configMC.backendApiExercisePath;
const params: HttpParams = new HttpParams().set('eid', exercise.eid);
HelperService.makeGetRequest(this.http, this.toastCtrl, url, params).then((ar: AnnisResponse) => {
// save this exercise only locally in the CorpusService (not as MostRecentSetup in the HelperService) because
......@@ -128,11 +148,10 @@ export class ExerciseListPage implements OnInit {
HelperService.goToPreviewPage(this.navCtrl).then();
}, () => {
});
});
}
sortExercises(): void {
if (!this.exercises ||
if (!this.exercises.length ||
this.sortingCategoriesVocCheck.has(this.currentSortingCategory) && !this.exercises.some(x => !!x.matching_degree)) {
return;
}
......
......@@ -21,6 +21,7 @@ import {CorpusMC} from '../models/corpusMC';
import {ApplicationState} from '../models/applicationState';
import {take} from 'rxjs/operators';
import {TextRange} from '../models/textRange';
import configMC from '../../configMC';
@Component({
selector: 'app-exercise-parameters',
......@@ -54,8 +55,7 @@ export class ExerciseParametersPage implements OnInit {
async generateExercise() {
const phenomenon: Phenomenon = this.corpusService.exercise.queryItems[0].phenomenon;
HelperService.config.pipe(take(1)).subscribe(async (config: object) => {
if (0 < config['maxTextLength'] && config['maxTextLength'] < this.corpusService.currentText.length) {
if (0 < configMC.maxTextLength && configMC.maxTextLength < this.corpusService.currentText.length) {
const toast = await this.toastCtrl.create({
message: this.textTooLongString,
duration: 3000,
......@@ -74,7 +74,6 @@ export class ExerciseParametersPage implements OnInit {
this.corpusService.annisResponse.solutions = null;
this.getExerciseData();
}
});
}
public getDisplayValue(query: QueryMC, key: string, queryIndex: number = 0): string {
......@@ -132,8 +131,7 @@ export class ExerciseParametersPage implements OnInit {
}
getH5Pexercise(formData: FormData) {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
const url = config['backendBaseUrl'] + config['backendApiExercisePath'];
const url: string = configMC.backendBaseUrl + configMC.backendApiExercisePath;
HelperService.currentError = null;
HelperService.isLoading = true;
this.http.post(url, formData).subscribe((ar: AnnisResponse) => {
......@@ -158,12 +156,10 @@ export class ExerciseParametersPage implements OnInit {
});
toast.present().then();
});
});
}
getKwicExercise(formData: FormData) {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
const kwicUrl: string = config['backendBaseUrl'] + config['backendApiKwicPath'];
const kwicUrl: string = configMC.backendBaseUrl + configMC.backendApiKwicPath;
HelperService.currentError = null;
HelperService.isLoading = true;
this.http.post(kwicUrl, formData).subscribe((svgString: string) => {
......@@ -180,14 +176,11 @@ export class ExerciseParametersPage implements OnInit {
});
toast.present().then();
});
});
}
initTranslation() {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
this.translateService.get('TEXT_TOO_LONG').subscribe(
value => this.textTooLongString = value + config['maxTextLength']);
});
value => this.textTooLongString = value + configMC.maxTextLength);
this.translateService.get('QUERY_VALUE_EMPTY').subscribe(value => this.emptyQueryValueString = value);
}
......
......@@ -11,6 +11,7 @@ import {ExerciseType, MoodleExerciseType} from 'src/app/models/enum';
import {CorpusService} from 'src/app/corpus.service';
import {ApplicationState} from '../models/applicationState';
import {take} from 'rxjs/operators';
import configMC from '../../configMC';
@Component({
selector: 'app-exercise',
......@@ -36,9 +37,8 @@ export class ExercisePage implements OnInit {
loadExercise(): void {
this.activatedRoute.queryParams.subscribe((params: object) => {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
if (params['eid']) {
let url: string = config['backendBaseUrl'] + config['backendApiExercisePath'];
let url: string = configMC.backendBaseUrl + configMC.backendApiExercisePath;
const httpParams: HttpParams = new HttpParams().set('eid', params['eid']);
HelperService.makeGetRequest(this.http, this.toastCtrl, url, httpParams).then((ar: AnnisResponse) => {
HelperService.applicationState.pipe(take(1)).subscribe((as: ApplicationState) => {
......@@ -48,9 +48,9 @@ export class ExercisePage implements OnInit {
const met: MoodleExerciseType = MoodleExerciseType[ar.exercise_type];
this.corpusService.exercise.type = ExerciseType[met.toString()];
// this will be called via GET request from the h5p standalone javascript library
url = `${config['backendBaseUrl']}${config['backendApiH5pPath']}` +
url = `${configMC.backendBaseUrl}${configMC.backendApiH5pPath}` +
`?eid=${this.corpusService.annisResponse.exercise_id}&lang=${this.translateService.currentLang}`;
window.localStorage.setItem(config['localStorageKeyH5P'], url);
window.localStorage.setItem(configMC.localStorageKeyH5P, url);
const exerciseTypePath: string = this.corpusService.exercise.type === ExerciseType.markWords ?
'mark_words' : 'drag_text';
this.exerciseService.initH5P(exerciseTypePath);
......@@ -63,12 +63,11 @@ export class ExercisePage implements OnInit {
this.exerciseService.fillBlanksString : exerciseType;
const file: string = params['file'];
const lang: string = this.translateService.currentLang;
window.localStorage.setItem(config['localStorageKeyH5P'],
window.localStorage.setItem(configMC.localStorageKeyH5P,
HelperService.baseUrl + '/assets/h5p/' + exerciseType + '/content/' + file + '_' + lang + '.json');
this.exerciseService.initH5P(exerciseTypePath);
}
});
});
}
ngOnInit() {
......
......@@ -10,7 +10,7 @@ import {Storage} from '@ionic/storage';
import {Language} from 'src/app/models/language';
import {ReplaySubject} from 'rxjs';
import {TextData} from './models/textData';
import {take} from 'rxjs/operators';
import configMC from '../configMC';
@Injectable({
providedIn: 'root'
......@@ -30,7 +30,6 @@ export class HelperService {
Voc: CaseValue.vocative,
Loc: CaseValue.locative,
};
public static config: ReplaySubject<object>;
public static corpusUpdateCompletedString: string;
public static currentError: HttpErrorResponse;
public static currentLanguage: Language;
......@@ -225,7 +224,8 @@ export class HelperService {
});
}
static makeGetRequest(http: HttpClient, toastCtrl: ToastController, url: string, params: HttpParams): Promise<any> {
static makeGetRequest(http: HttpClient, toastCtrl: ToastController, url: string, params: HttpParams,
errorMessage: string = HelperService.generalErrorAlertMessage): Promise<any> {
HelperService.currentError = null;
// dirty hack to avoid ExpressionChangedAfterItHasBeenCheckedError
setTimeout(() => {
......@@ -245,7 +245,7 @@ export class HelperService {
}, 0);
HelperService.currentError = error;
const toast = await toastCtrl.create({
message: HelperService.generalErrorAlertMessage,
message: errorMessage,
duration: 3000,
position: 'top'
});
......@@ -273,32 +273,28 @@ export class HelperService {
initApplicationState(): void {
HelperService.applicationState = new ReplaySubject<ApplicationState>(1);
if (!HelperService.applicationStateCache) {
HelperService.config.pipe(take(1)).subscribe((config: object) => {
this.storage.get(config['localStorageKeyApplicationState']).then((jsonString: string) => {
this.storage.get(configMC.localStorageKeyApplicationState).then((jsonString: string) => {
HelperService.applicationStateCache = new ApplicationState({
currentSetup: new TextData()
currentSetup: new TextData(),
exerciseList: []
});
if (jsonString) {
const parsedJson: object = JSON.parse(jsonString);
HelperService.applicationStateCache = parsedJson as ApplicationState;
const state: ApplicationState = JSON.parse(jsonString) as ApplicationState;
state.exerciseList = state.exerciseList ? state.exerciseList : [];
HelperService.applicationStateCache = state;
}
HelperService.applicationState.next(HelperService.applicationStateCache);
});
});
} else {
HelperService.applicationState.next(HelperService.applicationStateCache);
}
}
initConfig() {
HelperService.config = new ReplaySubject<object>(1);
this.http.get('assets/config.json').subscribe((config: object) => {
if (!config['backendBaseUrl']) {
if (!configMC.backendBaseUrl) {
const part1: string = location.protocol.concat('//').concat(window.location.host);
config['backendBaseUrl'] = part1.concat(config['backendBaseApiPath']).concat('/');
configMC.backendBaseUrl = part1.concat(configMC.backendBaseApiPath).concat('/');
}
HelperService.config.next(config);
});
}
initLanguage(): void {
......@@ -313,11 +309,9 @@ export class HelperService {
return new Promise((resolve) => {
HelperService.applicationStateCache = mrs;
HelperService.applicationState.next(HelperService.applicationStateCache);
HelperService.config.pipe(take(1)).subscribe((config: object) => {
this.storage.set(config['localStorageKeyApplicationState'], JSON.stringify(mrs)).then(() => {
this.storage.set(configMC.localStorageKeyApplicationState, JSON.stringify(mrs)).then(() => {
return resolve();
});
});
});
}
}
import {TextData} from './textData';
import {ExerciseMC} from './exerciseMC';
export class ApplicationState {
public currentSetup: TextData;
public exerciseList: ExerciseMC[];
public mostRecentSetup: TextData;
constructor(init?: Partial<ApplicationState>) {
......
export class UpdateInfo {
public corpora: number;
public exerciseList: number;
constructor(init?: Partial<UpdateInfo>) {
Object.assign(this, init);
}
}