From 8e05c41fdebdd62c52d4573b86b82e3a0d28c171 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Fri, 21 Aug 2026 15:19:44 -0600 Subject: [PATCH 1/2] Improve sorting of exercises when adding to assignment via book browse --- components/rsptx/db/crud/question.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/components/rsptx/db/crud/question.py b/components/rsptx/db/crud/question.py index a57cf775c..a41dac2af 100644 --- a/components/rsptx/db/crud/question.py +++ b/components/rsptx/db/crud/question.py @@ -1,3 +1,4 @@ +import re from typing import List, Optional, Tuple, Dict from sqlalchemy import select, and_, or_, func, asc, desc, not_, update, delete from sqlalchemy.exc import IntegrityError @@ -706,6 +707,23 @@ async def update_question(question: QuestionValidator) -> QuestionValidator: return question +def _natural_sort_key(value: Optional[str]) -> List[Tuple[int, object]]: + """Key for natural ordering of exercise numbers like "Q3.10.1" or "Exercise 3.1.2". + + Splits the string into alternating text/number chunks so numeric runs compare + numerically instead of character-by-character (e.g. "3.2.1" sorts before "3.10.1", + where a plain string sort would put "3.10.1" first). Each chunk is tagged with a + type flag so chunks of different types never get compared directly, which would + otherwise raise a TypeError. + """ + if not value: + return [(1, "")] + return [ + (0, int(chunk)) if chunk.isdigit() else (1, chunk.lower()) + for chunk in re.split(r"(\d+)", value) + ] + + async def fetch_questions_for_chapter_subchapter( base_course: str, skipreading: bool = False, @@ -808,6 +826,15 @@ async def fetch_questions_for_chapter_subchapter( del q.question questions[c.chapter_label][sc.sub_chapter_label].append(q) + # The SQL query above orders by Question.qnumber as a plain string, which + # sorts "3.10.1" before "3.2.1". Re-sort each subchapter's questions with a + # natural sort key so multi-part exercise numbers order the way a human expects. + for chapter_questions in questions.values(): + for subchapter_questions in chapter_questions.values(): + subchapter_questions.sort( + key=lambda q: _natural_sort_key(q.qnumber or q.name) + ) + # Now create the hierarchical json structure where the keys are the chapter and subchapter labels # This is the structure that is used by the React TreeTable component def find_page_id(chapter, subchapter): From 3999ff0b5bf27ddbe3b9c24672a225ef5b8cff5e Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Fri, 21 Aug 2026 15:48:25 -0600 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- components/rsptx/db/crud/question.py | 1 + 1 file changed, 1 insertion(+) diff --git a/components/rsptx/db/crud/question.py b/components/rsptx/db/crud/question.py index a41dac2af..7d3c23933 100644 --- a/components/rsptx/db/crud/question.py +++ b/components/rsptx/db/crud/question.py @@ -721,6 +721,7 @@ def _natural_sort_key(value: Optional[str]) -> List[Tuple[int, object]]: return [ (0, int(chunk)) if chunk.isdigit() else (1, chunk.lower()) for chunk in re.split(r"(\d+)", value) + if chunk ]