Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Wyszukiwanie wydawnictwa nadrzędnego rozumie teraz ISBN. Pola „Wydawnictwo
nadrzędne" (w module redakcyjnym, w wyszukiwarce publicznej i w formularzu
zgłaszania publikacji) szukają po numerze ISBN i e-ISBN niezależnie od tego,
czy myślniki wpisano w wyszukiwarce, czy zapisano je w bazie. Pole
„Wydawnictwo nadrzędne w PBN" przestało wymagać poprawnej cyfry kontrolnej —
ISBN z literówką był dotąd po cichu traktowany jak tytuł i nie znajdował
niczego — oraz przestało ograniczać wyszukiwanie w PBN do monografii
autorskich, przez co nie znajdowało prac zbiorowych pod redakcją, czyli
najczęstszych wydawnictw nadrzędnych. Dodatkowo, jeżeli w PBN numer zapisano
jako ISBN-13, a wpisano ISBN-10 (lub odwrotnie), rekord zostanie znaleziony.
7 changes: 7 additions & 0 deletions src/bpp/newsfragments/wyszukiwarki-globalne-isbn.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Wyszukiwarki globalne — publiczna i redakcyjna — znajdują teraz prace po numerze
ISBN niezależnie od tego, czy myślniki lub spacje wpisano w wyszukiwarce, czy
zapisano je w bazie. Przeszukiwany jest także numer e-ISBN, a rekord zapisany
jako ISBN-13 zostanie znaleziony po wpisaniu ISBN-10 i odwrotnie. Wcześniej
wyszukiwarka publiczna porównywała numer dosłownie, bez żadnej normalizacji,
a redakcyjna normalizowała obie strony porównania niejednakowo — przez co
przepadał m.in. ISBN-10 z cyfrą kontrolną „X".
112 changes: 112 additions & 0 deletions src/bpp/tests/test_autocomplete/test_navigation_isbn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Wyszukiwarki globalne (publiczna i redakcyjna) znajdują rekordy po ISBN.

