Commit 3ae42866 authored by Konstantin Schulz's avatar Konstantin Schulz
Browse files

introducing the exercise repository, where users can view and solve old exercises

parent 70163b4a
{
"name": "mc_frontend",
"version": "0.9.7",
"version": "0.9.9",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
......
......@@ -18,7 +18,8 @@ const routes: Routes = [
{ path: 'test', loadChildren: './test/test.module#TestPageModule' },
{ path: 'text-range', loadChildren: './text-range/text-range.module#TextRangePageModule' },
{ path: 'vocabulary-check', loadChildren: './vocabulary-check/vocabulary-check.module#VocabularyCheckPageModule' },
{ path: 'exercise', loadChildren: './exercise/exercise.module#ExercisePageModule' },
{ path: 'exercise', loadChildren: './exercise/exercise.module#ExercisePageModule' }, { path: 'exercise-list', loadChildren: './exercise-list/exercise-list.module#ExerciseListPageModule' },
......
......@@ -40,7 +40,7 @@
<ion-col size="12">
<label>
<ion-icon name="search" class="search"></ion-icon>
<input type="search" (ngModelChange)="getAuthors($event.toString())" style=""
<input type="search" (ngModelChange)="getAuthors($event.toString())"
placeholder="{{ 'AUTHOR_SEARCH' | translate }}" [(ngModel)]="currentSearchValue"/>
</label>
</ion-col>
......
......@@ -337,7 +337,7 @@ export class CorpusService {
if (this.currentText) {
return resolve();
}
this.getText(false).then(() => {
this.getText(!HelperService.mostRecentSetup.annisResponse).then(() => {
return resolve();
}, () => {
return reject();
......
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {Routes, RouterModule} from '@angular/router';
import {IonicModule} from '@ionic/angular';
import {ExerciseListPage} from './exercise-list.page';
import {TranslateModule} from '@ngx-translate/core';
const routes: Routes = [
{
path: '',
component: ExerciseListPage
}
];
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild(routes),
TranslateModule.forChild()
],
declarations: [ExerciseListPage]
})
export class ExerciseListPageModule {
}
<ion-header>
<ion-toolbar>
<div class="toolbar-left">
<ion-back-button icon="arrow-round-back" defaultHref="home"></ion-back-button>
<ion-title>{{ 'EXERCISE_LIST' | translate }}</ion-title>
</div>
<div class="toolbar-right">
<ion-spinner *ngIf="HelperService.isLoading"></ion-spinner>
<button (click)="HelperService.goToHomePage(navCtrl)">
<ion-icon name="home"></ion-icon>
</button>
</div>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-grid>
<ion-row>
<ion-col size="6">
<label>
<input type="search" (ngModelChange)="filterExercises($event.toString())" class="searchbar"
placeholder="{{ 'SEARCH' | translate }}" [(ngModel)]="currentSearchValue"/>
</label>
</ion-col>
<ion-col>
<ion-label>{{ 'SORT_BY' | translate }}</ion-label>
<ion-select [(ngModel)]="currentSortingCategory" (ngModelChange)="sortExercises()"
placeholder="{{ currentSortingCategory.toString() | translate }}">
<ion-select-option *ngFor="let key of HelperService.getEnumValues(SortingCategory)"
value="{{key}}">{{ key | translate }}
</ion-select-option>
</ion-select>
</ion-col>
</ion-row>
<ion-row *ngIf="exercises; else loading">
<ion-col>
<ion-list>
<ion-item *ngFor="let exercise of exercises">
<ion-list (click)="showExercise(exercise)">
<ion-item lines="none" class="item-nested-top">
{{exercise.work_author}}: {{exercise.work_title}}
</ion-item>
<ion-item lines="none" class="item-nested-bottom">
{{exercise.exercise_type_translation}} ({{ getDateString(exercise.last_access_time) }})
</ion-item>
</ion-list>
</ion-item>
</ion-list>
</ion-col>
</ion-row>
<ng-template #loading>
<ion-spinner></ion-spinner>
</ng-template>
</ion-grid>
</ion-content>
ion-list {
cursor: pointer;
}
.item-nested-top {
max-height: 2.6em;
}
.item-nested-bottom {
max-height: 2.1em;
}
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ExerciseListPage } from './exercise-list.page';
describe('ExerciseListPage', () => {
let component: ExerciseListPage;
let fixture: ComponentFixture<ExerciseListPage>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ExerciseListPage ],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ExerciseListPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
/* tslint:disable:no-string-literal */
import {Component, OnInit} from '@angular/core';
import {HelperService} from 'src/app/helper.service';
import {NavController, ToastController} from '@ionic/angular';
import {ExerciseMC} from 'src/app/models/exerciseMC';
import {HttpClient, HttpErrorResponse} from '@angular/common/http';
import {ExerciseTypeTranslation, MoodleExerciseType, SortingCategory} from '../models/enum';
import {TranslateService} from '@ngx-translate/core';
import {AnnisResponse} from 'src/app/models/annisResponse';
import {CorpusService} from 'src/app/corpus.service';
@Component({
selector: 'app-exercise-list',
templateUrl: './exercise-list.page.html',
styleUrls: ['./exercise-list.page.scss'],
})
export class ExerciseListPage implements OnInit {
public availableExercises: ExerciseMC[];
public currentSearchValue: string;
public currentSortingCategory: SortingCategory = SortingCategory.dateDesc;
public exercises: ExerciseMC[];
public ExerciseTypeTranslation = ExerciseTypeTranslation;
public HelperService = HelperService;
public metadata: { [eid: string]: string } = {};
public ObjectKeys = Object.keys;
public SortingCategory = SortingCategory;
constructor(public navCtrl: NavController,
public http: HttpClient,
public translateService: TranslateService,
public helperService: HelperService,
public corpusService: CorpusService,
public toastCtrl: ToastController) {
}
filterExercises(searchValue: string) {
if (!searchValue) {
this.exercises = this.availableExercises;
} else {
const searchValueLower: string = searchValue.toLowerCase();
const metadata = this.metadata;
this.exercises = this.availableExercises.filter((exercise: ExerciseMC) => {
return metadata[exercise.eid].includes(searchValueLower);
});
}
this.sortExercises();
}
getDateString(dateMilliseconds: number) {
return new Date(dateMilliseconds).toLocaleDateString();
}
ngOnInit() {
const url: string = HelperService.config['backendBaseUrl'] + HelperService.config['backendApiExerciseListPath'];
this.http.get(url).subscribe((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.sortExercises();
});
}
showExercise(exercise: ExerciseMC) {
// this.corpusService.annisResponse = new AnnisResponse({exercise_id: exercise.eid});
const url = HelperService.config['backendBaseUrl'] + HelperService.config['backendApiExercisePath'];
HelperService.isLoading = true;
this.http.get(url, {params: {eid: exercise.eid}}).subscribe((ar: AnnisResponse) => {
HelperService.isLoading = false;
HelperService.mostRecentSetup.annisResponse = ar;
this.helperService.saveMostRecentSetup().then();
this.corpusService.annisResponse = ar;
HelperService.goToPreviewPage(this.navCtrl);
}, async (error: HttpErrorResponse) => {
HelperService.isLoading = false;
HelperService.currentError = error;
const toast = await this.toastCtrl.create({
message: HelperService.generalErrorAlertMessage,
duration: 3000,
position: 'top'
});
toast.present().then();
});
}
sortExercises() {
switch (this.currentSortingCategory) {
case SortingCategory.dateAsc:
this.exercises.sort((a: ExerciseMC, b: ExerciseMC) => {
return a.last_access_time === b.last_access_time ? 0 : (a.last_access_time < b.last_access_time ? -1 : 1);
});
break;
case SortingCategory.dateDesc:
this.exercises.sort((a: ExerciseMC, b: ExerciseMC) => {
return a.last_access_time === b.last_access_time ? 0 : (a.last_access_time < b.last_access_time ? 1 : -1);
});
break;
case SortingCategory.typeAsc:
this.exercises.sort((a: ExerciseMC, b: ExerciseMC) => {
return a.exercise_type_translation === b.exercise_type_translation ? 0 :
(a.exercise_type_translation < b.exercise_type_translation ? -1 : 1);
});
break;
case SortingCategory.typeDesc:
this.exercises.sort((a: ExerciseMC, b: ExerciseMC) => {
return a.exercise_type_translation === b.exercise_type_translation ? 0 :
(a.exercise_type_translation < b.exercise_type_translation ? 1 : -1);
});
break;
case SortingCategory.authorAsc:
this.exercises.sort((a: ExerciseMC, b: ExerciseMC) => {
return a.work_author === b.work_author ? 0 : (a.work_author < b.work_author ? -1 : 1);
});
break;
case SortingCategory.authorDesc:
this.exercises.sort((a: ExerciseMC, b: ExerciseMC) => {
return a.work_author === b.work_author ? 0 : (a.work_author < b.work_author ? 1 : -1);
});
break;
default:
break;
}
}
}
......@@ -85,8 +85,9 @@ export class ExerciseParametersPage {
instructions += ` [${values.map(x => map[0][x]).join(', ')}]`;
}
// TODO: change the corpus title to something meaningful, e.g. concatenate user ID and wanted exercise title
const workTitle: string = this.corpusService.currentCorpus.title + ':' + this.corpusService.currentTextRange.start + '-' +
this.corpusService.currentTextRange.end;
const workTitle: string = this.corpusService.currentCorpus.title + ', ' +
this.corpusService.currentTextRange.start.filter(x => x).join('.') + '-' +
this.corpusService.currentTextRange.end.filter(x => x).join('.');
formData.append('work_title', workTitle);
formData.append('type', MoodleExerciseType[this.corpusService.exercise.type]);
formData.append('type_translation', this.corpusService.exercise.typeTranslation);
......@@ -108,7 +109,7 @@ export class ExerciseParametersPage {
this.corpusService.annisResponse.exercise_id = ar.exercise_id;
this.corpusService.annisResponse.uri = ar.uri;
this.corpusService.annisResponse.solutions = ar.solutions;
this.navCtrl.navigateForward('preview').then();
HelperService.goToPreviewPage(this.navCtrl);
}, async (error: HttpErrorResponse) => {
HelperService.isLoading = false;
HelperService.currentError = error;
......
......@@ -129,10 +129,20 @@ export class HelperService {
});
}
static goToAuthorPage(navCtrl) {
static getEnumValues(target: any): string[] {
return Object.keys(target).filter((value, index, array) => {
return index % 2 !== 0;
});
}
static goToAuthorPage(navCtrl: NavController) {
navCtrl.navigateForward('/author').then();
}
static goToExerciseListPage(navCtrl: NavController) {
navCtrl.navigateForward('/exercise-list').then();
}
static goToHomePage(navCtrl: NavController) {
navCtrl.navigateRoot('/home').then();
}
......@@ -145,6 +155,10 @@ export class HelperService {
navCtrl.navigateForward('/info').then();
}
static goToPreviewPage(navCtrl: NavController) {
navCtrl.navigateForward('preview').then();
}
static goToShowTextPage(navCtrl: NavController, isVocabularyCheck: boolean = false) {
navCtrl.navigateForward('/show-text').then(() => {
HelperService.isVocabularyCheck = isVocabularyCheck;
......
......@@ -8,7 +8,8 @@
</div>
<div class="toolbar-right">
<ion-spinner *ngIf="HelperService.isLoading"></ion-spinner>
<ion-select [(ngModel)]="HelperService.currentLanguage" (ngModelChange)="changeLanguage($event)" name="currentLanguage"
<ion-select [(ngModel)]="HelperService.currentLanguage" (ngModelChange)="changeLanguage($event)"
name="currentLanguage"
placeholder="{{HelperService.currentLanguage.name}}">
<ion-select-option *ngFor="let lang of HelperService.languages"
value="{{lang.shortcut}}">{{lang.name}}</ion-select-option>
......@@ -29,6 +30,12 @@
<ion-button (click)="HelperService.goToTestPage(navCtrl)">{{ 'TEST' | translate }}</ion-button>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<ion-button
(click)="HelperService.goToExerciseListPage(navCtrl)">{{ 'EXERCISE_LIST' | translate }}</ion-button>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<br>
......
/* tslint:disable:no-string-literal */
import {Component, OnInit} from '@angular/core';
import {HelperService} from 'src/app/helper.service';
import {NavController, ToastController} from '@ionic/angular';
......@@ -5,6 +6,7 @@ import {HttpClient} from '@angular/common/http';
import {TranslateService} from '@ngx-translate/core';
import {ExerciseService} from 'src/app/exercise.service';
import {CorpusService} from 'src/app/corpus.service';
import {ExerciseMC} from 'src/app/models/exerciseMC';
@Component({
selector: 'app-home',
......
......@@ -98,8 +98,8 @@ export enum ExerciseType {
export enum ExerciseTypeTranslation {
cloze = 'EXERCISE_TYPE_CLOZE' as any,
kwic = 'EXERCISE_TYPE_KWIC' as any,
matching = 'EXERCISE_TYPE_MATCHING' as any,
markWords = 'EXERCISE_TYPE_MARK_WORDS' as any,
matching = 'EXERCISE_TYPE_MATCHING' as any,
}
export enum FileType {
......@@ -172,6 +172,15 @@ export enum PhenomenonTranslation {
partOfSpeech = 'PHENOMENON_PART_OF_SPEECH' as any,
}
export enum SortingCategory {
authorAsc = 'SORTING_CATEGORY_AUTHOR_ASCENDING' as any,
authorDesc = 'SORTING_CATEGORY_AUTHOR_DESCENDING' as any,
dateAsc = 'SORTING_CATEGORY_DATE_ASCENDING' as any,
dateDesc = 'SORTING_CATEGORY_DATE_DESCENDING' as any,
typeAsc = 'SORTING_CATEGORY_TYPE_ASCENDING' as any,
typeDesc = 'SORTING_CATEGORY_TYPE_DESCENDING' as any,
}
export enum TestModuleState {
inProgress = 'inProgress' as any,
showResults = 'showResults' as any,
......
/* tslint:disable:variable-name */
import {Solution} from 'src/app/models/solution';
export class ExerciseMC {
public conll: string;
public correct_feedback: string;
public eid: string;
public exercise_type: string;
public exercise_type_translation: string;
public general_feedback: string;
public incorrect_feedback: string;
public instructions: string;
public last_access_time: number;
public partially_correct_feedback: string;
public search_values: string[];
public solutions: Solution[];
public uri: string;
public work_author: string;
public work_title: string;
constructor(init?: Partial<ExerciseMC>) {
Object.assign(this, init);
}
}
......@@ -77,9 +77,12 @@
<ion-row>
<ion-col>{{ 'UNIT_DATA_SECURITY' | translate}}</ion-col>
</ion-row>
<ion-row>
<ion-col>{{ 'TEST_MODULE_LINK_TO_CONCEPT' | translate}}
<a href="{{HelperService.config['machinaCallidaConceptUrl']}}" target="_blank">Link</a>!
</ion-col>
</ion-row>
<ion-row class="button-continue">
<ion-col></ion-col>
<ion-col></ion-col>
<ion-col>
<ion-button (click)="continue(false)">{{ 'START_LEARNING' | translate}}</ion-button>
</ion-col>
......
{
"agldtTreebankUrl": "https://perseusdl.github.io/treebank_data/",
"backendApiCorporaPath": "corpora",
"backendApiExerciseListPath": "exerciseList",
"backendApiExercisePath": "exercise",
"backendApiFilePath": "file",
"backendApiH5pPath": "h5p",
......@@ -19,6 +20,7 @@
"localStorageKeyMostRecentSetup": "mc/mostRecentSetup",
"localStorageKeyUpdateInfo": "mc/updateInfo",
"machinaCallidaBackendUrl": "https://scm.cms.hu-berlin.de/callidus/mc_backend",
"machinaCallidaConceptUrl": "https://www.projekte.hu-berlin.de/de/callidus/machina-callida",
"machinaCallidaFrontendUrl": "https://scm.cms.hu-berlin.de/callidus/mc_frontend",
"maxTextLength": 0,
"perseidsCTSapiBaseUrl": "https://cts.perseids.org/api/cts?request=",
......
......@@ -18,6 +18,7 @@
"CORPUS_UPDATE_COMPLETED": "Korpus-Update abgeschlossen",
"DATA_ALREADY_SENT": "Daten wurden bereits gesendet",
"DATA_SENT": "Daten wurden erfolgreich gesendet",
"DATE": "Datum",
"DEPENDENCY_ADJECTIVAL_CLAUSE": "Attributsatz",
"DEPENDENCY_ADJECTIVAL_MODIFIER": "Adjektivische Ergänzung",
"DEPENDENCY_ADVERBIAL_CLAUSE_MODIFIER": "Adverbialsatz",
......@@ -67,6 +68,7 @@
"EXERCISE_FEEDBACK_PARTIALLY_CORRECT": "teilweise korrekt",
"EXERCISE_FEEDBACK_PARTIALLY_CORRECT_DEFAULT": "Das ist teilweise korrekt.",
"EXERCISE_GENERATE": "Übung erstellen",
"EXERCISE_LIST": "Übungsdatenbank",
"EXERCISE_NO_OOV": "Unbekannte Vokabeln ausschließen",
"EXERCISE_PARAMETERS": "Übungsparameter",
"EXERCISE_SET_PARAMETERS": "Parameter festlegen",
......@@ -129,9 +131,17 @@
"QUERY_VALUE": "Suche",
"QUERY_VALUE_EMPTY": "Keine Suchanfrage ausgewählt",
"RESULT": "Ergebnis",
"SEARCH": "Durchsuchen...",
"SELECTION_CONFIRM": "Auswahl bestätigen",
"SOFTWARE_DEPENDENCIES": "Software-Abhängigkeiten",
"SOLUTIONS_SHUFFLE": "Mischen",
"SORT_BY": "Sortieren nach",
"SORTING_CATEGORY_AUTHOR_ASCENDING": "Autor (aufsteigend)",
"SORTING_CATEGORY_AUTHOR_DESCENDING": "Autor (absteigend)",
"SORTING_CATEGORY_DATE_ASCENDING": "Datum (aufsteigend)",
"SORTING_CATEGORY_DATE_DESCENDING": "Datum (absteigend)",
"SORTING_CATEGORY_TYPE_ASCENDING": "Typ (aufsteigend)",
"SORTING_CATEGORY_TYPE_DESCENDING": "Typ (absteigend)",
"SOURCES": "Ressourcen",
"START": "Anfang",
"START_LEARNING": "Lernmodus",
......@@ -139,11 +149,13 @@
"TEST": "Wortschatzeinheit Cicero",
"TEST_MODULE_EXERCISE_ID": "Übungs-ID",
"TEST_MODULE_GO_TO_EXERCISE": "Gehe zu Übung",
"TEST_MODULE_LINK_TO_CONCEPT": "Um mehr über den theoretischen Hintergrund zu erfahren, folge dem ",
"TEST_MODULE_PROGRESS_PART": "Fortschritt: Teil",
"TEST_MODULE_SEND_DATA": "Daten senden",
"TEST_REPEAT": "Einheit wiederholen",
"TEXT_SHOW_OOV": "Unbekannte Vokabeln markieren",
"TEXT_TOO_LONG": "Text zu lang, max. Wortzahl: ",
"TYPE": "Typ",
"UNIT_APPLICATION_TITLE": "Wortschatzarbeit am Text:",
"UNIT_DATA_SECURITY": "Datenschutz: Es werden keine persönlichen Daten erhoben. Die Ergebnisse können auch nicht bis zu einzelnen Teilnehmern zurückverfolgt werden.",
"UNIT_DIAGNOSIS_TITLE": "Eingangstest:",
......
......@@ -18,6 +18,7 @@
"CORPUS_UPDATE_COMPLETED": "Corpus update completed",
"DATA_ALREADY_SENT": "Data was already sent",
"DATA_SENT": "Data sent successfully",
"DATE": "Date",
"DEPENDENCY_ADJECTIVAL_CLAUSE": "Adjectival Clause",
"DEPENDENCY_ADJECTIVAL_MODIFIER": "Adjectival Clause",
"DEPENDENCY_ADVERBIAL_CLAUSE_MODIFIER": "Adverbial Clause",
......@@ -67,6 +68,7 @@
"EXERCISE_FEEDBACK_PARTIALLY_CORRECT": "partially correct",
"EXERCISE_FEEDBACK_PARTIALLY_CORRECT_DEFAULT": "That is partially correct.",
"EXERCISE_GENERATE": "Create exercise",
"EXERCISE_LIST": "Exercise Repository",
"EXERCISE_NO_OOV": "Exclude unknown words",
"EXERCISE_PARAMETERS": "Exercise parameters",
"EXERCISE_SET_PARAMETERS": "Set parameters",
......@@ -129,9 +131,17 @@
"QUERY_VALUE": "Search",
"QUERY_VALUE_EMPTY": "Query value is empty",
"RESULT": "Result",
"SEARCH": "Search...",
"SELECTION_CONFIRM": "Confirm selection",
"SOFTWARE_DEPENDENCIES": "Software Dependencies",
"SOLUTIONS_SHUFFLE": "Shuffle",
"SORT_BY": "Sort by",
"SORTING_CATEGORY_AUTHOR_ASCENDING": "Author (ascending)",
"SORTING_CATEGORY_AUTHOR_DESCENDING": "Author (descending)",
"SORTING_CATEGORY_DATE_ASCENDING": "Date (ascending)",
"SORTING_CATEGORY_DATE_DESCENDING": "Date (descending)",
"SORTING_CATEGORY_TYPE_ASCENDING": "Type (ascending)",
"SORTING_CATEGORY_TYPE_DESCENDING": "Type (descending)",
"SOURCES": "Sources",
"START": "Start",
"START_LEARNING": "Learning mode",
......@@ -139,11 +149,13 @@
"TEST": "Vocabulary unit (Cicero)",
"TEST_MODULE_EXERCISE_ID": "Exercise ID",
"TEST_MODULE_GO_TO_EXERCISE": "Go to exercise",
"TEST_MODULE_LINK_TO_CONCEPT": "Want to know more about the theoretical background? Follow the ",
"TEST_MODULE_PROGRESS_PART": "Progress: Part",
"TEST_MODULE_SEND_DATA": "Send data",
"TEST_REPEAT": "Repeat test",
"TEXT_SHOW_OOV": "Highlight unknown vocabulary",
"TEXT_TOO_LONG": "Text too long, max. word count: ",
"TYPE": "Type",
"UNIT_APPLICATION_TITLE": "Vocabulary work on text:",
"UNIT_DATA_SECURITY": "Privacy protection: No personal data will be collected. The results can also not be traced up to individual participants.",
"UNIT_DIAGNOSIS_TITLE": "Entry test:",
......
......@@ -134,6 +134,11 @@
cursor: pointer;
}
.searchbar {
max-width: 120px;
margin: 15px 0 0 25px;
}
.text {
text-align: left;
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment