diff --git a/docker-compose.yml b/docker-compose.yml index 96f7fac55e9b33e8ff59edeb919324846205ee23..35c34c729868928dfaa96f1566effde345b36b6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: db: - image: postgres:12-alpine + image: postgres:16-alpine environment: - POSTGRES_HOST_AUTH_METHOD=trust ports: diff --git a/mc_backend/README.md b/mc_backend/README.md index 8e3780cef8a9c18ae76da6f3d30655e24dce01b4..c1eb7bf0a082633c88a2a9c3e4f4b3d321a5fae5 100644 --- a/mc_backend/README.md +++ b/mc_backend/README.md @@ -36,10 +36,6 @@ To autogenerate a new migration script: ---------------------------------------------------------------- -# Models -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 ./mcserver/mcserver_api.yaml -g python-flask -o ./openapi/ && python openapi_generator.py`. # Testing To check the coverage of the current tests, run `coverage run --rcfile=.coveragerc tests.py && coverage combine && coverage report -m`. \ No newline at end of file diff --git a/mc_backend/app.py b/mc_backend/app.py index 3fb9c8fe5a38216a8063ccdaaf76ef4e34acb896..80aab6471394affd0932f576279f2482139b7c33 100644 --- a/mc_backend/app.py +++ b/mc_backend/app.py @@ -1,8 +1,11 @@ -from flask import Flask +from pathlib import Path + +import connexion from mcserver import get_app, get_cfg -app: Flask = get_app() +app: connexion.App = get_app() if __name__ == "__main__": - app.run(host=get_cfg().HOST_IP_MCSERVER, port=get_cfg().HOST_PORT, use_reloader=False) + app.run(import_string=f"{Path(__file__).stem}:app", host=get_cfg().HOST_IP_MCSERVER, port=get_cfg().HOST_PORT, + use_reloader=True) diff --git a/mc_backend/mcserver/__init__.py b/mc_backend/mcserver/__init__.py index de920a63ae8b5ccb86f3bb82ba13d88d9036e135..53b89e23185b784f346e91f475d82f3e822354b0 100644 --- a/mc_backend/mcserver/__init__.py +++ b/mc_backend/mcserver/__init__.py @@ -5,12 +5,12 @@ generating language exercises for them.""" import sys from typing import Type -from flask import Flask +import connexion from mcserver.config import Config, ProductionConfig, TestingConfig, DevelopmentConfig from mcserver.app import create_app -def get_app() -> Flask: +def get_app() -> connexion.App: return create_app(get_cfg()) diff --git a/mc_backend/mcserver/app/__init__.py b/mc_backend/mcserver/app/__init__.py index 7c2c683d4900bf824a4297bbcc121f7c64ac7927..7089358b66aec9b964e305399402ab9e816b89b2 100644 --- a/mc_backend/mcserver/app/__init__.py +++ b/mc_backend/mcserver/app/__init__.py @@ -12,8 +12,8 @@ import connexion import open_alchemy import prance from connexion import FlaskApp +from connexion.middleware import MiddlewarePosition from flask import Flask, got_request_exception, request, Response, send_from_directory -from flask_cors import CORS from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy from graphannis.cs import CorpusStorageManager @@ -22,6 +22,7 @@ from sqlalchemy import create_engine from sqlalchemy.engine import Connection, Engine from sqlalchemy.orm import Session, sessionmaker from sqlalchemy_utils import database_exists +from starlette.middleware.cors import CORSMiddleware from mcserver.config import Config @@ -46,7 +47,7 @@ def apply_event_handlers(app: FlaskApp): app.app.teardown_appcontext(shutdown_session) -def create_app(cfg: Type[Config] = Config) -> Flask: +def create_app(cfg: Type[Config] = Config) -> connexion.App: """Create a new Flask application and configure it. Initialize the application and the database. Arguments: cfg -- the desired configuration class for the application @@ -58,12 +59,12 @@ def create_app(cfg: Type[Config] = Config) -> Flask: Config.GRAPH_DATABASE_DIR = os.path.join(Config.GRAPH_DATABASE_DIR_BASE, guid) Config.GRAPHANNIS_LOG_PATH = os.path.join(Config.LOGS_DIRECTORY, f"graphannis_{guid}.log") Config.CORPUS_STORAGE_MANAGER = CorpusStorageManager(Config.GRAPH_DATABASE_DIR) - app: Flask = init_app_common(cfg=cfg) + connexion_app: connexion.App = init_app_common(cfg=cfg) from mcserver.app.services import bp as services_bp - app.register_blueprint(services_bp) + connexion_app.app.register_blueprint(services_bp) from mcserver.app.api import bp as api_bp - app.register_blueprint(api_bp) - return app + connexion_app.app.register_blueprint(api_bp) + return connexion_app def create_database_handler() -> SQLAlchemy: @@ -119,16 +120,22 @@ def get_api_specification() -> dict: return parser.specification -def init_app_common(cfg: Type[Config] = Config) -> Flask: - """ Initializes common Flask parts, e.g. CORS, configuration, database, migrations and custom corpora.""" - connexion_app: FlaskApp = connexion.FlaskApp( - __name__, port=cfg.HOST_PORT, specification_dir=Config.MC_SERVER_DIRECTORY) +def init_app_common(cfg: Type[Config] = Config) -> connexion.App: + """ Initializes common Flask parts, e.g., CORS, configuration, database, migrations and custom corpora.""" + connexion_app: connexion.FlaskApp = connexion.FlaskApp(__name__) # , specification_dir=Config.MC_SERVER_DIRECTORY spec: dict = get_api_specification() connexion_app.add_api(spec) apply_event_handlers(connexion_app) app: Flask = connexion_app.app # allow CORS requests for all API routes - CORS(app) # , resources=r"/*" + connexion_app.add_middleware( + CORSMiddleware, + position=MiddlewarePosition.BEFORE_EXCEPTION, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) app.config.from_object(cfg) app.app_context().push() init_logging(app, Config.LOG_PATH_MCSERVER) @@ -150,7 +157,7 @@ def init_app_common(cfg: Type[Config] = Config) -> Flask: TextService.init_stop_words_latin() if not cfg.TESTING: full_init(app, cfg) - return app + return connexion_app def init_logging(app: Flask, log_file_path: str): diff --git a/mc_backend/mcserver/app/api/exerciseAPI.py b/mc_backend/mcserver/app/api/exerciseAPI.py index 006d1b834c44600ead7e9766336ba3ad98abf4cc..afb470dc9a92cdc7f342b1f0215985c0fc8eb033 100644 --- a/mc_backend/mcserver/app/api/exerciseAPI.py +++ b/mc_backend/mcserver/app/api/exerciseAPI.py @@ -109,10 +109,10 @@ def map_exercise_data_to_database(exercise_data: ExerciseData, exercise_type: st return new_exercise -def post(exercise_data: dict) -> Union[Response, ConnexionResponse]: +def post(body: dict) -> Union[Response, ConnexionResponse]: """ The POST method for the Exercise REST API. It creates a new exercise from the given data and stores it in the database. """ - ef: ExerciseForm = ExerciseForm.from_dict(exercise_data) + ef: ExerciseForm = ExerciseForm.from_dict(body) ef.urn = ef.urn if ef.urn else "" exercise_type: ExerciseType = ExerciseType(ef.type) search_values_list: List[str] = json.loads(ef.search_values) diff --git a/mc_backend/mcserver/app/api/fileAPI.py b/mc_backend/mcserver/app/api/fileAPI.py index 28d8722ba19d60bbb053bd76dc09b70b1884d9bd..5d5c1a3abfc96ca5684c5fe6db57b20b6d41dd62 100644 --- a/mc_backend/mcserver/app/api/fileAPI.py +++ b/mc_backend/mcserver/app/api/fileAPI.py @@ -61,11 +61,11 @@ def get(id: str, type: FileType, solution_indices: List[int]) -> Union[Response, return send_from_directory(Config.TMP_DIRECTORY, existing_file.file_name, mimetype=mime_type, as_attachment=True) -def post(file_data: dict) -> Response: +def post(body: dict) -> Response: """ The POST method for the File REST API. It writes learning results or HTML content to the disk for later access. """ - ff: FileForm = FileForm.from_dict(file_data) + ff: FileForm = FileForm.from_dict(body) if ff.learning_result: lr_dict: dict = json.loads(ff.learning_result) for exercise_id in lr_dict: diff --git a/mc_backend/mcserver/app/api/h5pAPI.py b/mc_backend/mcserver/app/api/h5pAPI.py index 75c5147053239731d916870d80c362ebafb9904e..88b6453688e89ad06829249bba2e42555bc211e2 100644 --- a/mc_backend/mcserver/app/api/h5pAPI.py +++ b/mc_backend/mcserver/app/api/h5pAPI.py @@ -126,9 +126,9 @@ def make_h5p_archive(file_name_no_ext: str, response_dict: dict, target_dir: str shutil.rmtree(content_dir) -def post(h5p_data: dict): +def post(body: dict): """ The POST method for the H5P REST API. It offers client-side H5P exercises for download as ZIP archives. """ - h5p_form: H5PForm = H5PForm.from_dict(h5p_data) + h5p_form: H5PForm = H5PForm.from_dict(body) language: Language = determine_language(h5p_form.lang) exercise: Exercise = DatabaseService.query(Exercise, filter_by=dict(eid=h5p_form.eid), first=True) if not exercise: diff --git a/mc_backend/mcserver/app/api/kwicAPI.py b/mc_backend/mcserver/app/api/kwicAPI.py index f527204c74ce70ef475590f109723e7136050465..427bc2790efa84ccee58d937c582657da388b774 100644 --- a/mc_backend/mcserver/app/api/kwicAPI.py +++ b/mc_backend/mcserver/app/api/kwicAPI.py @@ -16,10 +16,10 @@ from openapi.openapi_server.models import GraphData, Solution from openapi.openapi_server.models.kwic_form import KwicForm -def post(kwic_data: dict) -> Response: +def post(body: dict) -> Response: """ The POST method for the KWIC REST API. It provides example contexts for a given phenomenon in a given corpus. """ - kwic_form: KwicForm = KwicForm.from_dict(kwic_data) + kwic_form: KwicForm = KwicForm.from_dict(body) search_values_list: List[str] = json.loads(kwic_form.search_values) aqls: List[str] = AnnotationService.map_search_values_to_aql(search_values_list, ExerciseType.kwic) disk_urn: str = AnnotationService.get_disk_urn(kwic_form.urn) diff --git a/mc_backend/mcserver/app/api/rawTextAPI.py b/mc_backend/mcserver/app/api/rawTextAPI.py index b0677010b213fbc34323cb739f01b3fb951ccd90..830f34858ffdc92e3969cd6bcb821c1f3c359f22 100644 --- a/mc_backend/mcserver/app/api/rawTextAPI.py +++ b/mc_backend/mcserver/app/api/rawTextAPI.py @@ -21,9 +21,9 @@ def get(urn: str) -> Union[Response, ConnexionResponse]: return NetworkService.make_json_response(ar.to_dict()) -def post(raw_text_data: dict) -> Union[Response, ConnexionResponse]: +def post(body: dict) -> Union[Response, ConnexionResponse]: """ Provides annotations and text complexity for arbitrary Latin texts. """ - rtf: RawTextFormExtension = RawTextFormExtension.from_dict(raw_text_data) + rtf: RawTextFormExtension = RawTextFormExtension.from_dict(body) text_with_extra_whitespaces: str = TextService.insert_whitespace_before_punctuation(rtf.plain_text) annotations_conll: str = AnnotationService.get_udpipe(text_with_extra_whitespaces) # parse CONLL and add root dependencies as separate node annotations diff --git a/mc_backend/mcserver/app/api/vectorNetworkAPI.py b/mc_backend/mcserver/app/api/vectorNetworkAPI.py index 4fee4a78cdebafab1755e0b434f096b862e09d5b..8180e28c8fb1f9eb7e6338e913d8bfcdc736e336 100644 --- a/mc_backend/mcserver/app/api/vectorNetworkAPI.py +++ b/mc_backend/mcserver/app/api/vectorNetworkAPI.py @@ -76,11 +76,11 @@ def get_concept_network(search_regex_string: str, min_count: int = 1, highlight_ return svg_string -def post(network_data: dict) -> Response: +def post(body: dict) -> Response: """ The POST method for the vector network REST API. It provides sentences whose content is similar to a given word. """ - vnf: VectorNetworkForm = VectorNetworkForm.from_dict(network_data) + vnf: VectorNetworkForm = VectorNetworkForm.from_dict(body) nearest_neighbor_count = vnf.nearest_neighbor_count if vnf.nearest_neighbor_count else 10 w2v: Word2Vec = Word2Vec.load(Config.PANEGYRICI_LATINI_MODEL_PATH) search_regex: Pattern[str] = re.compile(vnf.search_regex) diff --git a/mc_backend/mcserver/app/api/vocabularyAPI.py b/mc_backend/mcserver/app/api/vocabularyAPI.py index f7f2dd39a2bd63d5570289e67a88dbbe3e4ceec2..381baf28b2bb2c0af3809589f63b7eda69d712eb 100644 --- a/mc_backend/mcserver/app/api/vocabularyAPI.py +++ b/mc_backend/mcserver/app/api/vocabularyAPI.py @@ -55,9 +55,9 @@ def get(frequency_upper_bound: int, query_urn: str, vocabulary: str) -> Union[Re return NetworkService.make_json_response([x.to_dict() for x in sentences]) -def post(vocabulary_data: dict): +def post(body: dict): """ Indicates for each token of a corpus whether it is covered by a reference vocabulary. """ - vf: VocabularyForm = VocabularyForm.from_dict(vocabulary_data) + vf: VocabularyForm = VocabularyForm.from_dict(body) vc: VocabularyCorpus = VocabularyCorpus[vf.vocabulary.__str__()] vocabulary_set: Set[str] = FileService.get_vocabulary_set(vc, vf.frequency_upper_bound) # punctuation should count as a match because we don't want to count this as part of the vocabulary diff --git a/mc_backend/mcserver/app/api/zenodoAPI.py b/mc_backend/mcserver/app/api/zenodoAPI.py index 0cd951c678743216ef4de3400d64c24a69c4e316..ce2abfd5fc0f835b367d1b9ad58cbd89f6ef0665 100644 --- a/mc_backend/mcserver/app/api/zenodoAPI.py +++ b/mc_backend/mcserver/app/api/zenodoAPI.py @@ -40,9 +40,9 @@ def get() -> Response: return NetworkService.make_json_response([x.to_dict() for x in zenodo_records]) -def post(zenodo_data: dict) -> Response: +def post(body: dict) -> Response: """The POST method for the Zenodo REST API. It provides file URIs for exercises from the Zenodo repository.""" - zenodo_form: ZenodoForm = ZenodoForm.from_dict(zenodo_data) + zenodo_form: ZenodoForm = ZenodoForm.from_dict(body) sickle: Sickle = Sickle(Config.ZENODO_API_URL) record: Record = sickle.GetRecord(metadataPrefix=ZenodoMetadataPrefix.MARC21, identifier=f"oai:zenodo.org:{zenodo_form.record_id}") diff --git a/mc_backend/mcserver/app/models.py b/mc_backend/mcserver/app/models.py index bad26947dedd9add26b2ac7c7122be571ebf5f78..027d2376c177bef6c46865b3c8254da4109a88db 100644 --- a/mc_backend/mcserver/app/models.py +++ b/mc_backend/mcserver/app/models.py @@ -169,18 +169,17 @@ class CorpusMC: @classmethod def from_dict(cls, + cid: int, source_urn: str, author: str = "Anonymus", - cid: typing.Optional[int] = None, citation_level_1: str = "default", citation_level_2: str = "default", citation_level_3: str = "default", title: str = "Anonymus", ) -> TCorpus: - # ignore CID (corpus ID) because it is going to be generated automatically return Corpus.from_dict( source_urn=source_urn, author=author, citation_level_1=citation_level_1, - citation_level_2=citation_level_2, citation_level_3=citation_level_3, title=title) + citation_level_2=citation_level_2, citation_level_3=citation_level_3, title=title, cid=cid) class ExerciseMC: diff --git a/mc_backend/mcserver/app/services/annotationService.py b/mc_backend/mcserver/app/services/annotationService.py index cf06fd6541cba3cd8942ce0ae083f771bb4399cc..e7cf6de68f55612399be66bf95b55701898361d4 100644 --- a/mc_backend/mcserver/app/services/annotationService.py +++ b/mc_backend/mcserver/app/services/annotationService.py @@ -82,8 +82,7 @@ class AnnotationService: Phenomenon.LEMMA: {}} @staticmethod - def add_annotations_to_graph( - conll: List[TokenList], g: GraphUpdate, doc_name: str, doc_path: str) -> None: + def add_annotations_to_graph(conll: List[TokenList], g: GraphUpdate, doc_name: str, doc_path: str) -> None: """ Adds new annotations (provided in CONLL-U format) to a networkx graph. """ current_urn: str = "" tok_before: str = "" @@ -304,7 +303,7 @@ class AnnotationService: return aqls @staticmethod - def map_token(tok, tok_id: str, g: GraphUpdate): + def map_token(tok: dict, tok_id: str, g: GraphUpdate): """ Maps a token's annotations to the graphANNIS format. """ g.add_node(tok_id) # this is an annotation node @@ -347,13 +346,13 @@ class AnnotationService: node_id_set: Set[str] = set([x.id for x in graph_data.nodes]) # create set for ordering link targets target_set: Set[str] = set(map(lambda x: x.target, ordering_links)) - # look for the node of the first token in the text, i.e. the one that is not a target of any ordering link + # look for the node of the first token in the text, i.e., the one that is not a target of any ordering link source_node_id: str = list(node_id_set ^ target_set)[0] - # create a copy of the nodes so wen can change their order and then add them back to the graph + # create a copy of the nodes, so we can change their order and then add them back to the graph nodes: List[NodeMC] = graph_data.nodes # clear the previous node order graph_data.nodes = [] - # create a lookup table so we can retrieve ordering links easily by their source value / node id + # create a lookup table, so we can retrieve ordering links easily by their source value / node id ordering_link_source_to_index_dict: Dict[str, int] = {} for i in range(len(ordering_links)): ordering_link_source_to_index_dict[ordering_links[i].source] = i diff --git a/mc_backend/mcserver/app/services/corpusService.py b/mc_backend/mcserver/app/services/corpusService.py index 6f6dfff9011be22755d8afceb4e376f95e1cfe44..9407fc19d1b9c41c7d2ae0245bc317e9e5caf45c 100644 --- a/mc_backend/mcserver/app/services/corpusService.py +++ b/mc_backend/mcserver/app/services/corpusService.py @@ -18,10 +18,12 @@ from sqlalchemy.exc import OperationalError from mcserver.app import db from mcserver.app.models import CitationLevel, GraphData, Solution, ExerciseType, Phenomenon, AnnisResponse, CorpusMC, \ make_solution_element_from_salt_id, FrequencyItem, ResourceType, ReferenceableText -from mcserver.app.services import AnnotationService, XMLservice, TextService, FileService, FrequencyService, \ +from mcserver.app.services import AnnotationService, XMLservice, FileService, FrequencyService, \ CustomCorpusService, DatabaseService from mcserver.config import Config -from mcserver.models_auto import Corpus, UpdateInfo +from mcserver.models_auto import Corpus, UpdateInfo, TCorpus +from sqlalchemy import select +from sqlalchemy.sql import functions class CorpusService: @@ -43,15 +45,15 @@ class CorpusService: def add_corpus(title_value: str, urn: str, group_name_value: str, citation_levels: List[Union[CitationLevel, str]]) -> None: """Adds a new corpus to the database.""" - new_corpus = CorpusMC.from_dict(title=title_value, source_urn=urn, author=group_name_value, - citation_level_1=citation_levels[0]) + # set the max_cid to 0 if there are no corpora in the table + max_cid: int = db.session.execute(select(functions.max(Corpus.cid))).first()[0] or 0 + new_corpus: TCorpus = CorpusMC.from_dict(title=title_value, source_urn=urn, author=group_name_value, + citation_level_1=citation_levels[0], cid=max_cid + 1) CorpusService.add_citation_levels(new_corpus, citation_levels) - db.session.add(new_corpus) - # need to commit once so the Corpus ID (cid) gets generated by the database - db.session.commit() # now we can build the URI from the Corpus ID new_corpus.uri = "/{0}".format(new_corpus.cid) - db.session.commit() + db.session.add(new_corpus) + DatabaseService.commit() @staticmethod def check_corpus_list_age(app: Flask) -> None: @@ -323,7 +325,7 @@ class CorpusService: def load_text_list(cts_urn_raw: str) -> List[ReferenceableText]: """ Loads the text list for a new corpus. """ if CustomCorpusService.is_custom_corpus_urn(cts_urn_raw): - # this is a custom corpus, e.g. the VIVA textbook + # this is a custom corpus, e.g., the VIVA textbook return CustomCorpusService.get_custom_corpus_text(cts_urn_raw) else: resolver: HttpCtsRetriever = HttpCtsRetriever(Config.CTS_API_BASE_URL) @@ -398,9 +400,9 @@ class CorpusService: if urn not in urn_set_new: corpus_to_delete: Corpus = DatabaseService.query(Corpus, dict(source_urn=urn), True) db.session.delete(corpus_to_delete) - db.session.commit() + DatabaseService.commit() CorpusService.existing_corpora = DatabaseService.query(Corpus) - db.session.commit() + DatabaseService.commit() @staticmethod def update_corpus(title_value: str, urn: str, author: str, @@ -416,4 +418,4 @@ class CorpusService: corpus_to_update.citation_level_1 = citation_levels[0] corpus_to_update.source_urn = urn CorpusService.add_citation_levels(corpus_to_update, citation_levels) - db.session.commit() + DatabaseService.commit() diff --git a/mc_backend/mcserver/app/services/customCorpusService.py b/mc_backend/mcserver/app/services/customCorpusService.py index 15b5dde2953b60d2a7db0713d0c993e04b0d5775..6a9b80170089c34c111f9baf6052899e364291ad 100644 --- a/mc_backend/mcserver/app/services/customCorpusService.py +++ b/mc_backend/mcserver/app/services/customCorpusService.py @@ -17,51 +17,51 @@ class CustomCorpusService: custom_corpora: List[CustomCorpus] = [ CustomCorpus(corpus=CorpusMC.from_dict( title="Commentarii de bello Gallico", source_urn=Config.CUSTOM_CORPUS_PROIEL_URN_TEMPLATE.format( - "caes-gal"), author="C. Iulius Caesar", citation_level_1=CitationLevel.book.value, + "caes-gal"), author="C. Iulius Caesar", cid=-1, citation_level_1=CitationLevel.book.value, citation_level_2=CitationLevel.chapter.value, citation_level_3=CitationLevel.section.value), file_path=Config.CUSTOM_CORPUS_CAES_GAL_FILE_PATH), CustomCorpus(corpus=CorpusMC.from_dict( title="Epistulae ad Atticum", source_urn=Config.CUSTOM_CORPUS_PROIEL_URN_TEMPLATE.format( - "cic-att"), author="M. Tullius Cicero", citation_level_1=CitationLevel.book.value, + "cic-att"), author="M. Tullius Cicero", cid=-2, citation_level_1=CitationLevel.book.value, citation_level_2=CitationLevel.letter.value, citation_level_3=CitationLevel.section.value), file_path=Config.CUSTOM_CORPUS_CIC_ATT_FILE_PATH), CustomCorpus(corpus=CorpusMC.from_dict( title="De officiis", source_urn=Config.CUSTOM_CORPUS_PROIEL_URN_TEMPLATE.format( - "cic-off"), author="M. Tullius Cicero", citation_level_1=CitationLevel.book.value, + "cic-off"), author="M. Tullius Cicero", cid=-3, citation_level_1=CitationLevel.book.value, citation_level_2=CitationLevel.section.value, citation_level_3=CitationLevel.default.value), file_path=Config.CUSTOM_CORPUS_CIC_OFF_FILE_PATH), CustomCorpus(corpus=CorpusMC.from_dict( title="Vulgata (Novum Testamentum)", source_urn=Config.CUSTOM_CORPUS_PROIEL_URN_TEMPLATE.format( - "latin-nt"), author="Hieronymus", citation_level_1=CitationLevel.chapter.value, + "latin-nt"), author="Hieronymus", cid=-4, citation_level_1=CitationLevel.chapter.value, citation_level_2=CitationLevel.section.value, citation_level_3=CitationLevel.default.value), file_path=Config.CUSTOM_CORPUS_LATIN_NT_FILE_PATH), CustomCorpus(corpus=CorpusMC.from_dict( title="Opus Agriculturae", source_urn=Config.CUSTOM_CORPUS_PROIEL_URN_TEMPLATE.format( - "pal-agr"), author="Palladius", citation_level_1=CitationLevel.book.value, + "pal-agr"), author="Palladius", cid=-5, citation_level_1=CitationLevel.book.value, citation_level_2=CitationLevel.chapter.value, citation_level_3=CitationLevel.section.value), file_path=Config.CUSTOM_CORPUS_PAL_AGR_FILE_PATH), CustomCorpus(corpus=CorpusMC.from_dict( title="Peregrinatio Aetheriae", source_urn=Config.CUSTOM_CORPUS_PROIEL_URN_TEMPLATE.format( - "per-aeth"), author="Peregrinatio Aetheriae", citation_level_1=CitationLevel.chapter.value, + "per-aeth"), author="Peregrinatio Aetheriae", cid=-6, citation_level_1=CitationLevel.chapter.value, citation_level_2=CitationLevel.section.value, citation_level_3=CitationLevel.default.value), file_path=Config.CUSTOM_CORPUS_PER_AET_FILE_PATH), CustomCorpus(init_fn=InitializationService.init_custom_corpus_viva, corpus=CorpusMC.from_dict( - title="VIVA", source_urn=Config.CUSTOM_CORPUS_VIVA_URN, author="VIVA (textbook)", + title="VIVA", source_urn=Config.CUSTOM_CORPUS_VIVA_URN, author="VIVA (textbook)", cid=-7, citation_level_1=CitationLevel.book.value, citation_level_2=CitationLevel.unit.value, citation_level_3=CitationLevel.default.value), file_path=Config.CUSTOM_CORPUS_VIVA_FILE_PATH), CustomCorpus(init_fn=InitializationService.init_custom_corpus_pro_marcello, corpus=CorpusMC.from_dict( - title="Pro M. Marcello", source_urn=Config.CUSTOM_CORPUS_CIC_MARC_URN, author="M. Tullius Cicero", + title="Pro M. Marcello", source_urn=Config.CUSTOM_CORPUS_CIC_MARC_URN, author="M. Tullius Cicero", cid=-8, citation_level_1=CitationLevel.section.value, citation_level_2=CitationLevel.default.value, citation_level_3=CitationLevel.default.value), file_path=Config.CUSTOM_CORPUS_CIC_MARC_FILE_PATH), CustomCorpus(init_fn=InitializationService.init_custom_corpus_de_revolutionibus, corpus=CorpusMC.from_dict( title="De Revolutionibus Orbium Coelestium", - source_urn=Config.CUSTOM_CORPUS_COPERNICUS_DE_REVOLUTIONIBUS_URN, author="Nicolaus Copernicus", + source_urn=Config.CUSTOM_CORPUS_COPERNICUS_DE_REVOLUTIONIBUS_URN, author="Nicolaus Copernicus", cid=-9, citation_level_1=CitationLevel.book.value, citation_level_2=CitationLevel.chapter.value, citation_level_3=CitationLevel.default.value), file_path=Config.CUSTOM_CORPUS_COPERNICUS_DE_REVOLUTIONIBUS_FILE_PATH), CustomCorpus(init_fn=InitializationService.init_custom_corpus_commentariolus, corpus=CorpusMC.from_dict( title="Commentariolus", source_urn=Config.CUSTOM_CORPUS_COPERNICUS_COMMENTARIOLUS_URN, - author="Nicolaus Copernicus", citation_level_1=CitationLevel.chapter.value, + author="Nicolaus Copernicus", cid=-10, citation_level_1=CitationLevel.chapter.value, citation_level_2=CitationLevel.default.value, citation_level_3=CitationLevel.default.value), file_path=Config.CUSTOM_CORPUS_COPERNICUS_COMMENTARIOLUS_FILE_PATH), ] diff --git a/mc_backend/mcserver/app/services/databaseService.py b/mc_backend/mcserver/app/services/databaseService.py index 2cd4a6c01fa7eaae7b1d9709cc97c808f9760424..2deb89c0bf6a28f2c177183e932ee25ac9166aab 100644 --- a/mc_backend/mcserver/app/services/databaseService.py +++ b/mc_backend/mcserver/app/services/databaseService.py @@ -22,6 +22,12 @@ class DatabaseService: db.session.rollback() raise + @staticmethod + def delete(table: Union[Corpus, Exercise, LearningResult, UpdateInfo]): + """Deletes all rows from a table in the database.""" + db.session.query(table).delete() + DatabaseService.commit() + @staticmethod def has_table(table: str) -> bool: """Checks if a table is present in the database or not.""" diff --git a/mc_backend/mcserver/config.py b/mc_backend/mcserver/config.py index 2a53a0d530767cd042ae4956fa6d59292b38214c..ec8de52906867b78a5a7bafb9e783735dd0fae9a 100644 --- a/mc_backend/mcserver/config.py +++ b/mc_backend/mcserver/config.py @@ -205,7 +205,8 @@ class TestingConfig(Config): CTS_API_GET_PASSAGE_URL = "https://cts.perseids.org/api/cts/?request=GetPassage&urn=urn:cts:latinLit:phi1351.phi002.perseus-lat1:2.2" HOST_IP_MCSERVER = "0.0.0.0" PRESERVE_CONTEXT_ON_EXCEPTION = False - SERVER_NAME = Config.HOST_IP_MCSERVER + ":{0}".format(Config.HOST_PORT) + # reuse the server name from Starlette's TestClient implementation + SERVER_NAME = "testserver" SESSION_COOKIE_DOMAIN = False SIMULATE_HTTP_ERROR = False SQLALCHEMY_DATABASE_URI = Config.DATABASE_URL_SQLITE diff --git a/mc_backend/mcserver/models_auto.py b/mc_backend/mcserver/models_auto.py index de9a6a29d6d1d8b4aa59805eb9d9624db9185764..2a4e37cd6d66850ec604033cd7a0d5c62bc127d1 100644 --- a/mc_backend/mcserver/models_auto.py +++ b/mc_backend/mcserver/models_auto.py @@ -14,6 +14,7 @@ Base = models.Base # type: ignore class _CorpusDictBase(typing.TypedDict, total=True): """TypedDict for properties that are required.""" + cid: int source_urn: str @@ -21,7 +22,6 @@ class CorpusDict(_CorpusDictBase, total=False): """TypedDict for properties that are not required.""" author: str - cid: int citation_level_1: str citation_level_2: str citation_level_3: str @@ -59,7 +59,7 @@ class TCorpus(typing.Protocol): source_urn: 'sqlalchemy.Column[str]' title: 'sqlalchemy.Column[str]' - def __init__(self, source_urn: str, author: str = "Anonymus", cid: typing.Optional[int] = None, citation_level_1: str = "default", citation_level_2: str = "default", citation_level_3: str = "default", title: str = "Anonymus") -> None: + def __init__(self, cid: int, source_urn: str, author: str = "Anonymus", citation_level_1: str = "default", citation_level_2: str = "default", citation_level_3: str = "default", title: str = "Anonymus") -> None: """ Construct. @@ -76,7 +76,7 @@ class TCorpus(typing.Protocol): ... @classmethod - def from_dict(cls, source_urn: str, author: str = "Anonymus", cid: typing.Optional[int] = None, citation_level_1: str = "default", citation_level_2: str = "default", citation_level_3: str = "default", title: str = "Anonymus") -> "TCorpus": + def from_dict(cls, cid: int, source_urn: str, author: str = "Anonymus", citation_level_1: str = "default", citation_level_2: str = "default", citation_level_3: str = "default", title: str = "Anonymus") -> "TCorpus": """ Construct from a dictionary (eg. a POST payload). diff --git a/mc_backend/mocks.py b/mc_backend/mocks.py index 2853c78300ab3a0bca31ac6ef61f72d5187e4100..bfb6135a034eda64e505c6918148ddede57731e0 100644 --- a/mc_backend/mocks.py +++ b/mc_backend/mocks.py @@ -1,3 +1,4 @@ +import asyncio import json import logging from collections import OrderedDict @@ -5,6 +6,7 @@ from datetime import datetime from typing import List, Tuple, Dict from unittest.mock import patch +import connexion from conllu import TokenList from flask import Flask from flask.ctx import AppContext @@ -14,6 +16,7 @@ from gensim.models.keyedvectors import Vocab from networkx import Graph from numpy.core.multiarray import ndarray from requests import HTTPError +from starlette.testclient import TestClient from mcserver import Config, TestingConfig from mcserver.app import db, shutdown_session @@ -74,12 +77,13 @@ class MockW2V: class TestHelper: - def __init__(self, app: Flask): - self.app: Flask = app - self.app_context: AppContext = self.app.app_context() + def __init__(self, app: connexion.App): + self.app: connexion.App = app + self.app_context: AppContext = self.app.app.app_context() self.app_context.push() - self.client: FlaskClient = self.app.test_client() - self.app.logger.setLevel(logging.WARNING) + # force Starlette/anyio to use the asyncio event loop that was patched by PyCharm + self.client: TestClient = self.app.test_client(backend_options={'loop_factory': asyncio.new_event_loop}) + self.app.app.logger.setLevel(logging.WARNING) self.app.testing = True @staticmethod @@ -695,9 +699,9 @@ class Mocks: annis_response: AnnisResponse = AnnisResponse(graph_data=graph_data) corpora: List[Corpus] = [ CorpusMC.from_dict(title="title1", source_urn="urn1", author="author1", - citation_level_1=CitationLevel.default.value), + citation_level_1=CitationLevel.default.value, cid=1), CorpusMC.from_dict(title="title2", source_urn="urn2", author="author2", - citation_level_1=CitationLevel.default.value)] + citation_level_1=CitationLevel.default.value, cid=2)] 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"><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>' diff --git a/mc_backend/openapi/openapi_server/models/corpus.py b/mc_backend/openapi/openapi_server/models/corpus.py index b9a8470b6b7323eb66e2015b0687d8351390d7ae..e748eb0f3bab5f7cae8e2c8bc084f001ef4617af 100644 --- a/mc_backend/openapi/openapi_server/models/corpus.py +++ b/mc_backend/openapi/openapi_server/models/corpus.py @@ -112,6 +112,8 @@ class Corpus(Model): :param cid: The cid of this Corpus. :type cid: int """ + if cid is None: + raise ValueError("Invalid value for `cid`, must not be `None`") # noqa: E501 self._cid = cid diff --git a/mc_backend/openapi/openapi_server/openapi/openapi.yaml b/mc_backend/openapi/openapi_server/openapi/openapi.yaml index bc979191e68c50a719fc48fb1e75d86d26178055..6d6749d845b595784f213fd36a422b8e4413128b 100644 --- a/mc_backend/openapi/openapi_server/openapi/openapi.yaml +++ b/mc_backend/openapi/openapi_server/openapi/openapi.yaml @@ -717,7 +717,6 @@ components: required: - plain_text type: object - x-body-name: raw_text_data ZenodoMetadataPrefixExtension: description: "Prefix for requests to the Zenodo API, indicating the desired\ \ metadata scheme." @@ -750,7 +749,7 @@ components: title: cid type: integer x-primary-key: true - x-autoincrement: true + x-autoincrement: false citation_level_1: default: default description: First level for citing the corpus. @@ -783,6 +782,7 @@ components: title: title type: string required: + - cid - source_urn title: Corpus type: object @@ -1327,7 +1327,6 @@ components: - urn type: object type: object - x-body-name: exercise_data ExerciseBase: description: Base data for creating and evaluating interactive exercises. properties: @@ -1512,7 +1511,6 @@ components: example: urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.1-1.1.1 type: string type: object - x-body-name: file_data H5PForm: description: Metadata for the H5P exercise. properties: @@ -1533,7 +1531,6 @@ components: type: integer type: array type: object - x-body-name: h5p_data ExerciseTypePath: description: Paths to the data directories for various H5P exercise types. enum: @@ -1576,7 +1573,6 @@ components: - search_values - urn type: object - x-body-name: kwic_data RawTextForm: description: Text data for an annotation request. properties: @@ -1587,7 +1583,6 @@ components: required: - plain_text type: object - x-body-name: raw_text_data StaticExercise: description: Metadata for a static exercise. properties: @@ -1631,7 +1626,6 @@ components: required: - search_regex type: object - x-body-name: network_data Sentence: description: Sentence with metadata example: @@ -1670,7 +1664,6 @@ components: - query_urn - vocabulary type: object - x-body-name: vocabulary_data ZenodoRecord: description: Record with header and metadata from Zenodo. example: @@ -1789,7 +1782,6 @@ components: required: - record_id type: object - x-body-name: zenodo_data ExerciseAuthor: description: Authors of curated exercises in the Machina Callida. enum: diff --git a/mc_backend/openapi_models.yaml b/mc_backend/openapi_models.yaml index 90f1e05fe5ff0bfd48d60da27ca7a7844bd21f13..092d64758f57e97c28ad822bb2881cd93934d2db 100644 --- a/mc_backend/openapi_models.yaml +++ b/mc_backend/openapi_models.yaml @@ -95,7 +95,7 @@ components: description: Unique identifier for the corpus. example: 1 x-primary-key: true - x-autoincrement: true + x-autoincrement: false citation_level_1: type: string description: First level for citing the corpus. @@ -123,6 +123,7 @@ components: nullable: false default: Anonymus required: + - cid - source_urn Exercise: allOf: @@ -243,7 +244,6 @@ components: - eid - last_access_time ExerciseForm: - x-body-name: exercise_data type: object allOf: - $ref: '#/components/schemas/ExerciseBase' @@ -272,7 +272,6 @@ components: description: Paths to the data directories for various H5P exercise types. example: drag_text FileForm: - x-body-name: file_data description: Metadata for file content to be saved for later access. type: object properties: @@ -348,7 +347,6 @@ components: H5PForm: type: object description: Metadata for the H5P exercise. - x-body-name: h5p_data properties: eid: type: string @@ -367,7 +365,6 @@ components: example: 0 description: Indices for the solutions that should be included in the download. KwicForm: - x-body-name: kwic_data type: object description: Relevant parameters for creating a Keyword In Context view. properties: @@ -610,7 +607,6 @@ components: description: "Linguistic phenomena: syntactic dependencies, morphological features, lemmata, parts of speech." example: upostag RawTextForm: - x-body-name: raw_text_data description: Text data for an annotation request. properties: plain_text: @@ -784,7 +780,6 @@ components: - last_modified_time - resource_type VectorNetworkForm: - x-body-name: network_data type: object description: Relevant parameters for finding sentences that are similar to a target word. properties: @@ -801,7 +796,6 @@ components: PROIEL treebank, Vischer Wortkunde, VIVA textbook" example: agldt VocabularyForm: - x-body-name: vocabulary_data type: object description: Relevant parameters for comparing a corpus to a reference vocabulary. properties: @@ -820,7 +814,6 @@ components: - query_urn - vocabulary ZenodoForm: - x-body-name: zenodo_data type: object description: Relevant parameters for accessing file URIs in a Zenodo record. properties: diff --git a/mc_backend/requirements.txt b/mc_backend/requirements.txt index 22df72c82131d506c67746c9cbe911ac1ee47016..f0ebac82cfe36cf4e67883d90acda71764fe6e7f 100644 --- a/mc_backend/requirements.txt +++ b/mc_backend/requirements.txt @@ -1,39 +1,51 @@ -alembic==1.4.2 +a2wsgi==1.10.0 +alembic==1.13.1 aniso8601==9.0.1 -attrs==20.3.0 +anyio==4.2.0 +asgiref==3.7.2 +attrs==23.2.0 beautifulsoup4==4.9.0 blinker==1.6.3 cachetools==4.2.1 certifi==2020.12.5 cffi==1.14.5 chardet==3.0.4 +charset-normalizer==3.3.2 click==8.1.7 clickclick==20.10.2 conllu==2.3.2 -connexion==2.14.2 +connexion==3.0.6 coverage==5.5 cycler==0.10.0 decorator==4.4.2 -Flask==2.2.5 -Flask-Cors==3.0.10 -Flask-Migrate==2.7.0 +exceptiongroup==1.2.0 +Flask==3.0.1 +Flask-Migrate==4.0.5 Flask-RESTful==0.3.10 -Flask-SQLAlchemy==2.5.1 +Flask-SQLAlchemy==3.1.1 frozendict==1.2 future==0.18.2 gensim==3.8.2 graphannis==0.27.0 greenlet==3.0.1 gunicorn==20.0.4 +h11==0.14.0 html5lib==1.1 +httpcore==1.0.2 +httptools==0.6.1 +httpx==0.26.0 idna==2.10 importlib-metadata==6.8.0 +importlib-resources==5.13.0 inflection==0.5.1 isodate==0.6.0 itsdangerous==2.1.2 Jinja2==3.1.2 -jsonschema==3.2.0 +jsonschema==4.17.3 +jsonschema-spec==0.1.6 +jsonschema-specifications==2023.12.1 kiwisolver==1.3.1 +lazy-object-proxy==1.10.0 LinkHeader==0.4.3 lxml==4.6.2 Mako==1.1.4 @@ -44,12 +56,14 @@ mypy==0.812 mypy-extensions==0.4.3 networkx==3.1 numpy==1.24.4 -openalchemy==2.5.0 -openapi-schema-validator==0.1.4 -openapi-spec-validator==0.3.0 +OpenAlchemy==2.0.1 +openapi-schema-validator==0.4.4 +openapi-spec-validator==0.5.7 packaging==23.2 +pathable==0.4.3 Pillow==8.1.2 -prance==0.18.2 +pkgutil_resolve_name==1.3.10 +prance==23.6.21.0 psycopg2-binary==2.8.6 pycparser==2.20 PyLD==2.0.3 @@ -60,28 +74,40 @@ python-dateutil==2.8.1 python-docx==0.8.10 python-dotenv==0.13.0 python-editor==1.0.4 +python-multipart==0.0.6 python-rapidjson==1.0 pytz==2021.1 PyYAML==5.4.1 rdflib==6.0.1 rdflib-jsonld==0.6.2 +referencing==0.33.0 reportlab==3.5.63 -requests==2.23.0 +requests==2.31.0 +rfc3339-validator==0.1.4 +rpds-py==0.17.1 +ruamel.yaml==0.18.5 +ruamel.yaml.clib==0.2.8 scipy==1.6.1 semver==2.13.0 Sickle==0.7.0 -six==1.14.0 +six==1.16.0 smart-open==6.3.0 +sniffio==1.3.0 soupsieve==2.2 -SQLAlchemy==1.4.49 +SQLAlchemy==2.0.25 sqlalchemy-stubs==0.4 SQLAlchemy-Utils==0.41.1 -swagger-ui-bundle==0.0.8 +starlette==0.37.1 +swagger_ui_bundle==1.1.0 typed-ast==1.4.2 typing==3.7.4.1 typing_extensions==4.8.0 urllib3==1.25.11 +uvicorn==0.27.1 +uvloop==0.19.0 +watchfiles==0.21.0 webencodings==0.5.1 -Werkzeug==2.2.3 +websockets==12.0 +Werkzeug==3.0.1 xhtml2pdf==0.2.4 zipp==3.17.0 diff --git a/mc_backend/tests.py b/mc_backend/tests.py index d47f71d76fc49a9b8487b9df76f2c0d9e9999566..c89a64a398c8d1e6bab5c982ba3a307e79857935 100644 --- a/mc_backend/tests.py +++ b/mc_backend/tests.py @@ -17,18 +17,21 @@ from unittest.mock import patch, MagicMock, mock_open from zipfile import ZipFile import MyCapytain +import connexion +import flask import rapidjson as json +import sqlalchemy from conllu import TokenList from flask import Flask from gensim.models import Word2Vec from graphannis.errors import NoSuchCorpus +from httpx import Response from lxml import etree from networkx import MultiDiGraph, Graph from requests import HTTPError from sqlalchemy import inspect from sqlalchemy.exc import OperationalError, InvalidRequestError from sqlalchemy.orm import session -from werkzeug.wrappers import Response import mcserver from mcserver.app import create_app, db, start_updater, full_init, log_exception, get_api_specification, \ @@ -86,16 +89,15 @@ class APItestCase(unittest.TestCase): def test_api_corpus_delete(self): """ Deletes a single corpus. """ - db.session.query(Corpus).delete() - response: Response = Mocks.app_dict[self.class_name].client.delete( - f"{Config.SERVER_URI_CORPORA}/1") + DatabaseService.delete(Corpus) + response: Response = Mocks.app_dict[self.class_name].client.delete(f"{Config.SERVER_URI_CORPORA}/1") self.assertEqual(response.status_code, 404) TestHelper.add_corpus(Mocks.corpora[0]) response = Mocks.app_dict[self.class_name].client.delete(f"{Config.SERVER_URI_CORPORA}/{Mocks.corpora[0].cid}") - data_json: dict = json.loads(response.get_data()) + data_json: bool = response.json() self.assertEqual(data_json, True) - db.session.query(Corpus).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Corpus) + DatabaseService.delete(UpdateInfo) # dirty hack so we can reuse it in other tests session.make_transient(Mocks.corpora[0]) @@ -105,13 +107,12 @@ class APItestCase(unittest.TestCase): f"{Config.SERVER_URI_CORPORA}/{Mocks.corpora[0].cid}") self.assertEqual(response.status_code, 404) TestHelper.add_corpus(Mocks.corpora[0]) - response: Response = Mocks.app_dict[self.class_name].client.get( - f"{Config.SERVER_URI_CORPORA}/{Mocks.corpora[0].cid}") - data_json: dict = json.loads(response.get_data()) + response = Mocks.app_dict[self.class_name].client.get(f"{Config.SERVER_URI_CORPORA}/{Mocks.corpora[0].cid}") + data_json: dict = response.json() old_dict: dict = Mocks.corpora[0].to_dict() self.assertEqual(data_json, old_dict) - db.session.query(Corpus).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Corpus) + DatabaseService.delete(UpdateInfo) # dirty hack so we can reuse it in other tests session.make_transient(Mocks.corpora[0]) @@ -124,8 +125,8 @@ class APItestCase(unittest.TestCase): last_modified_time=lmt.timestamp(), created_time=1.0) mock.session.query.return_value = MockQuery(ui) response: Response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_CORPORA, query_string=dict(last_update_time=lut)) - data_json = json.loads(response.get_data()) + TestingConfig.SERVER_URI_CORPORA, params=dict(last_update_time=lut)) + data_json = response.json() if data_json: result = [x.to_dict() for x in result] self.assertEqual(data_json, result) @@ -136,8 +137,8 @@ class APItestCase(unittest.TestCase): db.session.add_all(Mocks.corpora) DatabaseService.commit() expect_result(self, mock_db, "0", Mocks.corpora, datetime.fromtimestamp(time.time())) - db.session.query(Corpus).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Corpus) + DatabaseService.delete(UpdateInfo) # dirty hack so we can reuse it in other tests session.make_transient(Mocks.corpora[0]) @@ -149,23 +150,23 @@ class APItestCase(unittest.TestCase): TestHelper.add_corpus(Mocks.corpora[0]) old_title: str = Mocks.corpora[0].title new_title: str = "new_title" - response: Response = Mocks.app_dict[self.class_name].client.patch( + response = Mocks.app_dict[self.class_name].client.patch( f"{Config.SERVER_URI_CORPORA}/{Mocks.corpora[0].cid}", data=dict(title=new_title)) - data_json: dict = json.loads(response.get_data()) + data_json: dict = response.json() old_dict: dict = Mocks.corpora[0].to_dict() self.assertEqual(data_json["title"], old_dict["title"]) Mocks.corpora[0].title = old_title - db.session.query(Corpus).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Corpus) + DatabaseService.delete(UpdateInfo) # dirty hack so we can reuse it in other tests session.make_transient(Mocks.corpora[0]) def test_api_exercise_get(self): """ Retrieves an existing exercise by its exercise ID. """ ar_copy: AnnisResponse = Mocks.copy(Mocks.annis_response, AnnisResponse) - db.session.query(Exercise).delete() + DatabaseService.delete(Exercise) response: Response = Mocks.app_dict[self.class_name].client.get( - Config.SERVER_URI_EXERCISE, query_string=dict(eid="")) + Config.SERVER_URI_EXERCISE, params=dict(eid="")) self.assertEqual(response.status_code, 404) old_urn: str = Mocks.exercise.urn Mocks.exercise.urn = "" @@ -174,15 +175,15 @@ class APItestCase(unittest.TestCase): mock_ar: AnnisResponse = AnnisResponse(solutions=[], graph_data=GraphData(links=[], nodes=[])) with patch.object(CorpusService, "get_corpus", side_effect=[mock_ar, ar_copy]): response = Mocks.app_dict[self.class_name].client.get(Config.SERVER_URI_EXERCISE, - query_string=dict(eid=Mocks.exercise.eid)) + params=dict(eid=Mocks.exercise.eid)) self.assertEqual(response.status_code, 404) Mocks.exercise.urn = old_urn DatabaseService.commit() response = Mocks.app_dict[self.class_name].client.get(Config.SERVER_URI_EXERCISE, - query_string=dict(eid=Mocks.exercise.eid)) - ar: AnnisResponse = AnnisResponse.from_dict(json.loads(response.get_data(as_text=True))) + params=dict(eid=Mocks.exercise.eid)) + ar: AnnisResponse = AnnisResponse.from_dict(response.json()) self.assertEqual(len(ar.graph_data.nodes), 52) - db.session.query(Exercise).delete() + DatabaseService.delete(Exercise) session.make_transient(Mocks.exercise) def test_api_exercise_list_get(self): @@ -193,29 +194,29 @@ class APItestCase(unittest.TestCase): DatabaseService.commit() args: dict = dict(lang="fr", last_update_time=int(time.time())) response: Response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_EXERCISE_LIST, - query_string=args) - self.assertEqual(json.loads(response.get_data()), []) + params=args) + self.assertEqual(response.json(), []) args["last_update_time"] = 0 db.session.add(Mocks.exercise) DatabaseService.commit() - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_EXERCISE_LIST, query_string=args) + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_EXERCISE_LIST, params=args) exercises: List[MatchingExercise] = [] - for exercise_dict in json.loads(response.get_data(as_text=True)): + for exercise_dict in response.json(): exercise_dict["search_values"] = json.dumps(exercise_dict["search_values"]) exercise_dict["solutions"] = json.dumps(exercise_dict["solutions"]) exercises.append(MatchingExercise.from_dict(exercise_dict)) self.assertEqual(len(exercises), 1) args = dict(lang=Language.English.value, vocabulary=VocabularyCorpus.agldt.name, frequency_upper_bound=500) - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_EXERCISE_LIST, query_string=args) - exercises: List[dict] = json.loads(response.get_data()) + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_EXERCISE_LIST, params=args) + exercises: List[dict] = response.json() self.assertTrue(exercises[0]["matching_degree"]) - db.session.query(Exercise).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Exercise) + DatabaseService.delete(UpdateInfo) session.make_transient(Mocks.exercise) def test_api_exercise_post(self): """ Creates a new exercise from scratch. """ - db.session.query(UpdateInfo).delete() + DatabaseService.delete(UpdateInfo) ui_exercises: UpdateInfo = UpdateInfo.from_dict(resource_type=ResourceType.exercise_list.name, last_modified_time=1.0, created_time=1.0) db.session.add(ui_exercises) @@ -230,13 +231,13 @@ class APItestCase(unittest.TestCase): "text_complexity", return_value=Mocks.text_complexity): response: Response = Mocks.app_dict[self.class_name].client.post( Config.SERVER_URI_EXERCISE, headers=Mocks.headers_form_data, data=ef.to_dict()) - ar: AnnisResponse = AnnisResponse.from_dict(json.loads(response.get_data(as_text=True))) + ar: AnnisResponse = AnnisResponse.from_dict(response.json()) self.assertEqual(len(ar.solutions), 3) response = Mocks.app_dict[self.class_name].client.post( Config.SERVER_URI_EXERCISE, headers=Mocks.headers_form_data, data=ef.to_dict()) self.assertEqual(response.status_code, 500) Mocks.app_dict[self.class_name].app_context.push() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(UpdateInfo) def test_api_file_get(self): """Gets an existing exercise""" @@ -248,26 +249,26 @@ class APItestCase(unittest.TestCase): FileService.create_tmp_file(FileType.XML, "old") args: dict = dict(type=FileType.XML, id=Mocks.exercise.eid, solution_indices=[0]) response: Response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_FILE, - query_string=args) + params=args) self.assertEqual(response.status_code, 404) - file_path: str = os.path.join(Config.TMP_DIRECTORY, Mocks.exercise.eid + "." + FileType.XML) + file_path: str = str(os.path.join(Config.TMP_DIRECTORY, Mocks.exercise.eid + "." + FileType.XML)) file_content: str = "<xml></xml>" with open(file_path, "w+") as f: f.write(file_content) ui_file.last_modified_time = datetime.utcnow().timestamp() DatabaseService.commit() del ui_file - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_FILE, query_string=args) + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_FILE, params=args) os.remove(file_path) - self.assertEqual(response.get_data(as_text=True), file_content) + self.assertEqual(response.content.decode("utf-8"), file_content) # add the mapped exercise to the database db.session.add(Mocks.exercise) DatabaseService.commit() args["type"] = FileType.PDF - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_FILE, query_string=args) - # the PDFs are not deterministically reproducible because the creation date etc. is written into them - self.assertTrue(response.data.startswith(Mocks.exercise_pdf)) - db.session.query(Exercise).delete() + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_FILE, params=args) + # the PDFs are not deterministically reproducible because the creation date, etc. is written into them + self.assertTrue(response.content.startswith(Mocks.exercise_pdf)) + DatabaseService.delete(Exercise) session.make_transient(Mocks.exercise) def test_api_file_post(self): @@ -280,10 +281,10 @@ class APItestCase(unittest.TestCase): data_dict: dict = dict(file_type=FileType.XML, urn=Mocks.urn_custom, html_content="<html></html>") response: Response = Mocks.app_dict[self.class_name].client.post( TestingConfig.SERVER_URI_FILE, headers=Mocks.headers_form_data, data=data_dict) - file_name = json.loads(response.data.decode("utf-8")) + file_name = response.json() self.assertTrue(file_name.endswith(".xml")) os.remove(os.path.join(Config.TMP_DIRECTORY, file_name)) - LearningResult.query.delete() + DatabaseService.delete(LearningResult) def test_api_frequency_get(self): """ Requests a frequency analysis for a given URN. """ @@ -294,13 +295,13 @@ class APItestCase(unittest.TestCase): mcserver.app.services.corpusService.CorpusService, "get_frequency_analysis", side_effect=[[FrequencyItem(values=[], phenomena=[], count=0)], expected_fa]): response: Response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_FREQUENCY, query_string=dict(urn=Mocks.urn_custom)) - result_list: List[dict] = json.loads(response.get_data(as_text=True)) + TestingConfig.SERVER_URI_FREQUENCY, params=dict(urn=Mocks.urn_custom)) + result_list: List[dict] = response.json() fa: List[FrequencyItem] = [FrequencyItem.from_dict(x) for x in result_list] self.assertEqual(len(fa), 1) response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_FREQUENCY, query_string=dict(urn=Mocks.urn_custom)) - result_list: List[dict] = json.loads(response.get_data(as_text=True)) + TestingConfig.SERVER_URI_FREQUENCY, params=dict(urn=Mocks.urn_custom)) + result_list: List[dict] = response.json() fa: List[FrequencyItem] = [FrequencyItem.from_dict(x) for x in result_list] self.assertEqual(fa[0].values, expected_fa[0].values) self.assertEqual(fa[1].values[0], None) @@ -308,33 +309,33 @@ class APItestCase(unittest.TestCase): def test_api_h5p_get(self): """ Requests a H5P JSON file for a given exercise. """ response: Response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_H5P, query_string=dict(eid=Config.EXERCISE_ID_TEST, lang=Language.English.value)) - self.assertIn(Mocks.h5p_json_cloze[1:-1], response.get_data(as_text=True)) + TestingConfig.SERVER_URI_H5P, params=dict(eid=Config.EXERCISE_ID_TEST, lang=Language.English.value)) + self.assertIn(Mocks.h5p_json_cloze[1:-1], response.content.decode("utf-8")) args: dict = dict(eid=Mocks.exercise.eid, lang=Language.English.value, solution_indices=[0]) - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, query_string=args) + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, params=args) self.assertEqual(response.status_code, 404) db.session.add(Mocks.exercise) DatabaseService.commit() - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, query_string=args) - self.assertIn(Mocks.h5p_json_cloze[1:-1], response.get_data(as_text=True)) + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, params=args) + self.assertIn(Mocks.h5p_json_cloze[1:-1], response.content.decode("utf-8")) Mocks.exercise.exercise_type = ExerciseType.kwic.value DatabaseService.commit() - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, query_string=args) + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, params=args) self.assertEqual(response.status_code, 422) Mocks.exercise.exercise_type = ExerciseType.matching.value DatabaseService.commit() - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, query_string=args) - self.assertIn(Mocks.h5p_json_matching[1:-1], response.get_data(as_text=True)) + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, params=args) + self.assertIn(Mocks.h5p_json_matching[1:-1], response.content.decode("utf-8")) Mocks.exercise.exercise_type = ExerciseType.cloze.value args["lang"] = "fr" - response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, query_string=args) - self.assertIn(Mocks.h5p_json_cloze[1:-1], response.get_data(as_text=True)) - db.session.query(Exercise).delete() + response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, params=args) + self.assertIn(Mocks.h5p_json_cloze[1:-1], response.content.decode("utf-8")) + DatabaseService.delete(Exercise) session.make_transient(Mocks.exercise) response = NetworkService.make_json_response(dict()) with patch.object(mcserver.app.api.h5pAPI, "get_remote_exercise", return_value=response) as get_mock: args["eid"] = f".{FileType.H5P}" - Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, query_string=args) + Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_H5P, params=args) self.assertEqual(get_mock.call_count, 1) def test_api_h5p_post(self): @@ -349,12 +350,12 @@ class APItestCase(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()), 1940145) + self.assertEqual(len(response.content), 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()) self.assertEqual(response.status_code, 422) - db.session.query(Exercise).delete() + DatabaseService.delete(Exercise) def test_api_kwic_post(self): """ Posts an AQL query to create a KWIC visualization in SVG format. """ @@ -372,7 +373,7 @@ class APItestCase(unittest.TestCase): with patch.object(mcserver.app.services.corpusService.requests, "post", return_value=mr): response: Response = Mocks.app_dict[self.class_name].client.post( TestingConfig.SERVER_URI_KWIC, headers=Mocks.headers_form_data, data=kf.to_dict()) - self.assertTrue(response.data.startswith(Mocks.kwic_svg)) + self.assertTrue(response.content.startswith(Mocks.kwic_svg)) def test_api_not_found(self): """Checks the 404 response in case of an invalid API query URL.""" @@ -387,18 +388,18 @@ class APItestCase(unittest.TestCase): mock_get_corpus.return_value = AnnisResponse( graph_data=GraphData(links=[], nodes=[]), solutions=[]) response: Response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_RAW_TEXT, query_string=dict(urn=Mocks.urn_custom)) + TestingConfig.SERVER_URI_RAW_TEXT, params=dict(urn=Mocks.urn_custom)) self.assertEqual(response.status_code, 404) mock_get_corpus.return_value = Mocks.annis_response response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_RAW_TEXT, - query_string=dict(urn=Mocks.urn_custom)) - ar: AnnisResponse = AnnisResponse.from_dict(json.loads(response.get_data(as_text=True))) + params=dict(urn=Mocks.urn_custom)) + ar: AnnisResponse = AnnisResponse.from_dict(response.json()) self.assertEqual(len(ar.graph_data.nodes), 52) ar_copy: AnnisResponse = Mocks.copy(Mocks.annis_response, AnnisResponse) ar_copy.graph_data.nodes = [] mock_get_corpus.return_value = ar_copy response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_RAW_TEXT, - query_string=dict(urn=Mocks.urn_custom)) + params=dict(urn=Mocks.urn_custom)) self.assertEqual(response.status_code, 404) def test_api_raw_text_post(self): @@ -407,7 +408,7 @@ class APItestCase(unittest.TestCase): with patch.object(AnnotationService, "get_udpipe", return_value=Mocks.udpipe_string): response: Response = Mocks.app_dict[self.class_name].client.post( TestingConfig.SERVER_URI_RAW_TEXT, headers=Mocks.headers_form_data, data=rtfe.to_dict()) - ar: AnnisResponse = AnnisResponse.from_dict(json.loads(response.get_data())) + ar: AnnisResponse = AnnisResponse.from_dict(response.json()) self.assertEqual(len(ar.graph_data.nodes), 5) def test_api_static_exercises_get(self): @@ -447,7 +448,7 @@ class APItestCase(unittest.TestCase): self.assertEqual(response.status_code, 503) response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_STATIC_EXERCISES) os.remove(TestingConfig.STATIC_EXERCISES_ZIP_FILE_PATH) - self.assertGreater(len(response.get_data(as_text=True)), 1900) + self.assertGreater(len(response.content.decode("utf-8")), 1900) response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_STATIC_EXERCISES) self.assertEqual(mock_udpipe.call_count, 1) @@ -462,25 +463,25 @@ class APItestCase(unittest.TestCase): "text_complexity", return_value=Mocks.text_complexity): args: dict = dict(urn=Mocks.urn_custom, measure=TextComplexityMeasure.all.name) response: Response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_TEXT_COMPLEXITY, query_string=args) - self.assertEqual(response.get_data(as_text=True), json.dumps(Mocks.text_complexity.to_dict())) + TestingConfig.SERVER_URI_TEXT_COMPLEXITY, params=args) + self.assertEqual(response.json(), Mocks.text_complexity.to_dict()) def test_api_valid_reff_get(self): # """ Retrieves possible citations for a given URN. """ with patch.object(MyCapytain.retrievers.cts5.requests, "get", side_effect=TestHelper.cts_get_mock): args: dict = dict(urn=Mocks.urn_custom[:-14]) response: Response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_VALID_REFF, query_string=args) - self.assertEqual(len(json.loads(response.data.decode("utf-8"))), 3) + TestingConfig.SERVER_URI_VALID_REFF, params=args) + self.assertEqual(len(response.json()), 3) APItestCase.clear_folder(Config.REFF_CACHE_DIRECTORY) args["urn"] = f"{Mocks.urn_custom[:-13]}4" response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_VALID_REFF, query_string=args) + TestingConfig.SERVER_URI_VALID_REFF, params=args) self.assertEqual(response.status_code, 404) APItestCase.clear_folder(Config.REFF_CACHE_DIRECTORY) args["urn"] = f"{Mocks.urn_custom[:-13]}abc" response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_VALID_REFF, - query_string=args) + params=args) self.assertEqual(response.status_code, 400) APItestCase.clear_folder(Config.REFF_CACHE_DIRECTORY) TestingConfig.SIMULATE_HTTP_ERROR = True @@ -495,8 +496,8 @@ class APItestCase(unittest.TestCase): with patch.object(mcserver.app.api.vectorNetworkAPI.Word2Vec, "load", return_value=MockW2V()): args: dict = dict(search_regex='ueritas', nearest_neighbor_count=150, min_count=6) response: Response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_VECTOR_NETWORK, query_string=args) - svg_string: str = json.loads(response.get_data(as_text=True)) + TestingConfig.SERVER_URI_VECTOR_NETWORK, params=args) + svg_string: str = response.json() self.assertGreater(len(svg_string), 6500) def test_api_vector_network_post(self): @@ -507,32 +508,34 @@ class APItestCase(unittest.TestCase): vnf: VectorNetworkForm = VectorNetworkForm(search_regex='uera', nearest_neighbor_count=10) response: Response = Mocks.app_dict[self.class_name].client.post( TestingConfig.SERVER_URI_VECTOR_NETWORK, headers=Mocks.headers_form_data, data=vnf.to_dict()) - self.assertEqual(len(json.loads(response.get_data(as_text=True))), 2) + self.assertEqual(len(response.json()), 2) def test_api_vocabulary_get(self): """ Retrieves sentence ID and matching degree for each sentence in the query text. """ vf: VocabularyForm = VocabularyForm(query_urn="", vocabulary=VocabularyMC.AGLDT, frequency_upper_bound=6000) response: Response = Mocks.app_dict[self.class_name].client.get( - TestingConfig.SERVER_URI_VOCABULARY, query_string=vf.to_dict()) + TestingConfig.SERVER_URI_VOCABULARY, params=vf.to_dict()) self.assertEqual(response.status_code, 404) with patch.object(mcserver.app.services.CorpusService, "get_corpus", return_value=Mocks.copy(Mocks.annis_response, AnnisResponse)): - vf.query_urn=Mocks.urn_custom + vf.query_urn = Mocks.urn_custom response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_VOCABULARY, - query_string=vf.to_dict()) - sentences: List[Sentence] = [Sentence.from_dict(x) for x in json.loads(response.get_data(as_text=True))] + params=vf.to_dict()) + sentences: List[Sentence] = [Sentence.from_dict(x) for x in response.json()] self.assertEqual(sentences[0].matching_degree, 90.9090909090909) def test_api_vocabulary_post(self): """ Indicates for each token of a corpus whether it is covered by a reference vocabulary. """ with patch.object(mcserver.app.services.corpusService.CorpusService, "get_corpus", return_value=Mocks.annis_response): - vf: VocabularyForm = VocabularyForm(frequency_upper_bound=500, query_urn=Mocks.urn_custom, - vocabulary=VocabularyMC.AGLDT) - response: Response = Mocks.app_dict[self.class_name].client.post( - TestingConfig.SERVER_URI_VOCABULARY, data=vf.to_dict()) - ar: AnnisResponse = AnnisResponse.from_dict(json.loads(response.get_data(as_text=True))) - self.assertTrue(NodeMC.from_dict(ar.graph_data.nodes[3].to_dict()).is_oov) + with patch.object(mcserver.app.services.textComplexityService.TextComplexityService, + "text_complexity", return_value=Mocks.text_complexity): + vf: VocabularyForm = VocabularyForm(frequency_upper_bound=500, query_urn=Mocks.urn_custom, + vocabulary=VocabularyMC.AGLDT) + response: Response = Mocks.app_dict[self.class_name].client.post( + TestingConfig.SERVER_URI_VOCABULARY, data=vf.to_dict()) + ar: AnnisResponse = AnnisResponse.from_dict(response.json()) + self.assertTrue(NodeMC.from_dict(ar.graph_data.nodes[3].to_dict()).is_oov) def test_api_zenodo_get(self): """ Provides exercise materials from the Zenodo repository.""" @@ -541,8 +544,7 @@ class APItestCase(unittest.TestCase): mock_records[0].metadata = dict(title=[title]) with patch.object(mcserver.app.api.zenodoAPI.Sickle, "ListRecords", return_value=mock_records): response: Response = Mocks.app_dict[self.class_name].client.get(TestingConfig.SERVER_URI_ZENODO) - records: List[ZenodoRecord] = [ZenodoRecord.from_dict(x) for x in - json.loads(response.get_data(as_text=True))] + records: List[ZenodoRecord] = [ZenodoRecord.from_dict(x) for x in response.json()] self.assertEqual(records[0].title, [title]) def test_api_zenodo_post(self): @@ -553,12 +555,12 @@ class APItestCase(unittest.TestCase): with patch.object(mcserver.app.api.zenodoAPI.Sickle, "GetRecord", return_value=record_mock): response: Response = Mocks.app_dict[self.class_name].client.post( TestingConfig.SERVER_URI_ZENODO, data=zf.to_dict()) - self.assertEqual(json.loads(response.get_data()), uris) + self.assertEqual(response.json(), uris) def test_create_app(self): """Creates a new Flask application and configures it. Initializes the application and the database.""" with patch.object(sys, "argv", [None, None, Config.FLASK_MIGRATE]): - with patch.object(mcserver.app, "init_app_common", return_value=Flask(__name__)): + with patch.object(mcserver.app, "init_app_common", return_value=connexion.FlaskApp(__name__)): cfg: Type[Config] = TestingConfig old_uri: str = cfg.SQLALCHEMY_DATABASE_URI create_app(cfg) @@ -571,7 +573,7 @@ class APItestCase(unittest.TestCase): response: Response = Mocks.app_dict[self.class_name].client.get(Config.SERVER_URI_FAVICON) with open(os.path.join(Config.ASSETS_DIRECTORY, Config.FAVICON_FILE_NAME), "rb") as f: content: bytes = f.read() - data_received: bytes = response.get_data() + data_received: bytes = response.content self.assertEqual(content, data_received) @@ -642,7 +644,7 @@ class CommonTestCase(unittest.TestCase): db.session.add(ui_cts) DatabaseService.commit() clean_tmp_folder() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(UpdateInfo) self.assertFalse(os.path.exists(dir_path)) def test_create_xml_string(self): @@ -666,13 +668,13 @@ class CommonTestCase(unittest.TestCase): def test_full_init(self): """ Fully initializes the application, including logging.""" - Mocks.app_dict[self.class_name].app.config["TESTING"] = False + Mocks.app_dict[self.class_name].app.app.config["TESTING"] = False with patch.object(CorpusService, "init_graphannis_logging"): with patch.object(mcserver.app, "start_updater") as updater_mock: - full_init(Mocks.app_dict[self.class_name].app) + full_init(Mocks.app_dict[self.class_name].app.app) self.assertEqual(updater_mock.call_count, 1) - Mocks.app_dict[self.class_name].app.config["TESTING"] = True - db.session.query(UpdateInfo).delete() + Mocks.app_dict[self.class_name].app.app.config["TESTING"] = True + DatabaseService.delete(UpdateInfo) def test_get_annotations_from_string(self): """ Gets annotation data from a given string, be it a CoNLL string or a corpus URN. """ @@ -792,8 +794,8 @@ class CommonTestCase(unittest.TestCase): response_mock.content = b"" with patch.object(mcserver.app.api.h5pAPI.requests, "get", return_value=response_mock): with patch.object(mcserver.app.api.h5pAPI, "ZipFile", return_value=ZipMock): - response: Response = get_remote_exercise("") - h5p_dict: dict = json.loads(response.get_data()) + response: flask.Response = get_remote_exercise("") + h5p_dict: dict = response.json self.assertEqual(h5p_dict["mainLibrary"], "lib") def test_get_solutions_by_index(self): @@ -866,7 +868,6 @@ class CommonTestCase(unittest.TestCase): import blinker # for signalling import coverage # for code coverage in unit tests from docx import Document # for exercise exports - from flask_cors import CORS # for HTTP requests from flask_migrate import Migrate # for database migrations from flask_restful import Api # for construction of the API import gunicorn # for production server @@ -936,9 +937,9 @@ class CommonTestCase(unittest.TestCase): def test_log_exception(self): """Logs errors that occur while the Flask app is working. """ - with patch.object(Mocks.app_dict[self.class_name].app.logger, "info") as mock_info: - with Mocks.app_dict[self.class_name].app.test_request_context("/?param=value"): - log_exception(Mocks.app_dict[self.class_name].app, ValueError()) + with patch.object(Mocks.app_dict[self.class_name].app.app.logger, "info") as mock_info: + with Mocks.app_dict[self.class_name].app.app.test_request_context("/?param=value"): + log_exception(Mocks.app_dict[self.class_name].app.app, ValueError()) self.assertEqual(mock_info.call_count, 1) def test_make_docx_file(self): @@ -1031,7 +1032,7 @@ class CommonTestCase(unittest.TestCase): self.assertEqual(len(xapi.serialize().keys()), 5) ed: ExerciseData = ExerciseData(json_dict=Mocks.exercise_data.serialize()) self.assertEqual(len(ed.graph.nodes), len(Mocks.exercise_data.graph.nodes)) - db.session.query(UpdateInfo).delete() + DatabaseService.delete(UpdateInfo) session.make_transient(Mocks.corpora[0]) session.make_transient(Mocks.exercise) @@ -1068,11 +1069,11 @@ class CommonTestCase(unittest.TestCase): db.session.add(ui_cts) DatabaseService.commit() with patch.object(CorpusService, 'update_corpora') as update_mock: - t: Thread = start_updater(Mocks.app_dict[self.class_name].app) + t: Thread = start_updater(Mocks.app_dict[self.class_name].app.app) self.assertIsInstance(t, Thread) self.assertTrue(t.is_alive()) time.sleep(0.1) - db.session.query(UpdateInfo).delete() + DatabaseService.delete(UpdateInfo) assert not update_mock.called Mocks.app_dict[self.class_name].app_context.push() @@ -1105,8 +1106,8 @@ class CorpusTestCase(unittest.TestCase): @patch('mcserver.app.services.corpusService.CorpusService.update_corpora') def test_check_corpus_list_age(self, mock_update: MagicMock): """Checks whether the list of available corpora needs to be updated.""" - db.session.query(UpdateInfo).delete() - CorpusService.check_corpus_list_age(Mocks.app_dict[self.class_name].app) + DatabaseService.delete(UpdateInfo) + CorpusService.check_corpus_list_age(Mocks.app_dict[self.class_name].app.app) ui_cts: UpdateInfo = DatabaseService.query( UpdateInfo, filter_by=dict(resource_type=ResourceType.cts_data.name), first=True) self.assertEqual(ui_cts, None) @@ -1115,11 +1116,11 @@ class CorpusTestCase(unittest.TestCase): db.session.add(ui_cts) DatabaseService.commit() utc_now: datetime = datetime.utcnow() - CorpusService.check_corpus_list_age(Mocks.app_dict[self.class_name].app) + CorpusService.check_corpus_list_age(Mocks.app_dict[self.class_name].app.app) ui_cts = DatabaseService.query( UpdateInfo, filter_by=dict(resource_type=ResourceType.cts_data.name), first=True) self.assertGreater(ui_cts.last_modified_time, utc_now.timestamp()) - db.session.query(UpdateInfo).delete() + DatabaseService.delete(UpdateInfo) def test_extract_custom_corpus_text(self): """ Extracts text from the relevant parts of a (custom) corpus. """ @@ -1239,14 +1240,14 @@ class CorpusTestCase(unittest.TestCase): ec: Corpus = CorpusService.existing_corpora[0] ec.title = "" DatabaseService.commit() - TestHelper.add_corpus(CorpusMC.from_dict(source_urn="123")) + TestHelper.add_corpus(CorpusMC.from_dict(source_urn="123", cid=123)) cls: List[CitationLevel] = [ec.citation_level_1, ec.citation_level_2, ec.citation_level_3] CorpusService.update_corpus(ec.title, ec.source_urn, ec.author, cls, ec) self.assertFalse(ec.title) CorpusService.update_corpora() self.assertTrue(ec.title) - db.session.query(Corpus).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Corpus) + DatabaseService.delete(UpdateInfo) class DatabaseTestCase(unittest.TestCase): @@ -1268,12 +1269,12 @@ class DatabaseTestCase(unittest.TestCase): self.assertTrue(os.path.exists(Config.GRAPHANNIS_LOG_PATH)) os.remove(Config.GRAPHANNIS_LOG_PATH) with patch.object(sys, 'argv', Mocks.test_args): - app: Flask = mcserver.get_app() + app: Flask = mcserver.get_app().app self.assertIsInstance(app, Flask) self.assertTrue(app.config["TESTING"]) - db.session.query(UpdateInfo).delete() + DatabaseService.delete(UpdateInfo) Mocks.app_dict[self.class_name].app_context.push() - db.session.query(Corpus).delete() + DatabaseService.delete(Corpus) def test_commit(self): """Commits the last action to the database and, if it fails, rolls back the current session.""" @@ -1285,30 +1286,32 @@ class DatabaseTestCase(unittest.TestCase): mock_db.session.commit.side_effect = commit with self.assertRaises(OperationalError): TestHelper.add_corpus(Mocks.corpora[0]) - db.session.query(Corpus).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Corpus) + DatabaseService.delete(UpdateInfo) session.make_transient(Mocks.corpora[0]) def test_create_postgres_database(self): """ Creates a new Postgres database. """ with patch.object(mcserver.app.Session, "execute") as execute_mock: with patch.object(mcserver.app.Session, "connection"): - create_postgres_database(Mocks.app_dict[self.class_name].app, TestingConfig) + create_postgres_database(Mocks.app_dict[self.class_name].app.app, TestingConfig) self.assertEqual(execute_mock.call_count, 1) def test_init_db_alembic(self): """In Docker, the alembic version is not initially written to the database, so we need to set it manually.""" + db.session.execute(sqlalchemy.text(f'DROP TABLE IF EXISTS {Config.DATABASE_TABLE_ALEMBIC}')) + DatabaseService.commit() self.assertEqual(inspect(db.engine).has_table(Config.DATABASE_TABLE_ALEMBIC), False) with patch.object(mcserver.app.services.databaseService, "flask_migrate") as migrate_mock: migrate_mock.stamp.return_value = MagicMock() migrate_mock.upgrade.return_value = MagicMock() migrate_mock.current.return_value = MagicMock() - DatabaseService.init_db_alembic(Mocks.app_dict[self.class_name].app) + DatabaseService.init_db_alembic(Mocks.app_dict[self.class_name].app.app) self.assertEqual(migrate_mock.stamp.call_count, 1) def test_init_db_corpus(self): """Initializes the corpus table.""" - db.session.query(Corpus).delete() + DatabaseService.delete(Corpus) cc: CustomCorpus = CustomCorpusService.custom_corpora[0] old_corpus: Corpus = Mocks.corpora[0] old_corpus.source_urn = cc.corpus.source_urn @@ -1318,8 +1321,8 @@ class DatabaseTestCase(unittest.TestCase): corpus: Corpus = DatabaseService.query( Corpus, filter_by=dict(source_urn=cc.corpus.source_urn), first=True) self.assertEqual(corpus.title, cc.corpus.title) - db.session.query(Corpus).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Corpus) + DatabaseService.delete(UpdateInfo) def test_map_exercise_data_to_database(self): """Maps exercise data to the database and saves it for later access.""" @@ -1351,8 +1354,8 @@ class DatabaseTestCase(unittest.TestCase): self.assertEqual(expected_values, actual_values) exercise_from_db: Exercise = DatabaseService.query(Exercise, first=True) self.assertEqual(exercise, exercise_from_db) - db.session.query(Exercise).delete() - db.session.query(UpdateInfo).delete() + DatabaseService.delete(Exercise) + DatabaseService.delete(UpdateInfo) session.make_transient(Mocks.exercise) def test_update_exercises(self): @@ -1371,7 +1374,7 @@ class DatabaseTestCase(unittest.TestCase): exercises = DatabaseService.query(Exercise) self.assertEqual(len(exercises), 1) self.assertEqual(exercises[0].text_complexity, 54.53) - db.session.query(Exercise).delete() + DatabaseService.delete(Exercise) if __name__ == '__main__': diff --git a/mc_frontend/openapi/model/corpus.ts b/mc_frontend/openapi/model/corpus.ts index f7fbe4562229eb3a3daea6efe1b35851942ec835..37629fe5bc43162688827aa010bdbe1ab1753e6d 100644 --- a/mc_frontend/openapi/model/corpus.ts +++ b/mc_frontend/openapi/model/corpus.ts @@ -22,7 +22,7 @@ export interface Corpus { /** * Unique identifier for the corpus. */ - cid?: number; + cid: number; /** * First level for citing the corpus. */ diff --git a/mc_frontend/package.json b/mc_frontend/package.json index cf298b3e3279410be90bfc2b993b7f2064812026..5a4d313ae31df3bbdb0682c5dabbaefdb5c538a5 100644 --- a/mc_frontend/package.json +++ b/mc_frontend/package.json @@ -1,6 +1,6 @@ { "name": "mc_frontend", - "version": "2.9.1", + "version": "2.9.2", "author": "Ionic Framework", "homepage": "https://ionicframework.com/", "scripts": { diff --git a/mc_frontend/src/app/author-detail/author-detail.page.spec.ts b/mc_frontend/src/app/author-detail/author-detail.page.spec.ts index b4962e7eb2fd43239a83df811e20c590b6b2003f..18e8b967f848bd780039a510edbd6c3db0d96de3 100644 --- a/mc_frontend/src/app/author-detail/author-detail.page.spec.ts +++ b/mc_frontend/src/app/author-detail/author-detail.page.spec.ts @@ -42,7 +42,7 @@ describe('AuthorDetailPage', () => { it('should show possible references', () => { const currentCorpusSpy: Spy = spyOn(authorDetailPage.corpusService, 'setCurrentCorpus'); const textRangeSpy: Spy = spyOn(authorDetailPage.helperService, 'goToPage').and.returnValue(Promise.resolve(true)); - authorDetailPage.showPossibleReferences({source_urn: ''}); + authorDetailPage.showPossibleReferences({source_urn: '', cid: -999}); expect(currentCorpusSpy).toHaveBeenCalledTimes(1); expect(textRangeSpy).toHaveBeenCalledTimes(1); }); diff --git a/mc_frontend/src/app/author/author.page.spec.ts b/mc_frontend/src/app/author/author.page.spec.ts index 603643c00cf764330f513f23acbb2631753d4fbe..39b2d922e1246ebe73e65d35977cb2e56139691f 100644 --- a/mc_frontend/src/app/author/author.page.spec.ts +++ b/mc_frontend/src/app/author/author.page.spec.ts @@ -87,7 +87,7 @@ describe('AuthorPage', () => { expect(toggleSpy).toHaveBeenCalledTimes(1); expect(storageSpy).toHaveBeenCalledTimes(2); authorPage.corpusService.availableAuthors = [new Author({ - corpora: [{source_urn: 'proiel'}], + corpora: [{source_urn: 'proiel', cid: -999}], name: 'name' })]; toggleSpy.and.callThrough(); diff --git a/mc_frontend/src/app/corpus.service.spec.ts b/mc_frontend/src/app/corpus.service.spec.ts index 6d33c9d35a7933a26b72fb4f1a2ca2c8cad03894..f7ebff1f2c938fc5fdb40658bd823ab6da151d5f 100644 --- a/mc_frontend/src/app/corpus.service.spec.ts +++ b/mc_frontend/src/app/corpus.service.spec.ts @@ -267,7 +267,7 @@ describe('CorpusService', () => { it('should initialize the current corpus', ((done) => { corpusService.helperService.applicationState.next(new ApplicationState({mostRecentSetup: new TextData()})); - let corpus: CorpusMC = {source_urn: ''}; + let corpus: CorpusMC = {source_urn: '', cid: -999}; const subscriptions: Subscription[] = []; function initCorpus(): Promise<void> { @@ -332,12 +332,12 @@ describe('CorpusService', () => { it('should process corpora', () => { const corpusList: CorpusMC[] = [ - {author: '', source_urn: 'proiel'}, - {author: 'b', source_urn: 'b', title: 't1'}, - {author: 'b', source_urn: 'b', title: 't1'}, - {author: 'b', source_urn: 'c', title: 't3'}, - {author: 'b', source_urn: 'e', title: 't2'}, - {author: 'a', source_urn: 'd'}, + {author: '', source_urn: 'proiel', cid: -999}, + {author: 'b', source_urn: 'b', title: 't1', cid: -999}, + {author: 'b', source_urn: 'b', title: 't1', cid: -999}, + {author: 'b', source_urn: 'c', title: 't3', cid: -999}, + {author: 'b', source_urn: 'e', title: 't2', cid: -999}, + {author: 'a', source_urn: 'd', cid: -999}, ]; corpusService.availableAuthors = [new Author({name: ' PROIEL', corpora: []})]; corpusService.processCorpora(corpusList); @@ -396,7 +396,7 @@ describe('CorpusService', () => { it('should save a new corpus', (done) => { corpusService.initCurrentTextRange(); corpusService.initCurrentCorpus().then(() => { - corpusService.setCurrentCorpus({source_urn: ''}); + corpusService.setCurrentCorpus({source_urn: '', cid: -999}); corpusService.currentCorpus.pipe(take(1)).subscribe((corpus: CorpusMC) => { expect(corpus).toBeTruthy(); done(); diff --git a/mc_frontend/src/app/doc-software/doc-software.page.html b/mc_frontend/src/app/doc-software/doc-software.page.html index 4849eb3703deb0a7a315a0b6b22ebf52862731ac..89ff5e4811b233494f029704a820f12a9a7dcb8b 100644 --- a/mc_frontend/src/app/doc-software/doc-software.page.html +++ b/mc_frontend/src/app/doc-software/doc-software.page.html @@ -30,7 +30,7 @@ </a> </li> <li> - <a href="https://korpling.org/mc-service/mc/api/v1.0/ui/" style="text-decoration: none;" target="_blank"> + <a href="https://korpling.org/mc-spec/" style="text-decoration: none;" target="_blank"> Documentation @ Swagger </a> </li> diff --git a/mc_frontend/src/app/models/mockMC.ts b/mc_frontend/src/app/models/mockMC.ts index b20033cb5dd97f6ed1193ffc262f69066866e148..94ee054eb54b2a88befd39be1b1610851caaf6d0 100644 --- a/mc_frontend/src/app/models/mockMC.ts +++ b/mc_frontend/src/app/models/mockMC.ts @@ -17,6 +17,7 @@ export default class MockMC { author: 'author', source_urn: 'urn', title: 'title', + cid: -999 }]; static apiResponseFrequencyAnalysisGet: FrequencyItem[] = [{ phenomena: [Phenomenon.Upostag], @@ -35,7 +36,7 @@ export default class MockMC { nodes: [{}], links: [] } }, - currentCorpus: {citations: {}, source_urn: 'exampleUrn'}, + currentCorpus: {citations: {}, source_urn: 'exampleUrn', cid: -999}, currentTextRange: new TextRange({start: ['1', '2'], end: ['1', '2']}) }), exerciseList: [new ExerciseMC()], diff --git a/mc_frontend/src/app/ranking/ranking.page.spec.ts b/mc_frontend/src/app/ranking/ranking.page.spec.ts index 4aca6356438666c8979fdd4460da132d6a4efb34..781b03014684d16aca2ed804a1b654a69b3ac614 100644 --- a/mc_frontend/src/app/ranking/ranking.page.spec.ts +++ b/mc_frontend/src/app/ranking/ranking.page.spec.ts @@ -64,15 +64,13 @@ describe('RankingPage', () => { const oovSpy: Spy = spyOn(rankingPage.vocService, 'getOOVwords').and.returnValue(Promise.resolve(ar)); spyOn(rankingPage.corpusService, 'processAnnisResponse'); spyOn(rankingPage.helperService, 'goToShowTextPage').and.returnValue(Promise.resolve(true)); - rankingPage.showText([{id: 1}]).then(() => { + rankingPage.showText([{id: 1}]).then(async () => { expect(rankingPage.helperService.isVocabularyCheck).toBe(true); oovSpy.and.callFake(() => Promise.reject()); rankingPage.helperService.isVocabularyCheck = false; - rankingPage.showText([{id: 1}]).then(() => { - }, () => { - expect(rankingPage.helperService.isVocabularyCheck).toBe(false); - done(); - }); + await rankingPage.showText([{id: 1}]); + expect(rankingPage.helperService.isVocabularyCheck).toBe(false); + done(); }); }); }); diff --git a/mc_frontend/src/app/ranking/ranking.page.ts b/mc_frontend/src/app/ranking/ranking.page.ts index c65f200df4a5443a6ae2ad3fe0017a689db7ee67..53951e0214070da137a5d3c39e616b07ded650a5 100644 --- a/mc_frontend/src/app/ranking/ranking.page.ts +++ b/mc_frontend/src/app/ranking/ranking.page.ts @@ -45,7 +45,7 @@ export class RankingPage implements OnInit { this.corpusService.baseUrn = parsedUrnParts[0]; this.vocService.desiredSentenceCount = params.desiredSentenceCount; this.vocService.frequencyUpperBound = params.frequencyUpperBound; - this.corpusService.currentCorpus.next({source_urn: params.urn}); + this.corpusService.currentCorpus.next({source_urn: params.urn, cid: -999}); this.corpusService.currentTextRange.next(parsedUrnParts[1]); this.corpusService.isTextRangeCorrect = true; this.vocService.currentReferenceVocabulary = params.referenceVocabulary; @@ -54,7 +54,7 @@ export class RankingPage implements OnInit { } showText(rank: Sentence[]): Promise<void> { - return new Promise<void>((resolve, reject) => { + return new Promise<void>((resolve) => { this.corpusService.currentUrn = this.corpusService.baseUrn + `@${rank[0].id}-${rank[rank.length - 1].id}`; this.vocService.getOOVwords(this.corpusService.currentUrn).then((ar: AnnisResponse) => { const urnStart: string = ar.graph_data.nodes[0].id.split('/')[1]; @@ -65,7 +65,7 @@ export class RankingPage implements OnInit { this.helperService.goToShowTextPage(this.navCtrl, true).then(); return resolve(); }, async (error: HttpErrorResponse) => { - return reject(); + return resolve(); }); }); } diff --git a/mc_frontend/src/app/text-range/text-range.page.spec.ts b/mc_frontend/src/app/text-range/text-range.page.spec.ts index 33e8a9b36b1927647d84b181ddd34eaa27e59b18..8581c8bf95fb4015c6dd78c84d297cd6ca4ec3ff 100644 --- a/mc_frontend/src/app/text-range/text-range.page.spec.ts +++ b/mc_frontend/src/app/text-range/text-range.page.spec.ts @@ -51,12 +51,13 @@ describe('TextRangePage', () => { it('should add level 3 references', (done) => { const addReferencesSpy: Spy = spyOn(textRangePage, 'addReferences').and.returnValue(Promise.resolve()); - textRangePage.addLevel3References([], {source_urn: ''}).then(() => { + textRangePage.addLevel3References([], {source_urn: '', cid: -999}).then(() => { expect(addReferencesSpy).toHaveBeenCalledTimes(0); textRangePage.currentInputId = 2; const corpus: CorpusMC = { source_urn: '', - citations: {1: new Citation({subcitations: {2: new Citation({subcitations: {3: new Citation()}})}})} + citations: {1: new Citation({subcitations: {2: new Citation({subcitations: {3: new Citation()}})}})}, + cid: -999 }; textRangePage.addLevel3References(['1', '2'], corpus).then(() => { expect(addReferencesSpy).toHaveBeenCalledTimes(0); @@ -81,7 +82,7 @@ describe('TextRangePage', () => { } textRangePage.corpusService.currentCorpus = new ReplaySubject<CorpusMC>(1); - textRangePage.corpusService.currentCorpus.next({citations: {4: new Citation()}, source_urn: ''}); + textRangePage.corpusService.currentCorpus.next({citations: {4: new Citation()}, source_urn: '', cid: -999}); resetCitationValues(); const citationLabels: string[] = ['1']; textRangePage.addMissingCitations(citationLabels, citationLabels).then(() => { @@ -107,7 +108,11 @@ describe('TextRangePage', () => { }); it('should add references', (done) => { - const corpus: CorpusMC = {citations: {0: new Citation({subcitations: {}, label: ''})}, source_urn: ''}; + const corpus: CorpusMC = { + citations: {0: new Citation({subcitations: {}, label: ''})}, + source_urn: '', + cid: -999 + }; textRangePage.corpusService.currentCorpus = new ReplaySubject<CorpusMC>(1); textRangePage.corpusService.currentCorpus.next(corpus); const validReffSpy: Spy = spyOn(textRangePage.corpusService, 'getCTSvalidReff').and.callFake(() => Promise.reject()); @@ -148,13 +153,14 @@ describe('TextRangePage', () => { textRangePage.corpusService.initCurrentTextRange(); textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); textRangePage.corpusService.currentCorpus = new ReplaySubject<CorpusMC>(1); - textRangePage.corpusService.currentCorpus.next({citations: {4: new Citation()}, source_urn: ''}); + textRangePage.corpusService.currentCorpus.next({citations: {4: new Citation()}, source_urn: '', cid: -999}); textRangePage.checkInputDisabled().then(() => { textRangePage.isInputDisabled[0].pipe(take(1)).subscribe((isDisabled: boolean) => { expect(isDisabled).toBe(true); textRangePage.corpusService.currentCorpus.next({ source_urn: '', - citations: {1: new Citation({subcitations: {2: new Citation()}})} + citations: {1: new Citation({subcitations: {2: new Citation()}})}, + cid: -999 }); textRangePage.checkInputDisabled().then(() => { textRangePage.isInputDisabled[0].pipe(take(1)).subscribe((isDisabled2: boolean) => { @@ -179,6 +185,7 @@ describe('TextRangePage', () => { citations: {4: new Citation()}, citation_level_2: CitationLevel.default.toString(), citation_level_3: CitationLevel.default.toString(), + cid: -999 }; textRangePage.corpusService.currentCorpus = new ReplaySubject<CorpusMC>(1); textRangePage.corpusService.currentCorpus.next(corpus); @@ -252,7 +259,8 @@ describe('TextRangePage', () => { textRangePage.corpusService.currentCorpus.next({ source_urn: '', citations: {4: new Citation()}, - citation_level_2: CitationLevel.default.toString() + citation_level_2: CitationLevel.default.toString(), + cid: -999 }); textRangePage.corpusService.initCurrentTextRange(); textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); @@ -287,7 +295,7 @@ describe('TextRangePage', () => { textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); textRangePage.initPage({ source_urn: '', citation_level_2: CitationLevel.default.toString(), - citations: {1: new Citation({label: '1'})} + citations: {1: new Citation({label: '1'})}, cid: -999 }).then(() => { textRangePage.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { expect(tr.start[0]).toBe('1'); @@ -307,7 +315,11 @@ describe('TextRangePage', () => { }); } - const corpus: CorpusMC = {citations: {4: new Citation({subcitations: {}, value: 4})}, source_urn: ''}; + const corpus: CorpusMC = { + citations: {4: new Citation({subcitations: {}, value: 4})}, + source_urn: '', + cid: -999 + }; textRangePage.corpusService.currentCorpus = new ReplaySubject<CorpusMC>(1); textRangePage.corpusService.currentCorpus.next(corpus); const valueList: number[] = []; @@ -351,13 +363,13 @@ describe('TextRangePage', () => { const addReferencesSpy: Spy = spyOn(textRangePage, 'addReferences').and.callFake(() => Promise.reject()); const initPageSpy: Spy = spyOn(textRangePage, 'initPage').and.returnValue(Promise.resolve()); textRangePage.corpusService.currentCorpus = new ReplaySubject<CorpusMC>(1); - textRangePage.corpusService.currentCorpus.next({citations: {}, source_urn: ''}); + textRangePage.corpusService.currentCorpus.next({citations: {}, source_urn: '', cid: -999}); textRangePage.ngOnInit().then(async () => { expect(initPageSpy).toHaveBeenCalledTimes(0); addReferencesSpy.and.returnValue(Promise.resolve()); await textRangePage.ngOnInit(); expect(initPageSpy).toHaveBeenCalledTimes(1); - textRangePage.corpusService.currentCorpus.next({citations: {1: new Citation()}, source_urn: ''}); + textRangePage.corpusService.currentCorpus.next({citations: {1: new Citation()}, source_urn: '', cid: -999}); await textRangePage.ngOnInit(); expect(initPageSpy).toHaveBeenCalledTimes(2); done(); @@ -380,7 +392,7 @@ describe('TextRangePage', () => { textRangePage.corpusService.initCurrentTextRange(); textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); textRangePage.corpusService.currentCorpus = new ReplaySubject<CorpusMC>(1); - textRangePage.corpusService.currentCorpus.next({citations: {1: new Citation()}, source_urn: ''}); + textRangePage.corpusService.currentCorpus.next({citations: {1: new Citation()}, source_urn: '', cid: -999}); textRangePage.resetCitations().then(() => { textRangePage.corpusService.currentTextRange.pipe(take(1)).subscribe((tr: TextRange) => { expect(tr.start[1]).toBeTruthy(); @@ -414,7 +426,7 @@ describe('TextRangePage', () => { textRangePage.helperService.applicationState.next(textRangePage.helperService.deepCopy(MockMC.applicationState)); textRangePage.corpusService.currentCorpus = new ReplaySubject<CorpusMC>(1); textRangePage.corpusService.currentCorpus.next({ - citations: {2: new Citation({subcitations: {2: new Citation()}})}, source_urn: '' + citations: {2: new Citation({subcitations: {2: new Citation()}})}, source_urn: '', cid: -999 }); textRangePage.showFurtherReferences(true).then(async () => { expect(addReferencesSpy).toHaveBeenCalledTimes(1); @@ -424,7 +436,8 @@ describe('TextRangePage', () => { textRangePage.corpusService.setCurrentTextRange(0, null, new TextRange({end: ['2'], start: ['2']})); textRangePage.corpusService.currentCorpus.next({ citations: {2: new Citation({subcitations: {}})}, - citation_level_2: CitationLevel.default.toString(), source_urn: '' + citation_level_2: CitationLevel.default.toString(), source_urn: '', + cid: -999 }); await textRangePage.showFurtherReferences(false); expect(addReferencesSpy).toHaveBeenCalledTimes(1); diff --git a/mc_frontend/src/app/upload-text/upload-text.component.ts b/mc_frontend/src/app/upload-text/upload-text.component.ts index 02e11db58a8f87e0a060ed06f578a77fa182272e..a7b6f72d0798e433d66fe7a7f96b183f68bb5eaf 100644 --- a/mc_frontend/src/app/upload-text/upload-text.component.ts +++ b/mc_frontend/src/app/upload-text/upload-text.component.ts @@ -38,7 +38,7 @@ export class UploadTextComponent implements OnInit { this.corpusService.getAnnotationsForCustomText().then((ar: AnnisResponse) => { this.corpusService.processAnnisResponse(ar); this.corpusService.currentUrn = ar.uri; - const corpus: CorpusMC = {source_urn: ar.uri, title: this.corpusService.myTextString}; + const corpus: CorpusMC = {source_urn: ar.uri, title: this.corpusService.myTextString, cid: -999}; this.corpusService.currentCorpus.next(corpus); this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => { state.mostRecentSetup = new TextData({