Commit 319924fd authored by Konstantin Schulz's avatar Konstantin Schulz

added a new H5P exercise type (Dialog Cards) and some analog exercise...

added a new H5P exercise type (Dialog Cards) and some analog exercise materials for download as PDF/DOCX
parent 67289414
Pipeline #14749 passed with stages
in 2 minutes and 47 seconds
......@@ -31,6 +31,8 @@ To generate class structures for this project automatically:
1. Install OpenAPI Generator (using, e.g., `brew install openapi-generator`).
2. Run: `openapi-generator generate -i mc_backend/mcserver/mcserver_api.yaml -g typescript-angular -o mc_frontend/openapi/ && openapi-generator generate -i mc_backend/mcserver/mcserver_api.yaml -g python-flask -o mc_backend/openapi/ && python mc_backend/openapi_generator.py`.
Note: If an **unused Enum** is specified in the API documentation, it will not be generated. To force-generate its model, provide a second, derived schema that relies on the Enum using allOf-inheritance.
## Documentation
### API
To view the official API documentation, visit https://korpling.org/mc-service/mc/api/v1.0/ui/ .
......
......@@ -2,7 +2,7 @@ import json
import os
import shutil
import zipfile
from typing import List, Union, Set
from typing import List, Union, Set, Any
import connexion
from connexion.lifecycle import ConnexionResponse
from flask import Response, send_from_directory
......@@ -12,11 +12,12 @@ from mcserver.app.models import Language, ExerciseType, Solution, MimeType, File
from mcserver.app.services import TextService, NetworkService, DatabaseService
from mcserver.models_auto import Exercise
from mocks import Mocks
from openapi.openapi_server.models import H5PForm
from openapi.openapi_server.models import H5PForm, ExerciseAuthor
from openapi.openapi_server.models.base_model_ import Model
def determine_language(lang: str) -> Language:
"""Convert the given language ISO code to our internal enum-style representation of languages."""
"""Converts the given language ISO code to our internal enum-style representation of languages."""
language: Language
try:
language = Language(lang)
......@@ -43,9 +44,14 @@ def get(eid: str, lang: str, solution_indices: List[int]) -> Union[Response, Con
return NetworkService.make_json_response(response_dict)
def get_possible_enum_values(openapi_enum: Union[Model, Any]) -> Set[str]:
"""Retrieves all distinct values for a string enum model specified using OpenAPI."""
return set([x for x in list(dict(openapi_enum.__dict__).values())[2:] if type(x) == str])
def get_response(response_dict: dict, lang: Language, json_template_drag_text: dict, exercise: Exercise,
text_field_content: str, feedback_template: str) -> dict:
"""Perform localization for an existing H5P exercise template and insert the relevant exercise materials."""
"""Performs localization for an existing H5P exercise template and insert the relevant exercise materials."""
# default values for buttons and response
button_dict: dict = {"check": ["checkAnswerButton", "Prüfen" if lang == Language.German else "Check"],
"again": ["tryAgainButton", "Nochmal" if lang == Language.German else "Retry"],
......@@ -68,7 +74,7 @@ def get_response(response_dict: dict, lang: Language, json_template_drag_text: d
def get_text_field_content(exercise: Exercise, solution_indices: List[int]) -> str:
"""Build the text field content for a H5P exercise, i.e. the task, exercise material and solutions."""
"""Builds the text field content for a H5P exercise, i.e. the task, exercise material and solutions."""
text_field_content: str = ""
if exercise.exercise_type in [ExerciseType.cloze.value, ExerciseType.markWords.value]:
text_field_content = TextService.get_h5p_text_with_solutions(exercise, solution_indices)
......@@ -89,10 +95,11 @@ def make_h5p_archive(file_name_no_ext: str, response_dict: dict, target_dir: str
# exclude empty directories from the archive because the Moodle H5P importer cannot handle them
white_list: Set[str] = {'.svg', '.otf', '.json', '.css', '.diff', '.woff', '.eot', '.png', '.gif',
'.woff2', '.js', '.ttf'}
excluded_folders: Set[str] = get_possible_enum_values(ExerciseAuthor).union({"content"})
with zipfile.ZipFile(os.path.join(target_dir, file_name), "w") as zipObj:
# Iterate over all the files in directory
for folder_name, subfolders, file_names in os.walk(source_dir):
if folder_name.endswith("content"):
if any(x for x in excluded_folders if folder_name.endswith(x)):
continue
for new_file_name in file_names:
# create complete filepath of file in directory
......
......@@ -450,7 +450,7 @@ paths:
# include this here so the data model gets generated correctly
components:
schemas:
ExerciseAuthorExtension:
$ref: '../openapi_models.yaml#/components/schemas/ExerciseAuthor'
TextComplexityFormExtension:
type: object
allOf:
- $ref: '../openapi_models.yaml#/components/schemas/TextComplexityForm'
$ref: '../openapi_models.yaml#/components/schemas/TextComplexityForm'
......@@ -5,6 +5,8 @@ from __future__ import absolute_import
# import models into model package
from openapi.openapi_server.models.annis_response import AnnisResponse
from openapi.openapi_server.models.corpus import Corpus
from openapi.openapi_server.models.exercise_author import ExerciseAuthor
from openapi.openapi_server.models.exercise_author_extension import ExerciseAuthorExtension
from openapi.openapi_server.models.exercise_base import ExerciseBase
from openapi.openapi_server.models.exercise_extension import ExerciseExtension
from openapi.openapi_server.models.exercise_form import ExerciseForm
......
# coding: utf-8
from __future__ import absolute_import
from datetime import date, datetime # noqa: F401
from typing import List, Dict # noqa: F401
from openapi.openapi_server.models.base_model_ import Model
from openapi.openapi_server import util
class ExerciseAuthor(Model):
"""NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
Do not edit the class manually.
"""
"""
allowed enum values
"""
CALLIDUS = "callidus"
POTSDAM = "potsdam"
def __init__(self): # noqa: E501
"""ExerciseAuthor - a model defined in OpenAPI
"""
self.openapi_types = {
}
self.attribute_map = {
}
@classmethod
def from_dict(cls, dikt) -> 'ExerciseAuthor':
"""Returns the dict as a model
:param dikt: A dict.
:type: dict
:return: The ExerciseAuthor of this ExerciseAuthor. # noqa: E501
:rtype: ExerciseAuthor
"""
return util.deserialize_model(dikt, cls)
# coding: utf-8
from __future__ import absolute_import
from datetime import date, datetime # noqa: F401
from typing import List, Dict # noqa: F401
from openapi.openapi_server.models.base_model_ import Model
from openapi.openapi_server import util
class ExerciseAuthorExtension(Model):
"""NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
Do not edit the class manually.
"""
"""
allowed enum values
"""
CALLIDUS = "callidus"
POTSDAM = "potsdam"
def __init__(self): # noqa: E501
"""ExerciseAuthorExtension - a model defined in OpenAPI
"""
self.openapi_types = {
}
self.attribute_map = {
}
@classmethod
def from_dict(cls, dikt) -> 'ExerciseAuthorExtension':
"""Returns the dict as a model
:param dikt: A dict.
:type: dict
:return: The ExerciseAuthorExtension of this ExerciseAuthorExtension. # noqa: E501
:rtype: ExerciseAuthorExtension
"""
return util.deserialize_model(dikt, cls)
......@@ -18,6 +18,7 @@ class ExerciseTypePath(Model):
"""
allowed enum values
"""
DIALOG_CARDS = "dialog_cards"
DRAG_TEXT = "drag_text"
FILL_BLANKS = "fill_blanks"
MARK_WORDS = "mark_words"
......
......@@ -6,10 +6,8 @@ from datetime import date, datetime # noqa: F401
from typing import List, Dict # noqa: F401
from openapi.openapi_server.models.base_model_ import Model
from openapi.openapi_server.models.text_complexity_form import TextComplexityForm
from openapi.openapi_server import util
from openapi.openapi_server.models.text_complexity_form import TextComplexityForm # noqa: E501
class TextComplexityFormExtension(Model):
"""NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
......
......@@ -605,10 +605,37 @@ paths:
x-openapi-router-controller: openapi_server.controllers.default_controller
components:
schemas:
ExerciseAuthorExtension:
description: Authors of curated exercises in the Machina Callida.
enum:
- callidus
- potsdam
example: callidus
type: string
TextComplexityFormExtension:
allOf:
- $ref: '#/components/schemas/TextComplexityForm'
description: Relevant parameters for measuring the text complexity of a text
passage.
discriminator:
propertyName: measure
properties:
measure:
description: Label of the desired measure for text complexity.
example: all
type: string
urn:
description: CTS URN for the text passage from which the text complexity
should be calculated.
example: urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.1-1.1.1
type: string
annis_response:
description: Serialized ANNIS response.
example: '{}'
type: string
required:
- measure
- urn
type: object
x-body-name: complexity_data
Corpus:
description: Collection of texts.
example:
......@@ -1300,6 +1327,7 @@ components:
ExerciseTypePath:
description: Paths to the data directories for various H5P exercise types.
enum:
- dialog_cards
- drag_text
- fill_blanks
- mark_words
......@@ -1420,6 +1448,13 @@ components:
- vocabulary
type: object
x-body-name: vocabulary_data
ExerciseAuthor:
description: Authors of curated exercises in the Machina Callida.
enum:
- callidus
- potsdam
example: callidus
type: string
TextComplexityForm:
description: Relevant parameters for measuring the text complexity of a text
passage.
......
......@@ -129,6 +129,11 @@ components:
description: Localized expression of the exercise type.
example: Cloze
default: ""
ExerciseAuthor:
type: string
enum: [ callidus, potsdam ]
description: Authors of curated exercises in the Machina Callida.
example: callidus
ExerciseBase:
description: Base data for creating and evaluating interactive exercises.
type: object
......@@ -255,7 +260,7 @@ components:
- urn
ExerciseTypePath:
type: string
enum: [drag_text, fill_blanks, mark_words, multi_choice, voc_list]
enum: [dialog_cards, drag_text, fill_blanks, mark_words, multi_choice, voc_list]
description: Paths to the data directories for various H5P exercise types.
example: drag_text
FileForm:
......
......@@ -358,7 +358,7 @@ class McTestCase(unittest.TestCase):
DatabaseService.commit()
response = Mocks.app_dict[self.class_name].client.post(
TestingConfig.SERVER_URI_H5P, headers=Mocks.headers_form_data, data=hf.to_dict())
self.assertEqual(len(response.get_data()), 1963607)
self.assertEqual(len(response.get_data()), 1940145)
with patch.object(mcserver.app.api.h5pAPI, "get_text_field_content", return_value=""):
response = Mocks.app_dict[self.class_name].client.post(
TestingConfig.SERVER_URI_H5P, headers=Mocks.headers_form_data, data=hf.to_dict())
......
/**
* Machina Callida Backend REST API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Authors of curated exercises in the Machina Callida.
*/
export type ExerciseAuthor = 'callidus' | 'potsdam';
export const ExerciseAuthor = {
Callidus: 'callidus' as ExerciseAuthor,
Potsdam: 'potsdam' as ExerciseAuthor
};
/**
* Machina Callida Backend REST API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Authors of curated exercises in the Machina Callida.
*/
export type ExerciseAuthorExtension = 'callidus' | 'potsdam';
export const ExerciseAuthorExtension = {
Callidus: 'callidus' as ExerciseAuthorExtension,
Potsdam: 'potsdam' as ExerciseAuthorExtension
};
......@@ -14,9 +14,10 @@
/**
* Paths to the data directories for various H5P exercise types.
*/
export type ExerciseTypePath = 'drag_text' | 'fill_blanks' | 'mark_words' | 'multi_choice' | 'voc_list';
export type ExerciseTypePath = 'dialog_cards' | 'drag_text' | 'fill_blanks' | 'mark_words' | 'multi_choice' | 'voc_list';
export const ExerciseTypePath = {
DialogCards: 'dialog_cards' as ExerciseTypePath,
DragText: 'drag_text' as ExerciseTypePath,
FillBlanks: 'fill_blanks' as ExerciseTypePath,
MarkWords: 'mark_words' as ExerciseTypePath,
......
export * from './annisResponse';
export * from './corpus';
export * from './exerciseAuthor';
export * from './exerciseAuthorExtension';
export * from './exerciseBase';
export * from './exerciseExtension';
export * from './exerciseForm';
......
......@@ -9,9 +9,23 @@
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { TextComplexityForm } from './textComplexityForm';
export interface TextComplexityFormExtension extends TextComplexityForm {
/**
* Relevant parameters for measuring the text complexity of a text passage.
*/
export interface TextComplexityFormExtension {
/**
* Label of the desired measure for text complexity.
*/
measure: string;
/**
* CTS URN for the text passage from which the text complexity should be calculated.
*/
urn: string;
/**
* Serialized ANNIS response.
*/
annis_response?: string;
}
......@@ -9491,10 +9491,9 @@
}
},
"jszip": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz",
"integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==",
"dev": true,
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
"integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
......@@ -9777,7 +9776,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"requires": {
"immediate": "~3.0.5"
}
......@@ -12624,8 +12622,7 @@
"set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
"dev": true
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
},
"set-value": {
"version": "2.0.1",
......
......@@ -43,6 +43,7 @@
"cordova-plugin-whitelist": "^1.3.4",
"core-js": "^3.6.4",
"imports-loader": "^1.0.0",
"jszip": "^3.5.0",
"rxjs": "^6.5.4",
"toposort-class": "^1.0.1",
"tslib": "^1.11.1",
......
......@@ -35,8 +35,8 @@
</ion-row>
<ion-row>
<ion-col>
<a (click)="helperService.goToTestPage(navCtrl).then(closeMenu.bind(this))">
{{ 'TEST' | translate }}
<a (click)="helperService.goToPage(navCtrl, configMC.pageUrlSequences).then(closeMenu.bind(this))">
{{ 'SEQUENCES' | translate }}
</a>
</ion-col>
</ion-row>
......
......@@ -7,7 +7,6 @@ import {TranslateTestingModule} from './translate-testing/translate-testing.modu
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {HttpClient, HttpErrorResponse} from '@angular/common/http';
import {HelperService} from './helper.service';
import MockMC from './models/mockMC';
import {CaseValue, DependencyValue, ExerciseType, PartOfSpeechValue} from './models/enum';
import {ApplicationState} from './models/applicationState';
......@@ -23,13 +22,12 @@ import {TextRange} from './models/textRange';
import Spy = jasmine.Spy;
import {AnnisResponse, NodeMC} from '../../openapi';
import {Phenomenon} from '../../openapi';
import {Subscription} from 'rxjs';
import {ReplaySubject, Subscription} from 'rxjs';
describe('CorpusService', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let corpusService: CorpusService;
let helperService: HelperService;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [
......@@ -40,13 +38,11 @@ describe('CorpusService', () => {
],
providers: [
{provide: APP_BASE_HREF, useValue: '/'},
HelperService,
],
});
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
corpusService = TestBed.inject(CorpusService);
helperService = TestBed.inject(HelperService);
});
it('should be created', () => {
......@@ -54,9 +50,10 @@ describe('CorpusService', () => {
});
it('should adjust translations', (done) => {
spyOn(helperService, 'makeGetRequest').and.returnValue(Promise.resolve(MockMC.apiResponseFrequencyAnalysisGet));
spyOn(corpusService.helperService, 'makeGetRequest').and.returnValue(Promise.resolve(MockMC.apiResponseFrequencyAnalysisGet));
spyOn(corpusService, 'getSortedQueryValues').and.returnValue([PartOfSpeechValue.adjective.toString()]);
helperService.applicationState.next(helperService.deepCopy(MockMC.applicationState) as ApplicationState);
corpusService.helperService.applicationState.next(
corpusService.helperService.deepCopy(MockMC.applicationState) as ApplicationState);
corpusService.exercise.type = ExerciseType.matching;
corpusService.annisResponse = {frequency_analysis: []};
corpusService.adjustTranslations().then(() => {
......@@ -70,23 +67,33 @@ describe('CorpusService', () => {
});
it('should check for an existing ANNIS response', (done) => {
helperService.applicationState.error(0);
let state: ReplaySubject<ApplicationState> = new ReplaySubject<ApplicationState>();
state.error(0);
const stateSpy: Spy = spyOn(corpusService.helperService.applicationState, 'pipe')
.and.returnValue(state.pipe(take(1)));
corpusService.annisResponse = undefined;
corpusService.checkAnnisResponse().then(() => {
}, () => {
expect(corpusService.annisResponse).toBeFalsy();
helperService.applicationState.next(new ApplicationState({mostRecentSetup: new TextData()}));
state.next(new ApplicationState({mostRecentSetup: new TextData()}));
stateSpy.and.returnValue(state.pipe(take(1)));