From 1ee4c86e39115efd967b7e5f3dc4d897e443f893 Mon Sep 17 00:00:00 2001 From: Konstantin Schulz <schulzkx@hu-berlin.de> Date: Tue, 27 Feb 2024 10:36:50 +0100 Subject: [PATCH] fix parameters for POST requests --- mc_backend/app.py | 8 +-- mc_backend/mcserver/__init__.py | 3 +- mc_backend/mcserver/__main__.py | 2 +- mc_backend/mcserver/app/__init__.py | 52 +++++++++++++------ mc_backend/tests.py | 11 ++-- mc_frontend/src/app/corpus.service.ts | 7 +-- .../exercise-parameters.page.ts | 12 ++--- mc_frontend/src/app/exercise.service.ts | 10 ++-- mc_frontend/src/app/helper.service.spec.ts | 4 +- mc_frontend/src/app/helper.service.ts | 15 ++++-- .../src/app/semantics/semantics.page.ts | 3 +- .../src/app/sequences/sequences.page.ts | 9 ++-- .../src/app/show-text/show-text.page.ts | 7 +-- mc_frontend/src/app/test/test.page.ts | 5 +- mc_frontend/src/app/vocabulary.service.ts | 3 +- 15 files changed, 79 insertions(+), 72 deletions(-) diff --git a/mc_backend/app.py b/mc_backend/app.py index 80aab64..031573d 100644 --- a/mc_backend/app.py +++ b/mc_backend/app.py @@ -1,5 +1,3 @@ -from pathlib import Path - import connexion from mcserver import get_app, get_cfg @@ -7,5 +5,7 @@ from mcserver import get_app, get_cfg app: connexion.App = get_app() if __name__ == "__main__": - app.run(import_string=f"{Path(__file__).stem}:app", host=get_cfg().HOST_IP_MCSERVER, port=get_cfg().HOST_PORT, - use_reloader=True) + # disable reloader (import_string) because it would result in multiple conflicting GraphANNIS instances + # exclude models_auto.py from watchfiles to prevent Uvicorn from running into an update/reload loop + app.run(host=get_cfg().HOST_IP_MCSERVER, port=get_cfg().HOST_PORT, + reload_excludes="models_auto.py") # import_string=f"{Path(__file__).stem}:app", diff --git a/mc_backend/mcserver/__init__.py b/mc_backend/mcserver/__init__.py index 53b89e2..d99c3db 100644 --- a/mc_backend/mcserver/__init__.py +++ b/mc_backend/mcserver/__init__.py @@ -20,5 +20,4 @@ def get_cfg() -> Type[Config]: if __name__ == "__main__": - # reloader has to be disabled because of a bug with Flask and multiprocessing - get_app().run(host=get_cfg().HOST_IP_MCSERVER, port=get_cfg().HOST_PORT, use_reloader=False) + get_app().run(host=get_cfg().HOST_IP_MCSERVER, port=get_cfg().HOST_PORT) diff --git a/mc_backend/mcserver/__main__.py b/mc_backend/mcserver/__main__.py index 08ecbc7..dc21b63 100644 --- a/mc_backend/mcserver/__main__.py +++ b/mc_backend/mcserver/__main__.py @@ -1,4 +1,4 @@ from mcserver import get_app, get_cfg if __name__ == "__main__": - get_app().run(host=get_cfg().HOST_IP_MCSERVER, port=get_cfg().HOST_PORT, use_reloader=False) + get_app().run(host=get_cfg().HOST_IP_MCSERVER, port=get_cfg().HOST_PORT) diff --git a/mc_backend/mcserver/app/__init__.py b/mc_backend/mcserver/app/__init__.py index 7089358..034b3fe 100644 --- a/mc_backend/mcserver/app/__init__.py +++ b/mc_backend/mcserver/app/__init__.py @@ -7,7 +7,7 @@ import uuid from logging.handlers import RotatingFileHandler from threading import Thread from time import strftime -from typing import Type +from typing import Type, List import connexion import open_alchemy import prance @@ -27,6 +27,14 @@ from starlette.middleware.cors import CORSMiddleware from mcserver.config import Config +class Specification: + def __init__(self, content: dict = None, models_last_modified_time: float = 0, + api_last_modified_time: float = 0): + self.content: dict = content + self.models_last_modified_time: float = models_last_modified_time + self.api_last_modified_time: float = api_last_modified_time + + def apply_event_handlers(app: FlaskApp): """Applies event handlers to a given Flask application, such as logging after requests or teardown logic.""" @@ -102,29 +110,44 @@ def full_init(app: Flask, cfg: Type[Config] = Config) -> None: start_updater(app) -def get_api_specification() -> dict: +def get_api_last_modified_time() -> float: + """Checks when the API specification was last modified.""" + return os.path.getmtime(Config.API_SPEC_MCSERVER_FILE_PATH) + + +def get_api_specification() -> Specification: """ Reads, parses and caches the OpenAPI specification including all shared models. """ parser: prance.ResolvingParser = prance.ResolvingParser(Config.API_SPEC_MCSERVER_FILE_PATH, lazy=True, strict=False) - api_last_modified_time: float = os.path.getmtime(Config.API_SPEC_MCSERVER_FILE_PATH) - models_last_modified_time: float = os.path.getmtime(Config.API_SPEC_MODELS_YAML_FILE_PATH) - lmt: float = api_last_modified_time + models_last_modified_time + spec: Specification = Specification(api_last_modified_time=get_api_last_modified_time(), + models_last_modified_time=get_models_last_modified_time()) if os.path.exists(Config.API_SPEC_CACHE_PATH): with open(Config.API_SPEC_CACHE_PATH, "rb") as f: - cache: dict = pickle.load(f) - if cache["lmt"] == lmt: - parser.specification = cache["spec"] - if not parser.specification: + content: dict = pickle.load(f) + # check for old data to enable backward compatibility + if "lmt" not in content.keys(): + cached_spec: Specification = Specification(content) + cached_time: List[float] = [cached_spec.api_last_modified_time, cached_spec.models_last_modified_time] + new_time: List[float] = [spec.api_last_modified_time, spec.models_last_modified_time] + if cached_time == new_time: + spec.content = cached_spec.content + if spec.content is None: parser.parse() + spec.content = parser.specification with open(Config.API_SPEC_CACHE_PATH, "wb+") as f: - pickle.dump(dict(lmt=lmt, spec=parser.specification), f) - return parser.specification + pickle.dump(spec.__dict__, f) + return spec + + +def get_models_last_modified_time() -> float: + """Checks when the model specification was last modified.""" + return os.path.getmtime(Config.API_SPEC_MODELS_YAML_FILE_PATH) 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) + spec: Specification = get_api_specification() + connexion_app.add_api(spec.content) apply_event_handlers(connexion_app) app: Flask = connexion_app.app # allow CORS requests for all API routes @@ -201,8 +224,7 @@ if not hasattr(open_alchemy.models, Config.DATABASE_TABLE_CORPUS): # initialize the database and models _BEFORE_ you add any APIs to your application init_yaml(Config.API_SPEC_MODELS_YAML_FILE_PATH, base=db.Model, models_filename=os.path.join(Config.MC_SERVER_DIRECTORY, "models_auto.py")) - -# import the models so we can access them from other parts of the app using imports from "app.models"; +# import the models, so we can access them from other parts of the app using imports from "app.models"; # this has to be at the bottom of the file from mcserver.app import models from mcserver.app import api diff --git a/mc_backend/tests.py b/mc_backend/tests.py index 3fee1da..e949e2d 100644 --- a/mc_backend/tests.py +++ b/mc_backend/tests.py @@ -35,7 +35,7 @@ from sqlalchemy.orm import session import mcserver from mcserver.app import create_app, db, start_updater, full_init, log_exception, get_api_specification, \ - init_app_common, create_postgres_database + init_app_common, create_postgres_database, Specification, get_models_last_modified_time, get_api_last_modified_time from mcserver.app.api.exerciseAPI import map_exercise_data_to_database, get_graph_data from mcserver.app.api.fileAPI import clean_tmp_folder from mcserver.app.api.h5pAPI import get_remote_exercise @@ -699,17 +699,16 @@ class CommonTestCase(unittest.TestCase): def test_get_api_specification(self): """ Reads, parses and caches the OpenAPI specification including all shared models. """ - spec: dict = get_api_specification() + spec: Specification = get_api_specification() os.remove(Config.API_SPEC_CACHE_PATH) with patch.object(mcserver.app.prance.ResolvingParser, "parse"): get_api_specification() self.assertTrue(os.path.exists(Config.API_SPEC_CACHE_PATH)) # restore the old cache, so that other unit tests can be initialized faster - api_last_modified_time: float = os.path.getmtime(Config.API_SPEC_MCSERVER_FILE_PATH) - models_last_modified_time: float = os.path.getmtime(Config.API_SPEC_MODELS_YAML_FILE_PATH) - lmt: float = api_last_modified_time + models_last_modified_time + spec.api_last_modified_time = get_api_last_modified_time() + spec.models_last_modified_time = get_models_last_modified_time() with open(Config.API_SPEC_CACHE_PATH, "wb+") as f: - pickle.dump(dict(lmt=lmt, spec=spec), f) + pickle.dump(spec.__dict__, f) def test_get_concept_network(self): """Extracts a network of words from vector data in an AI model.""" diff --git a/mc_frontend/src/app/corpus.service.ts b/mc_frontend/src/app/corpus.service.ts index 60eb9ce..e937936 100644 --- a/mc_frontend/src/app/corpus.service.ts +++ b/mc_frontend/src/app/corpus.service.ts @@ -170,11 +170,8 @@ export class CorpusService { const rtfe: RawTextFormExtension = { plain_text: this.currentText, }; - const formData: FormData = new FormData(); - Object.keys(rtfe).forEach((key: string) => { - formData.append(key, rtfe[key].toString()); - }); - this.helperService.makePostRequest(this.http, this.toastCtrl, url, formData).then((ar: AnnisResponse) => { + const body: HttpParams = this.helperService.getFormForPostRequest(rtfe); + this.helperService.makePostRequest(this.http, this.toastCtrl, url, body).then((ar: AnnisResponse) => { return resolve(ar); }, (error: HttpErrorResponse) => { return reject(error); diff --git a/mc_frontend/src/app/exercise-parameters/exercise-parameters.page.ts b/mc_frontend/src/app/exercise-parameters/exercise-parameters.page.ts index df90bae..32973ab 100644 --- a/mc_frontend/src/app/exercise-parameters/exercise-parameters.page.ts +++ b/mc_frontend/src/app/exercise-parameters/exercise-parameters.page.ts @@ -6,7 +6,7 @@ import { PhenomenonTranslation } from '../models/enum'; import {NavController, ToastController} from '@ionic/angular'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; import {Component, OnInit} from '@angular/core'; import {TranslateService} from '@ngx-translate/core'; import {ExerciseService} from 'src/app/exercise.service'; @@ -132,10 +132,7 @@ export class ExerciseParametersPage implements OnInit { getH5Pexercise(ef: ExerciseForm): Promise<void> { return new Promise<void>((resolve, reject) => { const url: string = configMC.backendBaseUrl + configMC.backendApiExercisePath; - const formData = new FormData(); - Object.keys(ef).forEach((key: string) => { - formData.append(key, ef[key]); - }); + const formData: HttpParams = this.helperService.getFormForPostRequest(ef); this.helperService.makePostRequest(this.http, this.toastCtrl, url, formData).then((ar: AnnisResponse) => { this.helperService.applicationState.pipe(take(1)).subscribe((as: ApplicationState) => { as.previewAnnisResponse = this.corpusService.previewAnnisResponse = ar; @@ -157,11 +154,8 @@ export class ExerciseParametersPage implements OnInit { search_values: searchValues, urn: this.corpusService.currentUrn }; - const formData = new FormData(); - Object.keys(kf).forEach((key: string) => { - formData.append(key, kf[key]); - }); const kwicUrl: string = configMC.backendBaseUrl + configMC.backendApiKwicPath; + const formData: HttpParams = this.helperService.getFormForPostRequest(kf); this.helperService.makePostRequest(this.http, this.toastCtrl, kwicUrl, formData).then((svgString: string) => { this.exerciseService.kwicGraphs = svgString; this.helperService.goToPage(this.navCtrl, configMC.pageUrlKwic).then(); diff --git a/mc_frontend/src/app/exercise.service.ts b/mc_frontend/src/app/exercise.service.ts index 4a944e1..cf384dd 100644 --- a/mc_frontend/src/app/exercise.service.ts +++ b/mc_frontend/src/app/exercise.service.ts @@ -137,12 +137,9 @@ export class ExerciseService { lang: this.currentExerciseParams.language.toString(), solution_indices: indices }; - const formData: FormData = new FormData(); - Object.keys(h5pForm).forEach((key: string) => { - formData.append(key, h5pForm[key].toString()); - }); - const options = {responseType: 'blob' as const}; + const options: object = {responseType: 'blob' as const}; const errorMsg: string = HelperService.generalErrorAlertMessage; + const formData: HttpParams = this.helperService.getFormForPostRequest(h5pForm); this.helperService.makePostRequest(this.http, this.toastCtrl, url, formData, errorMsg, options) .then((result: Blob) => { const fileName = this.currentExerciseParams.type + '.h5p'; @@ -338,8 +335,7 @@ export class ExerciseService { sendData(result: TestResultMC): Promise<void> { return new Promise<void>((resolve, reject) => { const fileUrl: string = configMC.backendBaseUrl + configMC.backendApiFilePath; - const formData = new FormData(); - formData.append('learning_result', JSON.stringify({0: result.statement})); + const formData: HttpParams = new HttpParams().set('learning_result', JSON.stringify({0: result.statement})); this.helperService.makePostRequest(this.http, this.toastCtrl, fileUrl, formData, '').then(() => { return resolve(); }, () => { diff --git a/mc_frontend/src/app/helper.service.spec.ts b/mc_frontend/src/app/helper.service.spec.ts index e9fb48b..90fe76d 100644 --- a/mc_frontend/src/app/helper.service.spec.ts +++ b/mc_frontend/src/app/helper.service.spec.ts @@ -209,11 +209,11 @@ describe('HelperService', () => { const toastCtrl: ToastController = TestBed.inject(ToastController); spyOn(toastCtrl, 'create').and.returnValue(Promise.resolve({present: () => Promise.resolve()} as HTMLIonToastElement)); const httpSpy: Spy = spyOn(helperService.http, 'post').and.returnValue(of(0)); - helperService.makePostRequest(helperService.http, toastCtrl, '', new FormData()).then((result: number) => { + helperService.makePostRequest(helperService.http, toastCtrl, '', new HttpParams()).then((result: number) => { expect(httpSpy).toHaveBeenCalledTimes(1); expect(result).toBe(0); httpSpy.and.returnValue(new Observable(subscriber => subscriber.error(new HttpErrorResponse({status: 500})))); - helperService.makePostRequest(helperService.http, toastCtrl, '', new FormData()).then(() => { + helperService.makePostRequest(helperService.http, toastCtrl, '', new HttpParams()).then(() => { }, (error: HttpErrorResponse) => { expect(error.status).toBe(500); done(); diff --git a/mc_frontend/src/app/helper.service.ts b/mc_frontend/src/app/helper.service.ts index 16e666b..0cd656a 100644 --- a/mc_frontend/src/app/helper.service.ts +++ b/mc_frontend/src/app/helper.service.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-string-literal */ -import {HttpClient, HttpErrorResponse, HttpParams} from '@angular/common/http'; +import {HttpClient, HttpErrorResponse, HttpHeaders, HttpParams} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {NavController, ToastController} from '@ionic/angular'; import {ApplicationState} from 'src/app/models/applicationState'; @@ -207,6 +207,14 @@ export class HelperService { }); } + getFormForPostRequest(form: object): HttpParams { + let params: HttpParams = new HttpParams(); + Object.keys(form).forEach((key: string) => { + params = params.set(key, form[key]); + }); + return params; + } + getH5P(): any { return H5P; } @@ -324,12 +332,13 @@ export class HelperService { })); } - makePostRequest(http: HttpClient, toastCtrl: ToastController, url: string, formData: FormData, + makePostRequest(http: HttpClient, toastCtrl: ToastController, url: string, body: HttpParams, errorMessage: string = HelperService.generalErrorAlertMessage, options: any = {}): Promise<any> { return new Promise(((resolve, reject) => { this.currentError = null; this.openRequests.push(url); - http.post(url, formData, options).subscribe((result: any) => { + options.headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'); + http.post(url, body, options).subscribe((result: any) => { this.openRequests.splice(this.openRequests.indexOf(url), 1); return resolve(result); }, async (error: HttpErrorResponse) => { diff --git a/mc_frontend/src/app/semantics/semantics.page.ts b/mc_frontend/src/app/semantics/semantics.page.ts index 98f13e3..bf6e74b 100644 --- a/mc_frontend/src/app/semantics/semantics.page.ts +++ b/mc_frontend/src/app/semantics/semantics.page.ts @@ -43,8 +43,7 @@ export class SemanticsPage implements AfterViewInit { search_regex: this.searchRegex, nearest_neighbor_count: Math.max(Math.round(this.nearestNeighborCount), 1) }; - const formData: FormData = new FormData(); - Object.keys(vnf).forEach((key: string) => formData.append(key, vnf[key])); + const formData: HttpParams = this.helperService.getFormForPostRequest(vnf); const semanticsUrl: string = configMC.backendBaseUrl + configMC.backendApiVectorNetworkPath; this.similarContexts = []; this.helperService.makePostRequest(this.http, this.toastCtrl, semanticsUrl, formData).then((contexts: string[][]) => { diff --git a/mc_frontend/src/app/sequences/sequences.page.ts b/mc_frontend/src/app/sequences/sequences.page.ts index e283b72..045b583 100644 --- a/mc_frontend/src/app/sequences/sequences.page.ts +++ b/mc_frontend/src/app/sequences/sequences.page.ts @@ -4,7 +4,7 @@ import {NavController, ToastController} from '@ionic/angular'; import configMC from '../../configMC'; import {ExerciseTypePath, ZenodoRecord} from '../../../openapi'; import {ExerciseService} from '../exercise.service'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; import {ZenodoRecordMC} from '../models/zenodoRecordMC'; import {ExerciseParams} from '../models/exerciseParams'; import {TranslateService} from '@ngx-translate/core'; @@ -86,11 +86,8 @@ export class SequencesPage implements OnInit { } else { const url: string = configMC.backendBaseUrl + configMC.backendApiZenodoPath; const zenodoForm: ZenodoForm = {record_id: +zr.record.identifier[0].split('.').slice(-1)[0]}; - const formData: FormData = new FormData(); - Object.keys(zenodoForm).forEach((key: string) => { - formData.append(key, zenodoForm[key]); - }); - this.helperService.makePostRequest(this.http, this.toastCtrl, url, formData).then((uris: string[]) => { + const params: HttpParams = this.helperService.getFormForPostRequest(zenodoForm); + this.helperService.makePostRequest(this.http, this.toastCtrl, url, params).then((uris: string[]) => { zr.fileURIs = uris; zr.showFiles = !zr.showFiles; return resolve(); diff --git a/mc_frontend/src/app/show-text/show-text.page.ts b/mc_frontend/src/app/show-text/show-text.page.ts index 12f4da3..9cb4ab5 100644 --- a/mc_frontend/src/app/show-text/show-text.page.ts +++ b/mc_frontend/src/app/show-text/show-text.page.ts @@ -6,7 +6,7 @@ import {VocabularyService} from 'src/app/vocabulary.service'; import {ExerciseService} from 'src/app/exercise.service'; import {HelperService} from 'src/app/helper.service'; import {TranslateService} from '@ngx-translate/core'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; import {CorpusMC} from '../models/corpusMC'; import {take} from 'rxjs/operators'; import configMC from '../../configMC'; @@ -64,13 +64,10 @@ export class ShowTextPage implements OnInit { this.corpusService.currentCorpus.pipe(take(1)).subscribe((cc: CorpusMC) => { const authorTitle: string = cc.author + ', ' + cc.title; content = `<p>${authorTitle} ${this.corpusService.currentUrn.split(':').slice(-1)[0]}</p>` + content; - const formData = new FormData(); const ff: FileForm = { file_type: fileType as FileType, html_content: content, urn: this.corpusService.currentUrn }; - Object.keys(ff).forEach((key: string) => { - formData.append(key, ff[key]); - }); + let formData: HttpParams = this.helperService.getFormForPostRequest(ff); const url: string = configMC.backendBaseUrl + configMC.backendApiFilePath; this.isDownloading = true; this.helperService.makePostRequest(this.http, this.toastCtrl, url, formData) diff --git a/mc_frontend/src/app/test/test.page.ts b/mc_frontend/src/app/test/test.page.ts index ec013f8..e07042c 100644 --- a/mc_frontend/src/app/test/test.page.ts +++ b/mc_frontend/src/app/test/test.page.ts @@ -10,7 +10,7 @@ import {ConfirmCancelPage} from 'src/app/confirm-cancel/confirm-cancel.page'; import {ExercisePart} from 'src/app/models/exercisePart'; import Activity from 'src/app/models/xAPI/Activity'; import LanguageMap from 'src/app/models/xAPI/LanguageMap'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; import Context from 'src/app/models/xAPI/Context'; import {TestResultMC} from 'src/app/models/testResultMC'; import {ExerciseService} from 'src/app/exercise.service'; @@ -380,12 +380,11 @@ export class TestPage implements OnDestroy, OnInit { return resolve(); } const fileUrl: string = configMC.backendBaseUrl + configMC.backendApiFilePath; - const formData = new FormData(); // tslint:disable-next-line:prefer-const let learningResult: object = {}; Object.keys(this.vocService.currentTestResults) .forEach(i => learningResult[i] = this.vocService.currentTestResults[i].statement); - formData.append('learning_result', JSON.stringify(learningResult)); + const formData: HttpParams = new HttpParams().set('learning_result', JSON.stringify(learningResult)); this.helperService.makePostRequest(this.http, this.toastCtrl, fileUrl, formData).then(async () => { this.wasDataSent = true; this.helperService.showToast(this.toastCtrl, this.corpusService.dataSentSuccessMessage).then(); diff --git a/mc_frontend/src/app/vocabulary.service.ts b/mc_frontend/src/app/vocabulary.service.ts index c995037..97d6888 100644 --- a/mc_frontend/src/app/vocabulary.service.ts +++ b/mc_frontend/src/app/vocabulary.service.ts @@ -103,8 +103,7 @@ export class VocabularyService implements OnInit { query_urn: queryUrn, vocabulary: this.currentReferenceVocabulary }; - const formData: FormData = new FormData(); - Object.keys(vf).forEach((key: string) => formData.append(key, vf[key])); + let formData: HttpParams = this.helperService.getFormForPostRequest(vf); this.helperService.makePostRequest(this.http, this.toastCtrl, url, formData).then((result: AnnisResponse) => { return resolve(result); }, (error: HttpErrorResponse) => { -- GitLab