Skip to content

Commit 27ce6c0

Browse files
Copilotinducer
andauthored
Fix InlineMultiQuestion form rendering: don't flex-wrap text-only/blank-only paragraphs
Agent-Logs-Url: https://github.com/inducer/relate/sessions/b158f964-365c-495e-ae0f-959d7bf5e5b3 Co-authored-by: inducer <352067+inducer@users.noreply.github.com>
1 parent 14b3bb0 commit 27ce6c0

3 files changed

Lines changed: 134 additions & 33 deletions

File tree

course/page/inline.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -632,33 +632,48 @@ def body(self, page_context: PageContext, page_data: PageData):
632632
return markup_to_html(page_context, self.prompt)
633633

634634
def get_question(self, page_context: PageContext):
635-
# for correct render of question with more than one
636-
# paragraph, replace <p> tags to new input-group.
637-
638-
div_start_css_class_list = [
635+
# Replace <p> tags with appropriate containers depending on content:
636+
# - paragraphs containing both text and blanks get a flex container
637+
# so that the input widget sits inline with the surrounding text;
638+
# - paragraphs containing only a single blank get no wrapper at all,
639+
# letting the form field render as a normal block element; and
640+
# - text-only paragraphs are left unchanged as <p> elements.
641+
642+
flex_class_list = [
639643
"input-group",
640644
# ensure spacing between input and text, mathjax and text
641645
"gap-1",
642-
"align-items-center"
646+
"align-items-center",
643647
]
648+
flex_div_start = f"<div class=\"{' '.join(flex_class_list)}\">"
649+
650+
question_html = markup_to_html(page_context, self.question)
651+
652+
def replace_paragraph(m: re.Match[str]) -> str:
653+
content = m.group(1)
654+
# Blank-only paragraph: strip the <p> wrapper so the form field
655+
# renders as a regular block element (no flex container).
656+
if re.match(r"^\s*\[\[[a-zA-Z_]\w*\]\]\s*$", content):
657+
return content.strip()
658+
# Mixed text-and-blank paragraph: use a flex container so the
659+
# input widget sits inline with the surrounding text.
660+
if WRAPPED_NAME_RE.search(content):
661+
return f"{flex_div_start}{content}</div>"
662+
# Text-only paragraph: keep as-is.
663+
return m.group(0)
664+
665+
result = re.sub(
666+
r"<p>(.*?)</p>", replace_paragraph, question_html, flags=re.DOTALL)
667+
668+
# Add mb-4 to the last flex div so there is spacing after the last
669+
# inline input field (before whatever follows the form).
670+
if flex_div_start in result:
671+
last_flex_div_start = (
672+
f"<div class=\"{' '.join([*flex_class_list, 'mb-4'])}\">")
673+
# https://stackoverflow.com/a/59082116/3437454
674+
result = last_flex_div_start.join(result.rsplit(flex_div_start, 1))
644675

645-
replace_p_start = f"<div class=\"{' '.join(div_start_css_class_list)}\">"
646-
647-
question_html = markup_to_html(
648-
page_context,
649-
self.question
650-
).replace(
651-
"<p>",
652-
replace_p_start
653-
).replace("</p>", "</div>")
654-
655-
# add mb-4 class to the last paragraph so as to add spacing before
656-
# submit buttons.
657-
last_div_start = (
658-
f"<div class=\"{' '.join([*div_start_css_class_list, 'mb-4'])}\">")
659-
660-
# https://stackoverflow.com/a/59082116/3437454
661-
return last_div_start.join(question_html.rsplit(replace_p_start, 1))
676+
return result
662677

663678
def get_form_info(self, page_context: PageContext):
664679
return FormInfo(

tests/test_pages/test_inline.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,44 @@
533533
- <plain>bar
534534
"""
535535

536+
INLINE_MULTI_MARKDOWN_BLOCK_BLANKS = """
537+
type: InlineMultiQuestion
538+
id: inlinemulti
539+
value: 10
540+
prompt: |
541+
542+
# An InlineMultiQuestion example
543+
544+
Questions with blanks on their own paragraphs.
545+
546+
question: |
547+
548+
Text before the first blank.
549+
550+
[[blank1]]
551+
552+
Text between the two blanks.
553+
554+
[[blank2]]
555+
556+
answers:
557+
558+
blank1:
559+
type: ShortAnswer
560+
width: 4em
561+
correct_answer:
562+
- <plain> FOO
563+
- <plain>foo
564+
565+
blank2:
566+
type: ShortAnswer
567+
width: 4em
568+
correct_answer:
569+
- <plain> BAR
570+
- <plain>bar
571+
572+
"""
573+
536574
INLINE_MULTI_MARKDOWN_NO_ANSWER_FIELD = """
537575
type: InlineMultiQuestion
538576
id: inlinemulti
@@ -817,6 +855,54 @@ def test_embedded_question_no_extra_html(self):
817855
# There's no html string between rendered blank1 field and blank2 field
818856
self.assertIn('</div> <div id="div_id_blank2"', resp.content.decode())
819857

858+
def test_block_blank_no_flex_container_on_text_paragraphs(self):
859+
"""Regression test: text-only paragraphs must not be wrapped in a
860+
flex (``input-group``) container when blanks appear on their own
861+
paragraphs (i.e. separated from surrounding text by blank lines)."""
862+
markdown = INLINE_MULTI_MARKDOWN_BLOCK_BLANKS
863+
resp = self.get_page_sandbox_preview_response(markdown)
864+
self.assertEqual(resp.status_code, 200)
865+
self.assertSandboxHasValidPage(resp)
866+
867+
content = resp.content.decode()
868+
869+
# Text-only paragraphs must keep their <p> wrapper and must NOT
870+
# be placed inside an ``input-group`` flex container.
871+
self.assertIn("<p>Text before the first blank.</p>", content)
872+
self.assertIn("<p>Text between the two blanks.</p>", content)
873+
874+
# The text paragraphs must not be inside flex containers.
875+
self.assertNotIn(
876+
'class="input-group gap-1 align-items-center">'
877+
"Text before the first blank.",
878+
content)
879+
self.assertNotIn(
880+
'class="input-group gap-1 align-items-center">'
881+
"Text between the two blanks.",
882+
content)
883+
884+
# The form fields must still be present.
885+
self.assertIn('id="div_id_blank1"', content)
886+
self.assertIn('id="div_id_blank2"', content)
887+
888+
# Submitting correct answers should work normally.
889+
resp = self.get_page_sandbox_submit_answer_response(
890+
markdown,
891+
answer_data={"blank1": "foo", "blank2": "bar"})
892+
self.assertResponseContextAnswerFeedbackCorrectnessEquals(resp, 1)
893+
894+
def test_inline_blank_uses_flex_container(self):
895+
"""Blanks appearing inline with text (on the same paragraph line)
896+
must still be wrapped in a flex container for proper inline layout."""
897+
markdown = INLINE_MULTI_MARKDOWN_SINGLE
898+
resp = self.get_page_sandbox_preview_response(markdown)
899+
self.assertEqual(resp.status_code, 200)
900+
self.assertSandboxHasValidPage(resp)
901+
902+
content = resp.content.decode()
903+
# The paragraph with an inline blank should be wrapped in a flex container.
904+
self.assertIn('class="input-group gap-1 align-items-center', content)
905+
820906
def test_embedded_weight_count(self):
821907
markdown = (INLINE_MULTI_MARKDOWN_EMBEDDED_ATTR_PATTERN
822908
% {"attr1": "weight: 15",

uv.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)