From a8f8c8d9da0d3995d151165e4cb7d164e15ac0ae Mon Sep 17 00:00:00 2001 From: wakqasahmed Date: Sat, 3 Oct 2026 01:34:43 +0200 Subject: [PATCH] Find hadiths stored under a combined number (#614) Some hadiths are stored with a combined hadithNumber such as "6924, 6925", so looking up 6924 or 6925 on its own returned 404. When the exact match finds nothing, fall back to a LIKE search and keep rows where the number is one of the comma or dash separated parts, as sunnah.com does in Util::getURNByNumber. --- hadith_number.py | 3 +++ main.py | 20 +++++++++++++++++--- tests/test_hadith_number.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 hadith_number.py create mode 100644 tests/test_hadith_number.py diff --git a/hadith_number.py b/hadith_number.py new file mode 100644 index 0000000..30530c6 --- /dev/null +++ b/hadith_number.py @@ -0,0 +1,3 @@ +def matches_hadith_number(stored, number): + """Check if `number` is one part of a combined number like "6924, 6925" or "6924-6925".""" + return any(part.strip() == number for separator in (",", "-") for part in stored.split(separator)) diff --git a/main.py b/main.py index f2ad7ec..ffdf379 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,8 @@ from sqlalchemy import and_, func, or_ from werkzeug.exceptions import HTTPException +from hadith_number import matches_hadith_number + app = Flask(__name__) app.config.from_object("config.Config") @@ -76,6 +78,18 @@ def ref_match(results, collection, hadith_number): return None +def filter_hadith_number(query, hadith_number): + """Match `hadith_number` exactly, else as one part of a combined number like "6924, 6925".""" + exact = query.filter(Hadith.hadithNumber == hadith_number) + + if exact.first() is not None: + return exact + + candidates = query.filter(Hadith.hadithNumber.contains(hadith_number, autoescape=True)).with_entities(Hadith.arabicURN, Hadith.hadithNumber) + urns = [urn for urn, stored in candidates if matches_hadith_number(stored, hadith_number)] + return query.filter(Hadith.arabicURN.in_(urns)) + + @app.before_request def verify_secret(): if not app.debug and request.headers.get("x-aws-secret") != app.config["AWS_SECRET"]: @@ -198,12 +212,12 @@ def api_collection_book_hadiths(collection_name, bookNumber): def api_collection_hadith(collection_name, hadithNumber): collection, book_number = resolve_collection(collection_name) # `forty` numbers each of its books from 1, so order to keep the pick stable - query = Hadith.query.filter_by(collection=collection, hadithNumber=hadithNumber).order_by(Hadith.englishURN) + query = Hadith.query.filter_by(collection=collection).order_by(Hadith.englishURN) if book_number is not None: query = query.filter_by(bookNumber=book_number) - return query, serialize_as(collection_name) + return filter_hadith_number(query, hadithNumber), serialize_as(collection_name) @app.route("/v1/collections//books//chapters", methods=["GET"]) @@ -245,7 +259,7 @@ def api_hadiths(): hadith_number = request.args.get("hadithNumber") if hadith_number: - query = query.filter_by(hadithNumber=hadith_number) + query = filter_hadith_number(query, hadith_number) # Order by URN for consistent results return query.order_by(Hadith.englishURN), serialize_as(collection) diff --git a/tests/test_hadith_number.py b/tests/test_hadith_number.py new file mode 100644 index 0000000..146eb05 --- /dev/null +++ b/tests/test_hadith_number.py @@ -0,0 +1,28 @@ +import unittest + +from hadith_number import matches_hadith_number + + +class TestMatchesHadithNumber(unittest.TestCase): + """Regression test for sunnah-com/api#614. + + Some hadiths are stored under a combined number such as "6924, 6925", + so looking up either number on its own must still find that hadith. + """ + + def test_comma_separated_parts(self): + for number in ("6924", "6925"): + self.assertTrue(matches_hadith_number("6924, 6925", number)) + self.assertTrue(matches_hadith_number("6924,6925,6926", "6926")) + + def test_dash_separated_parts(self): + for number in ("6924", "6925"): + self.assertTrue(matches_hadith_number("6924 - 6925", number)) + + def test_exact_number(self): + self.assertTrue(matches_hadith_number("6924", "6924")) + + def test_substring_is_not_a_match(self): + for number in ("692", "924", "69246", ""): + self.assertFalse(matches_hadith_number("6924, 6925", number)) + self.assertFalse(matches_hadith_number("16924", "6924"))