ISBN-y w BPP zapisywane są tak, jak wpisał je użytkownik — z myślnikami,
ze spacjami albo bez niczego. Obie wyszukiwarki muszą normalizować obie strony
porównania, przeszukiwać także ``e_isbn`` i radzić sobie z sytuacją, w której
rekord zapisano jako ISBN-13, a wpisano ISBN-10 (albo odwrotnie).
"""

import json

import pytest
from django.urls import reverse
from django.utils.http import urlencode

from bpp.models import Rekord

# ISBN-13 i jego odpowiednik ISBN-10 (poprawne sumy kontrolne).
ISBN13_Z_MYSLNIKAMI = "978-83-7430-700-0"
ISBN13_ZE_SPACJAMI = "978 83 7430 700 0"
ISBN13_BEZ_SEPARATOROW = "9788374307000"
ISBN10 = "8374307005"

# ISBN-10 z cyfrą kontrolną X — sprawdza zgodność wielkości liter po obu
# stronach porównania.
ISBN10_Z_X = "83-7430-653-X"
ISBN10_Z_X_BEZ_MYSLNIKOW = "837430653X"

TYTUL = "Zupełnie niepowtarzalny tytuł kontrolny"


def _url(nazwa, q):
return reverse(nazwa) + "?" + urlencode({"q": q})


def _etykiety(res):
"""Etykiety wyników; obie wyszukiwarki grupują je po modelu."""
etykiety = []
for grupa in json.loads(res.content)["results"]:
etykiety += [dziecko["text"] for dziecko in grupa.get("children", [])]
return etykiety


@pytest.fixture
def zwarte_z_isbn(wydawnictwo_zwarte):
def ustaw(isbn="", e_isbn=""):
wydawnictwo_zwarte.tytul_oryginalny = TYTUL
wydawnictwo_zwarte.isbn = isbn
wydawnictwo_zwarte.e_isbn = e_isbn
wydawnictwo_zwarte.save()
Rekord.objects.full_refresh()
return wydawnictwo_zwarte

return ustaw


PRZYPADKI = [
pytest.param(ISBN13_Z_MYSLNIKAMI, ISBN13_BEZ_SEPARATOROW, id="myslniki-w-bazie"),
pytest.param(ISBN13_BEZ_SEPARATOROW, ISBN13_Z_MYSLNIKAMI, id="myslniki-we-wpisie"),
pytest.param(ISBN13_ZE_SPACJAMI, ISBN13_BEZ_SEPARATOROW, id="spacje-w-bazie"),
pytest.param(ISBN13_Z_MYSLNIKAMI, ISBN10, id="isbn10-szuka-isbn13"),
pytest.param(ISBN10, ISBN13_BEZ_SEPARATOROW, id="isbn13-szuka-isbn10"),
pytest.param(ISBN10_Z_X, ISBN10_Z_X_BEZ_MYSLNIKOW, id="cyfra-kontrolna-X"),
]


@pytest.mark.django_db
@pytest.mark.parametrize("zapisany,wpisany", PRZYPADKI)
def test_publiczna_wyszukiwarka_znajduje_po_isbn(
client, zwarte_z_isbn, zapisany, wpisany
):
zwarte_z_isbn(isbn=zapisany)

res = client.get(_url("bpp:navigation-autocomplete", wpisany))
assert any(TYTUL in e for e in _etykiety(res))


@pytest.mark.django_db
@pytest.mark.parametrize("zapisany,wpisany", PRZYPADKI)
def test_redakcyjna_wyszukiwarka_znajduje_po_isbn(
admin_client, zwarte_z_isbn, zapisany, wpisany
):
zwarte_z_isbn(isbn=zapisany)

res = admin_client.get(_url("bpp:admin-navigation-autocomplete", wpisany))
assert any(TYTUL in e for e in _etykiety(res))


@pytest.mark.django_db
def test_publiczna_wyszukiwarka_znajduje_po_e_isbn(client, zwarte_z_isbn):
zwarte_z_isbn(isbn="", e_isbn=ISBN13_Z_MYSLNIKAMI)

res = client.get(_url("bpp:navigation-autocomplete", ISBN13_BEZ_SEPARATOROW))
assert any(TYTUL in e for e in _etykiety(res))


@pytest.mark.django_db
def test_redakcyjna_wyszukiwarka_znajduje_po_e_isbn(admin_client, zwarte_z_isbn):
zwarte_z_isbn(isbn="", e_isbn=ISBN13_Z_MYSLNIKAMI)

res = admin_client.get(
_url("bpp:admin-navigation-autocomplete", ISBN13_BEZ_SEPARATOROW)
)
assert any(TYTUL in e for e in _etykiety(res))


@pytest.mark.django_db
def test_wyszukiwanie_po_tytule_dziala_dalej(client, zwarte_z_isbn):
"""Kontrola: dołożenie ISBN-u nie może zepsuć wyszukiwania po tytule."""
zwarte_z_isbn(isbn=ISBN13_Z_MYSLNIKAMI)

res = client.get(_url("bpp:navigation-autocomplete", "niepowtarzalny"))
assert any(TYTUL in e for e in _etykiety(res))
106 changes: 106 additions & 0 deletions src/bpp/tests/test_autocomplete/test_wydawnictwo_nadrzedne_isbn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Wyszukiwanie wydawnictwa nadrzędnego po ISBN.

ISBN-y w BPP zapisywane są tak, jak wpisał je użytkownik — część z myślnikami,
część bez. Wyszukiwanie musi znaleźć rekord niezależnie od tego, w której
formie jest zapisany i w której został wpisany.
"""

import json

import pytest
from django.urls import reverse
from model_bakery import baker

from bpp.models import Wydawnictwo_Zwarte

# ISBN-13 i jego odpowiednik ISBN-10 (poprawne sumy kontrolne).
ISBN13_Z_MYSLNIKAMI = "978-83-7430-700-0"
ISBN13_BEZ_MYSLNIKOW = "9788374307000"
ISBN10 = "8374307005"


def _tytuly(res):
return [x["text"] for x in json.loads(res.content)["results"]]


@pytest.mark.django_db
@pytest.mark.parametrize(
"zapisany,wpisany",
[
(ISBN13_Z_MYSLNIKAMI, ISBN13_BEZ_MYSLNIKOW),
(ISBN13_BEZ_MYSLNIKOW, ISBN13_Z_MYSLNIKAMI),
(ISBN13_Z_MYSLNIKAMI, ISBN13_Z_MYSLNIKAMI),
(ISBN13_BEZ_MYSLNIKOW, ISBN13_BEZ_MYSLNIKOW),
],
)
def test_wydawnictwo_nadrzedne_autocomplete_isbn_niezaleznie_od_myslnikow(
admin_client, ksiazka, zapisany, wpisany
):
ksiazka.tytul_oryginalny = "Zupełnie inny tytuł"
ksiazka.isbn = zapisany
ksiazka.save()

res = admin_client.get(
reverse("bpp:wydawnictwo-nadrzedne-autocomplete") + f"?q={wpisany}"
)
assert _tytuly(res) == ["Zupełnie inny tytuł"]


