From dba47c3fd09405c326ce9d3c9b17434703709777 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Sat, 27 Jul 2024 09:50:17 +0200 Subject: [PATCH 01/10] Fix: key error --- backend/aethel_db/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/aethel_db/views.py b/backend/aethel_db/views.py index a4a3a99..4e2f7b9 100644 --- a/backend/aethel_db/views.py +++ b/backend/aethel_db/views.py @@ -41,7 +41,7 @@ class AethelListItem: def serialize(self): out = asdict(self) - out['samples'] = sorted(out['samples'], key=lambda sample: len(sample['sentence'])) + out['samples'] = sorted(out['samples'], key=lambda sample: len(sample['phrases'])) return out From 962ce8acac1d2a2a89dd54bcd2dd48077bd84bb9 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Tue, 13 Aug 2024 09:56:28 +0200 Subject: [PATCH 02/10] New Aethel types in frontend --- .../app/shared/services/aethel-api.service.ts | 17 +++++++++++++---- frontend/src/app/shared/types.ts | 8 ++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/shared/services/aethel-api.service.ts b/frontend/src/app/shared/services/aethel-api.service.ts index 00b409c..91efa1d 100644 --- a/frontend/src/app/shared/services/aethel-api.service.ts +++ b/frontend/src/app/shared/services/aethel-api.service.ts @@ -13,7 +13,7 @@ import { throttleTime, } from "rxjs"; import { environment } from "src/environments/environment"; -import { AethelDetail, AethelListReturn } from "../types"; +import { AethelDetail, AethelInput, AethelListReturn } from "../types"; import { ErrorHandlerService } from "./error-handler.service"; import { ParsePortDataService } from "./ParsePortDataService"; @@ -21,9 +21,9 @@ import { ParsePortDataService } from "./ParsePortDataService"; providedIn: "root", }) export class AethelApiService - implements ParsePortDataService + implements ParsePortDataService { - input$ = new Subject(); + input$ = new Subject(); throttledInput$ = this.input$.pipe( distinctUntilChanged(), @@ -37,7 +37,16 @@ export class AethelApiService "Content-Type": "application/json", }); - const params = new HttpParams().set("query", input); + + let params = new HttpParams() + + if (input.word) { + params = params.set('word', input.word) + } + + if (input.type) { + params = params.set('type', input.type) + } return this.http .get(`${environment.apiUrl}aethel/`, { diff --git a/frontend/src/app/shared/types.ts b/frontend/src/app/shared/types.ts index 6fc9864..99d3b9e 100644 --- a/frontend/src/app/shared/types.ts +++ b/frontend/src/app/shared/types.ts @@ -35,6 +35,14 @@ export interface SpindleReturn { lexical_phrases: LexicalPhrase[]; proof: Record | null; } + +export type AethelMode = "word" | "type" | "word-and-type"; + +export interface AethelInput { + word?: string; + type?: string; +} + export interface AethelListReturnItem { lemma: string; word: string; From 94cf08f1f1b0bffbb6a287b96fd056ed4c25de08 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Tue, 13 Aug 2024 10:01:36 +0200 Subject: [PATCH 03/10] Working frontend --- frontend/src/app/aethel/aethel.component.ts | 11 +++--- frontend/src/app/sample/sample.component.html | 12 +++++-- frontend/src/app/sample/sample.component.ts | 36 ++++++++++++------- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/aethel/aethel.component.ts b/frontend/src/app/aethel/aethel.component.ts index 20ec49b..4f53b1c 100644 --- a/frontend/src/app/aethel/aethel.component.ts +++ b/frontend/src/app/aethel/aethel.component.ts @@ -63,14 +63,17 @@ export class AethelComponent implements OnInit { // Whenever the query parameter changes, we run a new query. this.route.queryParams .pipe( - map((queryParams) => queryParams["query"]), isNonNull(), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef), ) .subscribe((query) => { - this.form.controls.aethelInput.setValue(query); - this.apiService.input$.next(query); + const word = query['word']; + const type = query['type'] + if (word) { + this.form.controls.aethelInput.setValue(word); + } + this.apiService.input$.next({ word, type }); }); } @@ -91,7 +94,7 @@ export class AethelComponent implements OnInit { private updateUrl(query: string): void { // This does not actually refresh the page because it just adds parameters to the current route. // It just updates the URL in the browser, triggering a new query. - const url = this.router.createUrlTree([], { relativeTo: this.route, queryParams: { query } }).toString(); + const url = this.router.createUrlTree([], { relativeTo: this.route, queryParams: { word: query } }).toString(); this.router.navigateByUrl(url); } diff --git a/frontend/src/app/sample/sample.component.html b/frontend/src/app/sample/sample.component.html index 54f205d..361609e 100644 --- a/frontend/src/app/sample/sample.component.html +++ b/frontend/src/app/sample/sample.component.html @@ -39,9 +39,15 @@ - @if (showButton(phrase.items)) { - + + } diff --git a/frontend/src/app/sample/sample.component.ts b/frontend/src/app/sample/sample.component.ts index 7135a9c..3ab68c3 100644 --- a/frontend/src/app/sample/sample.component.ts +++ b/frontend/src/app/sample/sample.component.ts @@ -1,8 +1,8 @@ import { Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute, Params, Router } from "@angular/router"; import { AethelApiService } from "../shared/services/aethel-api.service"; import { map } from "rxjs"; -import { LexicalPhrase } from "../shared/types"; +import { AethelMode, LexicalPhrase } from "../shared/types"; import { isNonNull } from "../shared/operators/IsNonNull"; import { faArrowLeft } from "@fortawesome/free-solid-svg-icons"; import { Location } from "@angular/common"; @@ -17,30 +17,27 @@ export class SampleComponent { private sample$ = this.apiService.sampleResult$(this.sampleName); public sampleResult$ = this.sample$.pipe( map((response) => response?.result), - isNonNull() + isNonNull(), ); public icons = { arrowLeft: faArrowLeft, - } + }; constructor( private route: ActivatedRoute, private apiService: AethelApiService, private router: Router, - private location: Location + private location: Location, ) {} - public routeToAethel(items: LexicalPhrase["items"]): void { - const combined = items.map((item) => item.word).join(" "); - this.router.navigate(["/aethel"], { - queryParams: { - query: combined, - }, - }); + public searchAethel(phrase: LexicalPhrase, mode: AethelMode): void { + const queryParams = this.formatQueryParams(phrase, mode); + this.router.navigate(["/aethel"], { queryParams }); } - public showButton(items: LexicalPhrase["items"]): boolean { + public showButtons(items: LexicalPhrase["items"]): boolean { + // Buttons are hidden if the phrase consists of too few characters. const combined = items.map((item) => item.word).join(" "); return combined.length > 2; } @@ -48,4 +45,17 @@ export class SampleComponent { public goBack(): void { this.location.back(); } + + private formatQueryParams(phrase: LexicalPhrase, mode: AethelMode): Params { + const queryParams: Params = {}; + if (mode === "word" || mode === "word-and-type") { + queryParams["word"] = phrase.items + .map((item) => item.word) + .join(" "); + } + if (mode === "type" || mode === "word-and-type") { + queryParams["type"] = phrase.type; + } + return queryParams; + } } From 7daa93519b997b83234f08ee152a8d2bb9a8d1b1 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Tue, 13 Aug 2024 10:01:58 +0200 Subject: [PATCH 04/10] Ugly, working backend --- backend/aethel_db/search.py | 32 ++++++++----- backend/aethel_db/views.py | 90 ++++++++++++++++++------------------- 2 files changed, 65 insertions(+), 57 deletions(-) diff --git a/backend/aethel_db/search.py b/backend/aethel_db/search.py index eb6c110..320ef78 100644 --- a/backend/aethel_db/search.py +++ b/backend/aethel_db/search.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Iterable, Callable, Iterator -from aethel.frontend import Sample +from aethel.frontend import Sample, LexicalPhrase, LexicalItem +from aethel.mill.types import type_repr # The following methods and classes have been extracted from aethel.scripts.search (not part of the published library), with some minor customisations / simplifications. @@ -9,26 +10,33 @@ def search(bank: Iterable[Sample], query: Callable[[Sample], bool]) -> Iterator[ return filter(query, bank) -def in_lemma(query_string: str) -> Query: +def get_query(word_input: str | None = None, type_input: str | None = None) -> Query: def f(sample: Sample) -> bool: return any( - query_string.lower() in item.lemma.lower() + match_word_with_phrase(phrase, word_input) or match_type(phrase, type_input) for phrase in sample.lexical_phrases - for item in phrase.items ) return Query(f) -def in_word(query_string: str) -> Query: - def f(sample: Sample) -> bool: - return any( - query_string.lower() in item.word.lower() - for phrase in sample.lexical_phrases - for item in phrase.items - ) +def match_type(phrase: LexicalPhrase, type_input: str | None) -> bool: + if type_input is None: + return False + return type_input == type_repr(phrase.type) - return Query(f) + +def match_word_with_phrase(phrase: LexicalPhrase, word_input: str | None) -> bool: + if word_input is None: + return False + return any(match_word_with_item(item, word_input) for item in phrase.items) + + +def match_word_with_item(item: LexicalItem, word_input: str) -> bool: + return ( + word_input.lower() in item.lemma.lower() + or word_input.lower() in item.word.lower() + ) class Query: diff --git a/backend/aethel_db/views.py b/backend/aethel_db/views.py index 4e2f7b9..8360fd8 100644 --- a/backend/aethel_db/views.py +++ b/backend/aethel_db/views.py @@ -5,15 +5,16 @@ from django.http import HttpRequest, JsonResponse from rest_framework import status from rest_framework.views import APIView -from aethel.frontend import LexicalItem from spindle.utils import serialize_phrases_with_infix_notation -from aethel_db.search import search, in_lemma, in_word +from aethel_db.search import ( + match_type, + match_word_with_item, + match_word_with_phrase, + search, + get_query, +) from aethel.frontend import Sample - -from aethel.frontend import LexicalItem - from .models import dataset -from .search import search, in_lemma, in_word def aethel_status(): @@ -41,7 +42,9 @@ class AethelListItem: def serialize(self): out = asdict(self) - out['samples'] = sorted(out['samples'], key=lambda sample: len(sample['phrases'])) + out["samples"] = sorted( + out["samples"], key=lambda sample: len(sample["phrases"]) + ) return out @@ -79,55 +82,52 @@ def json_response(self) -> JsonResponse: class AethelQueryView(APIView): def get(self, request: HttpRequest) -> JsonResponse: - query_input = self.request.query_params.get("query", None) - if query_input is None or len(query_input) < 3: - return AethelListResponse().json_response() + word_input = self.request.query_params.get("word", None) + type_input = self.request.query_params.get("type", None) - def item_contains_query_string(item: LexicalItem, query_input: str) -> bool: - """ - Checks if a LexicalItem contains a given input string in its word or its lemma. - """ - return ( - query_input.lower() in item.lemma.lower() - or query_input.lower() in item.word.lower() - ) + # We only search for strings of 3 or more characters. + if word_input is not None and len(word_input) < 3: + return AethelListResponse().json_response() response_object = AethelListResponse() - # First we select all relevant samples from the dataset that contain the query string. + # First we select all relevant samples from the dataset that contain the queried word and/or type. query_result = search( bank=dataset.samples, - query=in_word(query_input) | in_lemma(query_input), + query=get_query(word_input, type_input), ) + # Format results for sample in query_result: for phrase_index, phrase in enumerate(sample.lexical_phrases): - for item in phrase.items: - if item_contains_query_string(item, query_input): - result = response_object.get_or_create_result( - lemma=item.lemma, word=item.word, type=str(phrase.type) - ) - - # Check whether we have already added this sample for this result - existing_sample = next( - (s for s in result.samples if s.name == sample.name), - None, + if not match_word_with_phrase(phrase, word_input) and not match_type(phrase, type_input): + continue + + phrase_word = ' '.join([item.word for item in phrase.items]) + phrase_lemma = ' '.join([item.lemma for item in phrase.items]) + + result = response_object.get_or_create_result( + lemma=phrase_lemma, word=phrase_word, type=str(phrase.type) + ) + + # Check whether we have already added this sample for this result + existing_sample = next( + (s for s in result.samples if s.name == sample.name), + None, + ) + + if existing_sample: + existing_sample.phrases[phrase_index].highlight = True + else: + new_sample = AethelListSample(name=sample.name, phrases=[]) + for index, sample_phrase in enumerate(sample.lexical_phrases): + highlighted = index == phrase_index + new_phrase = AethelSamplePhrase( + display=sample_phrase.string, + highlight=highlighted, ) - - if existing_sample: - existing_sample.phrases[phrase_index].highlight = True - else: - new_sample = AethelListSample(name=sample.name, phrases=[]) - for index, sample_phrase in enumerate( - sample.lexical_phrases - ): - highlighted = index == phrase_index - new_phrase = AethelSamplePhrase( - display=sample_phrase.string, - highlight=highlighted, - ) - new_sample.phrases.append(new_phrase) - result.samples.append(new_sample) + new_sample.phrases.append(new_phrase) + result.samples.append(new_sample) return response_object.json_response() From 26aedb3a55c411d7a0f23144b181e83a81994b25 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Tue, 13 Aug 2024 10:19:05 +0200 Subject: [PATCH 05/10] Refactor filter signatures --- backend/aethel_db/search.py | 12 ++++-------- backend/aethel_db/views.py | 7 +++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/backend/aethel_db/search.py b/backend/aethel_db/search.py index 320ef78..0a873f5 100644 --- a/backend/aethel_db/search.py +++ b/backend/aethel_db/search.py @@ -13,22 +13,18 @@ def search(bank: Iterable[Sample], query: Callable[[Sample], bool]) -> Iterator[ def get_query(word_input: str | None = None, type_input: str | None = None) -> Query: def f(sample: Sample) -> bool: return any( - match_word_with_phrase(phrase, word_input) or match_type(phrase, type_input) + (word_input and match_word_with_phrase(phrase, word_input)) or + (type_input and match_type_with_phrase(phrase, type_input)) for phrase in sample.lexical_phrases ) - return Query(f) -def match_type(phrase: LexicalPhrase, type_input: str | None) -> bool: - if type_input is None: - return False +def match_type_with_phrase(phrase: LexicalPhrase, type_input: str) -> bool: return type_input == type_repr(phrase.type) -def match_word_with_phrase(phrase: LexicalPhrase, word_input: str | None) -> bool: - if word_input is None: - return False +def match_word_with_phrase(phrase: LexicalPhrase, word_input: str) -> bool: return any(match_word_with_item(item, word_input) for item in phrase.items) diff --git a/backend/aethel_db/views.py b/backend/aethel_db/views.py index 8360fd8..b688ed8 100644 --- a/backend/aethel_db/views.py +++ b/backend/aethel_db/views.py @@ -7,7 +7,7 @@ from rest_framework.views import APIView from spindle.utils import serialize_phrases_with_infix_notation from aethel_db.search import ( - match_type, + match_type_with_phrase, match_word_with_item, match_word_with_phrase, search, @@ -100,7 +100,10 @@ def get(self, request: HttpRequest) -> JsonResponse: # Format results for sample in query_result: for phrase_index, phrase in enumerate(sample.lexical_phrases): - if not match_word_with_phrase(phrase, word_input) and not match_type(phrase, type_input): + word_match = word_input and match_word_with_phrase(phrase, word_input) + type_match = type_input and match_type_with_phrase(phrase, type_input) + + if not (word_match or type_match): continue phrase_word = ' '.join([item.word for item in phrase.items]) From e7099d498a49963ae0b0e6df7058ba6125ec0453 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Tue, 13 Aug 2024 10:39:46 +0200 Subject: [PATCH 06/10] Remove unnecessary pre-filtering --- backend/aethel_db/search.py | 51 +------------------------------------ backend/aethel_db/views.py | 21 ++++----------- 2 files changed, 6 insertions(+), 66 deletions(-) diff --git a/backend/aethel_db/search.py b/backend/aethel_db/search.py index 0a873f5..3cb9874 100644 --- a/backend/aethel_db/search.py +++ b/backend/aethel_db/search.py @@ -1,24 +1,7 @@ from __future__ import annotations -from typing import Iterable, Callable, Iterator -from aethel.frontend import Sample, LexicalPhrase, LexicalItem +from aethel.frontend import LexicalPhrase, LexicalItem from aethel.mill.types import type_repr -# The following methods and classes have been extracted from aethel.scripts.search (not part of the published library), with some minor customisations / simplifications. - - -def search(bank: Iterable[Sample], query: Callable[[Sample], bool]) -> Iterator[Sample]: - return filter(query, bank) - - -def get_query(word_input: str | None = None, type_input: str | None = None) -> Query: - def f(sample: Sample) -> bool: - return any( - (word_input and match_word_with_phrase(phrase, word_input)) or - (type_input and match_type_with_phrase(phrase, type_input)) - for phrase in sample.lexical_phrases - ) - return Query(f) - def match_type_with_phrase(phrase: LexicalPhrase, type_input: str) -> bool: return type_input == type_repr(phrase.type) @@ -33,35 +16,3 @@ def match_word_with_item(item: LexicalItem, word_input: str) -> bool: word_input.lower() in item.lemma.lower() or word_input.lower() in item.word.lower() ) - - -class Query: - def __init__(self, fn: Callable[[Sample], bool]): - self.fn = fn - - def __and__(self, other: Query) -> Query: - def f(sample: Sample) -> bool: - return self.fn(sample) and other.fn(sample) - - return Query(f) - - def __or__(self, other) -> Query: - def f(sample: Sample) -> bool: - return self.fn(sample) or other.fn(sample) - - return Query(f) - - def __invert__(self) -> Query: - def f(sample: Sample) -> bool: - return not self.fn(sample) - - return Query(f) - - def __xor__(self, other) -> Query: - def f(sample: Sample) -> bool: - return self.fn(sample) ^ other.fn(sample) - - return Query(f) - - def __call__(self, sample: Sample) -> bool: - return self.fn(sample) diff --git a/backend/aethel_db/views.py b/backend/aethel_db/views.py index b688ed8..5bcb0ce 100644 --- a/backend/aethel_db/views.py +++ b/backend/aethel_db/views.py @@ -8,10 +8,7 @@ from spindle.utils import serialize_phrases_with_infix_notation from aethel_db.search import ( match_type_with_phrase, - match_word_with_item, match_word_with_phrase, - search, - get_query, ) from aethel.frontend import Sample from .models import dataset @@ -91,29 +88,21 @@ def get(self, request: HttpRequest) -> JsonResponse: response_object = AethelListResponse() - # First we select all relevant samples from the dataset that contain the queried word and/or type. - query_result = search( - bank=dataset.samples, - query=get_query(word_input, type_input), - ) - - # Format results - for sample in query_result: + for sample in dataset.samples: for phrase_index, phrase in enumerate(sample.lexical_phrases): word_match = word_input and match_word_with_phrase(phrase, word_input) - type_match = type_input and match_type_with_phrase(phrase, type_input) - + type_match = type_input and match_type_with_phrase(phrase, type_input) if not (word_match or type_match): continue - phrase_word = ' '.join([item.word for item in phrase.items]) - phrase_lemma = ' '.join([item.lemma for item in phrase.items]) + phrase_word = " ".join([item.word for item in phrase.items]) + phrase_lemma = " ".join([item.lemma for item in phrase.items]) result = response_object.get_or_create_result( lemma=phrase_lemma, word=phrase_word, type=str(phrase.type) ) - # Check whether we have already added this sample for this result + # Check whether we have already added this sample for this result. existing_sample = next( (s for s in result.samples if s.name == sample.name), None, From 703f3503a21e6fd24f4dcd0b5709620b6eaeb4b6 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Tue, 13 Aug 2024 11:25:06 +0200 Subject: [PATCH 07/10] Fix tests --- .../src/app/aethel/aethel.component.spec.ts | 18 +++++-- .../src/app/sample/sample.component.spec.ts | 47 ++++++++++++++----- .../app/shared/services/aethel-api.service.ts | 9 ++-- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/aethel/aethel.component.spec.ts b/frontend/src/app/aethel/aethel.component.spec.ts index 431bb4b..6ebfb7c 100644 --- a/frontend/src/app/aethel/aethel.component.spec.ts +++ b/frontend/src/app/aethel/aethel.component.spec.ts @@ -6,10 +6,12 @@ import { ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute, Router, RouterModule } from "@angular/router"; import { routes } from "../routes"; import { of } from "rxjs"; +import { AethelApiService } from "../shared/services/aethel-api.service"; describe("AethelComponent", () => { let component: AethelComponent; let fixture: ComponentFixture; + let apiService: AethelApiService; let httpController: HttpTestingController; let route: ActivatedRoute; let router: Router; @@ -28,6 +30,7 @@ describe("AethelComponent", () => { route = TestBed.inject(ActivatedRoute); router = TestBed.inject(Router); fixture = TestBed.createComponent(AethelComponent); + apiService = TestBed.inject(AethelApiService); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -43,17 +46,24 @@ describe("AethelComponent", () => { httpController.expectNone("/api/aethel"); }); - it("should request data when there is a query parameter on init", () => { - route.queryParams = of({ query: "test" }); + it("should insert data into the form when there is a 'word' query parameter", () => { + route.queryParams = of({ word: "test" }); component.ngOnInit(); expect(component.form.controls.aethelInput.value).toBe("test"); - httpController.expectOne("/api/aethel/?query=test"); + }); + + it("should pass query param data to the API service", () => { + apiService.input$.subscribe(input => { + expect(input.word).toBe("test3"); + }); + route.queryParams = of({ word: "test3" }); + component.ngOnInit(); }); it("should react to form submissions", () => { const navigatorSpy = spyOn(router, "navigateByUrl"); component.form.controls.aethelInput.setValue("test-two"); component.submit(); - expect(navigatorSpy).toHaveBeenCalledWith("/?query=test-two"); + expect(navigatorSpy).toHaveBeenCalledWith("/?word=test-two"); }); }); diff --git a/frontend/src/app/sample/sample.component.spec.ts b/frontend/src/app/sample/sample.component.spec.ts index fe14d3d..e21d797 100644 --- a/frontend/src/app/sample/sample.component.spec.ts +++ b/frontend/src/app/sample/sample.component.spec.ts @@ -16,6 +16,18 @@ import { By } from "@angular/platform-browser"; import { ProofPipe } from "../shared/pipes/proof.pipe"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +const fakePhrase: LexicalPhrase = { + type: "cheese->tosti", + items: [ + { + word: "cheeses", + lemma: "tostis", + pos: "TOSTI", + pt: "CHEESE", + }, + ], +}; + describe("SampleComponent", () => { let component: SampleComponent; let fixture: ComponentFixture; @@ -27,7 +39,11 @@ describe("SampleComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [SampleComponent, ProofPipe], - imports: [HttpClientTestingModule, FontAwesomeModule, RouterModule.forRoot(routes)], + imports: [ + HttpClientTestingModule, + FontAwesomeModule, + RouterModule.forRoot(routes), + ], providers: [ { provide: ActivatedRoute, @@ -53,20 +69,27 @@ describe("SampleComponent", () => { expect(component).toBeTruthy(); }); - it("should construct a valid route", () => { + it("should construct a valid route for word search", () => { const spy = spyOn(router, "navigate"); - const items: LexicalPhrase["items"] = [ - { - lemma: "test", - pos: "2", - pt: "2", - word: "testQuery", - }, - ]; + component.searchAethel(fakePhrase, 'word'); + expect(spy).toHaveBeenCalledOnceWith(["/aethel"], { + queryParams: { word: "cheeses" }, + }); + }); - component.routeToAethel(items); + it("should construct a valid route for type search", () => { + const spy = spyOn(router, "navigate"); + component.searchAethel(fakePhrase, 'type'); + expect(spy).toHaveBeenCalledOnceWith(["/aethel"], { + queryParams: { type: "cheese->tosti" }, + }); + }); + + it("should construct a valid route for word and type search", () => { + const spy = spyOn(router, "navigate"); + component.searchAethel(fakePhrase, 'word-and-type'); expect(spy).toHaveBeenCalledOnceWith(["/aethel"], { - queryParams: { query: "testQuery" }, + queryParams: { word: "cheeses", type: "cheese->tosti" }, }); }); diff --git a/frontend/src/app/shared/services/aethel-api.service.ts b/frontend/src/app/shared/services/aethel-api.service.ts index 91efa1d..891ef0c 100644 --- a/frontend/src/app/shared/services/aethel-api.service.ts +++ b/frontend/src/app/shared/services/aethel-api.service.ts @@ -37,15 +37,12 @@ export class AethelApiService "Content-Type": "application/json", }); - - let params = new HttpParams() - + let params = new HttpParams(); if (input.word) { - params = params.set('word', input.word) + params = params.set("word", input.word); } - if (input.type) { - params = params.set('type', input.type) + params = params.set("type", input.type); } return this.http From 93990d9bfa47aab1a9dda84c2b1d208f91c30147 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Tue, 13 Aug 2024 11:48:20 +0200 Subject: [PATCH 08/10] Styling tweaks --- frontend/src/app/sample/sample.component.html | 10 +++++----- frontend/src/app/sample/sample.component.scss | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/sample/sample.component.html b/frontend/src/app/sample/sample.component.html index 361609e..cc959ee 100644 --- a/frontend/src/app/sample/sample.component.html +++ b/frontend/src/app/sample/sample.component.html @@ -24,7 +24,7 @@ # Phrase Type - + Search in Spindle @@ -38,15 +38,15 @@ - + @if (showButtons(phrase.items)) { - - - } diff --git a/frontend/src/app/sample/sample.component.scss b/frontend/src/app/sample/sample.component.scss index e69de29..57e2824 100644 --- a/frontend/src/app/sample/sample.component.scss +++ b/frontend/src/app/sample/sample.component.scss @@ -0,0 +1,3 @@ +.search-button:not(:last-child) { + margin-right: .5rem; +} From 4e996d4c9250b129941942cbb59ceb9e61b9d6b9 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Tue, 13 Aug 2024 12:11:31 +0200 Subject: [PATCH 09/10] Correct table column header --- frontend/src/app/sample/sample.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/sample/sample.component.html b/frontend/src/app/sample/sample.component.html index cc959ee..10af818 100644 --- a/frontend/src/app/sample/sample.component.html +++ b/frontend/src/app/sample/sample.component.html @@ -24,7 +24,7 @@ # Phrase Type - Search in Spindle + Search in Æthel From b3fb33f05ba5a63f9d13d03111078c492a3e6a8b Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Thu, 15 Aug 2024 13:14:02 +0200 Subject: [PATCH 10/10] Use dict lookup instead of list --- backend/aethel_db/views.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/backend/aethel_db/views.py b/backend/aethel_db/views.py index 5bcb0ce..6d6a44b 100644 --- a/backend/aethel_db/views.py +++ b/backend/aethel_db/views.py @@ -51,22 +51,19 @@ class AethelListResponse: Response object for Aethel query view. """ - results: List[AethelListItem] = field(default_factory=list) + results: dict[tuple[str, str, str], AethelListItem] = field(default_factory=dict) error: Optional[str] = None def get_or_create_result(self, lemma: str, word: str, type: str) -> AethelListItem: """ Return an existing result with the same lemma, word, and type, or create a new one if it doesn't exist. """ - for result in self.results: - if result.lemma == lemma and result.type == type and result.word == word: - return result + key = (lemma, word, type) new_result = AethelListItem(lemma=lemma, word=word, type=type, samples=[]) - self.results.append(new_result) - return new_result + return self.results.setdefault(key, new_result) def json_response(self) -> JsonResponse: - results = [result.serialize() for result in self.results] + results = [result.serialize() for result in self.results.values()] return JsonResponse( {