@pytest.mark.django_db
def test_wydawnictwo_nadrzedne_autocomplete_isbn10_znajduje_isbn13(
admin_client, ksiazka
):
"""PBN ani baza nie przeliczają ISBN-10 na ISBN-13 — robi to wyszukiwarka."""
ksiazka.tytul_oryginalny = "Zupełnie inny tytuł"
ksiazka.isbn = ISBN13_Z_MYSLNIKAMI
ksiazka.save()

res = admin_client.get(
reverse("bpp:wydawnictwo-nadrzedne-autocomplete") + f"?q={ISBN10}"
)
assert _tytuly(res) == ["Zupełnie inny tytuł"]


@pytest.mark.django_db
def test_wydawnictwo_nadrzedne_autocomplete_szuka_takze_po_e_isbn(
admin_client, ksiazka
):
ksiazka.tytul_oryginalny = "Zupełnie inny tytuł"
ksiazka.isbn = ""
ksiazka.e_isbn = ISBN13_Z_MYSLNIKAMI
ksiazka.save()

res = admin_client.get(
reverse("bpp:wydawnictwo-nadrzedne-autocomplete") + f"?q={ISBN13_BEZ_MYSLNIKOW}"
)
assert _tytuly(res) == ["Zupełnie inny tytuł"]


@pytest.mark.django_db
def test_wydawnictwo_nadrzedne_autocomplete_nie_isbn_szuka_po_tytule(
admin_client, ksiazka
):
"""Zwykły tekst ma dalej trafiać w wyszukiwanie po tytule."""
ksiazka.tytul_oryginalny = "Interna Szczeklika 2021"
ksiazka.isbn = ISBN13_Z_MYSLNIKAMI
ksiazka.save()

res = admin_client.get(
reverse("bpp:wydawnictwo-nadrzedne-autocomplete") + "?q=Szczeklika"
)
assert _tytuly(res) == ["Interna Szczeklika 2021"]


@pytest.mark.django_db
def test_public_wydawnictwo_nadrzedne_autocomplete_isbn(admin_client, ksiazka):
"""Publiczny wariant też rozumie ISBN — zawężony do realnych nadrzędnych."""
ksiazka.tytul_oryginalny = "Zupełnie inny tytuł"
ksiazka.isbn = ISBN13_Z_MYSLNIKAMI
ksiazka.save()
baker.make(Wydawnictwo_Zwarte, wydawnictwo_nadrzedne=ksiazka)

res = admin_client.get(
reverse("bpp:public-wydawnictwo-nadrzedne-autocomplete")
+ f"?q={ISBN13_BEZ_MYSLNIKOW}"
)
assert _tytuly(res) == ["Zupełnie inny tytuł"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Rozpoznawanie i wyszukiwanie ISBN w autocomplete „Wydawnictwo nadrzędne w PBN".

Zachowanie serwera PBN sprawdzone empirycznie na ``/api/v1/search/publications``:

* ISBN dopasowywany jest **dosłownie, po formie kanonicznej** — zapytanie
z myślnikami zwraca zero wyników nawet wtedy, gdy PBN przechowuje ISBN
właśnie z myślnikami;
* PBN nie przelicza ISBN-10 na ISBN-13 ani odwrotnie;
* ``type="BOOK"`` odcina ``EDITED_BOOK``, czyli praktycznie wszystkie realne
wydawnictwa nadrzędne (prace zbiorowe pod redakcją).
"""

from unittest.mock import Mock, call

import pytest
from model_bakery import baker

from bpp.views.autocomplete.wydawnictwo_nadrzedne_w_pbn import (
Wydawnictwo_Nadrzedne_W_PBNAutocomplete as Widok,
)
from pbn_api.models import Publication

ISBN13_Z_MYSLNIKAMI = "978-83-7430-700-0"
ISBN13_BEZ_MYSLNIKOW = "9788374307000"
ISBN10 = "8374307005"

MONGO_ID = "616e76ca2467f070ae3355dd"


def _klient(*wyniki):
client = Mock()
client.search_publications.side_effect = list(wyniki) or [[]]
return client


# --- qualify_query -------------------------------------------------------


def test_qualify_query_rozpoznaje_isbn_z_myslnikami():
assert Widok().qualify_query(ISBN13_Z_MYSLNIKAMI) == Widok.ISBN


def test_qualify_query_rozpoznaje_isbn_bez_myslnikow():
assert Widok().qualify_query(ISBN13_BEZ_MYSLNIKOW) == Widok.ISBN


def test_qualify_query_rozpoznaje_isbn_ze_zla_suma_kontrolna():
"""Regresja: ``isbnlib.notisbn`` odrzucał taki numer, przez co wyszukiwarka
szukała go jako tytułu i cicho nie znajdowała niczego."""
assert Widok().qualify_query("978-83-01-12345-6") == Widok.ISBN


def test_qualify_query_rozpoznaje_mongoid():
assert Widok().qualify_query(MONGO_ID) == Widok.MONGO_ID


def test_qualify_query_rozpoznaje_doi():
assert Widok().qualify_query("10.1234/abcd.2021") == Widok.DOI


def test_qualify_query_reszta_to_tytul():
assert Widok().qualify_query("Interna Szczeklika 2021") == Widok.TITLE


# --- zapytania do serwera PBN -------------------------------------------


def test_isbn_leci_do_pbn_bez_filtra_type():
"""``type="BOOK"`` odcinałby EDITED_BOOK — czyli realne okładki."""
client = _klient([], [])
list(Widok()._get_pbn_search_results(client, Widok.ISBN, ISBN13_Z_MYSLNIKAMI))

for wywolanie in client.search_publications.call_args_list:
assert "type" not in wywolanie.kwargs


def test_isbn_leci_do_pbn_w_formie_kanonicznej_i_w_obu_dlugosciach():
client = _klient([], [])
list(Widok()._get_pbn_search_results(client, Widok.ISBN, ISBN13_Z_MYSLNIKAMI))

assert client.search_publications.call_args_list == [
call(isbn=ISBN13_BEZ_MYSLNIKOW),
call(isbn=ISBN10),
]


def test_isbn_nie_pyta_o_drugi_wariant_gdy_pierwszy_wystarczyl():
"""Zapytania mają być leniwe — drugi wariant to dodatkowy round-trip."""
client = _klient([{"mongoId": MONGO_ID}], [])
wyniki = Widok()._get_pbn_search_results(client, Widok.ISBN, ISBN13_Z_MYSLNIKAMI)

assert next(iter(wyniki)) == {"mongoId": MONGO_ID}
assert client.search_publications.call_args_list == [
call(isbn=ISBN13_BEZ_MYSLNIKOW)
]


def test_tytul_leci_do_pbn_dla_edited_book_i_book():
client = _klient([], [])
list(Widok()._get_pbn_search_results(client, Widok.TITLE, "Interna Szczeklika"))

assert client.search_publications.call_args_list == [
call(title="Interna Szczeklika", type="EDITED_BOOK"),
call(title="Interna Szczeklika", type="BOOK"),
]


def test_doi_leci_do_pbn_bez_filtra_type():
client = _klient([])
list(
Widok()._get_pbn_search_results(
client, Widok.DOI, "https://doi.org/10.1234/abcd"
)
)

assert client.search_publications.call_args_list == [call(doi="10.1234/abcd")]


# --- lokalny cache -------------------------------------------------------


@pytest.mark.django_db
@pytest.mark.parametrize("wpisany", [ISBN13_Z_MYSLNIKAMI, ISBN13_BEZ_MYSLNIKOW, ISBN10])
def test_lokalnie_isbn_znajduje_publikacje_w_kazdym_zapisie(wpisany):
baker.make(Publication, mongoId=MONGO_ID, title="Tytuł", isbn=ISBN13_BEZ_MYSLNIKOW)

widok = Widok()
widok.q = wpisany
assert [x.pk for x in widok.get_queryset()] == [MONGO_ID]


@pytest.mark.django_db
def test_lokalnie_isbn_znajduje_gdy_pole_ma_dopisek():
"""PBN bywa zaśmiecony — ``9788374307000 (druk)`` to realny zapis."""
baker.make(
Publication,
mongoId=MONGO_ID,
title="Tytuł",
isbn=f"{ISBN13_BEZ_MYSLNIKOW} (druk)",
)

widok = Widok()
widok.q = ISBN13_Z_MYSLNIKAMI
assert [x.pk for x in widok.get_queryset()] == [MONGO_ID]


# --- etykieta opcji „pobierz z PBN" -------------------------------------


def test_create_option_pokazuje_to_co_wpisal_uzytkownik():
"""Pokazywanie formy skanonizowanej myliło — user nie poznawał swojego ISBN-u."""
(opcja,) = Widok().get_create_option({}, ISBN13_Z_MYSLNIKAMI)

assert ISBN13_Z_MYSLNIKAMI in opcja["text"]
assert opcja["id"] == ISBN13_Z_MYSLNIKAMI
Loading