Repository: PrairieLearn/PrairieLearn
Pull request #11977: Fixup minor accessibility, grading, rendering bugs in questions & elements
Author: login: reteps
Labels: name: documentation
description: Related to user documentation (not code docs)
name: python
description: Requires Python code changes
name: element
description: Anything related to elements
name: bug-fix
Created: 2025-05-13T05:30:22Z | Merged: 2025-05-14T22:44:19Z

Pulled out of #11837


Diff:
diff --git a/apps/prairielearn/elements/pl-graph/pl-graph.py b/apps/prairielearn/elements/pl-graph/pl-graph.py
index 35a63f7eb34..897bbb00d95 100644
--- a/apps/prairielearn/elements/pl-graph/pl-graph.py
+++ b/apps/prairielearn/elements/pl-graph/pl-graph.py
@@ -1,3 +1,4 @@
+import re
 import warnings
 
 import lxml.html
@@ -18,6 +19,8 @@
 NEGATIVE_WEIGHTS_DEFAULT = False
 DIRECTED_DEFAULT = True
 LOG_WARNINGS_DEFAULT = True
+XML_DECLARATION = r"<\?xml[^>\?]*\?>"
+DOCTYPE_DECLARATION = r"<!DOCTYPE svg [^>]*>"
 
 
 def graphviz_from_networkx(
@@ -197,5 +200,9 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if svg_bytes is None:
             raise TypeError("Graph was not returned.")
         svg = svg_bytes.decode("utf-8", "strict")
+        # https://gitlab.com/graphviz/graphviz/-/merge_requests/3404
+        # We can switch to svg_inline when we have use graphviz >= 10.0.1
+        svg = re.sub(XML_DECLARATION, "", svg)
+        svg = re.sub(DOCTYPE_DECLARATION, "", svg)
 
     return f'<div class="pl-graph">{svg}</div>'
diff --git a/apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py b/apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
index 370211f60e3..f34fe595931 100644
--- a/apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
+++ b/apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
@@ -559,24 +559,20 @@ def create_table_for_html_display(
 ) -> str:
     editable = data["editable"]
 
-    label_attr = (
-        f'aria-label="{aria_label}"'
-        if aria_label is not None
-        else (
-            f'aria-labelledby="pl-matrix-component-input-{label_uuid}-label"'
-            if label
-            else ""
-        )
-    )
+    label_attr = ""
+
+    if aria_label is not None:
+        label_attr = f'aria-label="{aria_label}"'
+    elif label is not None:
+        label_attr = f'aria-labelledby="pl-matrix-component-input-{label_uuid}-label"'
+
     if format_type == "output-invalid":
         display_array = "<table>"
         display_array += "<tr>"
         display_array += (
-            '<td class="pl-matrix-component-input-close-left" rowspan="'
-            + str(m)
-            + '"></td>'
+            f'<td class="pl-matrix-component-input-close-left" rowspan="{m}"></td>'
         )
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+        display_array += f'<td style="width:4px" rowspan="{m}"></td>'
         # First row of array
         for j in range(n):
             each_entry_name = name + str(j + 1)
@@ -592,11 +588,9 @@ def create_table_for_html_display(
                 )
             display_array += escape(pl.escape_unicode_string(raw_submitted_answer))
             display_array += "</code></td> "
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+        display_array += f'<td style="width:4px" rowspan="{m}"></td>'
         display_array += (
-            '<td class="pl-matrix-component-input-close-right" rowspan="'
-            + str(m)
-            + '"></td>'
+            f'<td class="pl-matrix-component-input-close-right" rowspan="{m}"></td>'
         )
         # Add the other rows
         for i in range(1, m):
@@ -642,13 +636,11 @@ def create_table_for_html_display(
         display_array += "<tr>"
         # Add the prefix
         if label is not None:
-            display_array += '<td rowspan="0">' + label + "&nbsp;</td>"
+            display_array += f'<td rowspan="0">{label}&nbsp;</td>'
         display_array += (
-            '<td class="pl-matrix-component-input-close-left" rowspan="'
-            + str(m)
-            + '"></td>'
+            f'<td class="pl-matrix-component-input-close-left" rowspan="{m}"></td>'
         )
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+        display_array += f'<td style="width:4px" rowspan="{m}"></td>'
         # First row of array
         for j in range(n):
             each_entry_name = name + str(j + 1)
@@ -671,14 +663,12 @@ def create_table_for_html_display(
                 display_array += feedback_message
             display_array += "</td> "
         # Add the suffix
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+        display_array += f'<td style="width:4px" rowspan="{m}"></td>'
         display_array += (
-            '<td class="pl-matrix-component-input-close-right" rowspan="'
-            + str(m)
-            + '"></td>'
+            f'<td class="pl-matrix-component-input-close-right" rowspan="{m}"></td>'
         )
         if score_message:
-            display_array += '<td rowspan="0">&nbsp;' + score_message + "</td>"
+            display_array += f'<td rowspan="0">&nbsp;{score_message}</td>'
         display_array += "</tr>"
         # Add the other rows
         for i in range(1, m):
@@ -713,29 +703,24 @@ def create_table_for_html_display(
         display_array += "<tr>"
         # Add first row
         display_array += (
-            '<td class="pl-matrix-component-input-close-left" rowspan="'
-            + str(m)
-            + '"></td>'
+            f'<td class="pl-matrix-component-input-close-left" rowspan="{m}"></td>'
         )
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+        display_array += f'<td style="width:4px" rowspan="{m}"></td>'
         for j in range(n):
             each_entry_name = name + str(j + 1)
             raw_submitted_answer = data["raw_submitted_answers"].get(
                 each_entry_name, None
             )
-            display_array += f' <td> <input name= "{each_entry_name}" type="text" size="8" aria-label="Row 1, Column {j + 1}" '
-            if not editable:
-                display_array += " disabled "
+            disabled = "disabled" if not editable else ""
+            value = ""
             if raw_submitted_answer is not None:
-                display_array += '  value= "'
-                display_array += escape(raw_submitted_answer)
-            display_array += '" /> </td>'
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+                value = f'value="{escape(raw_submitted_answer)}"'
+            display_array += f'<td><input name="{each_entry_name}" type="text" size="8" aria-label="Row 1, Column {j + 1}" {disabled} {value}/></td>'
+        display_array += f'<td style="width:4px" rowspan="{m}"></td>'
         display_array += (
-            '<td class="pl-matrix-component-input-close-right" rowspan="'
-            + str(m)
-            + '"></td>'
+            f'<td class="pl-matrix-component-input-close-right" rowspan="{m}"></td>'
         )
+
         # Add other rows
         for i in range(1, m):
             display_array += " <tr>"
@@ -744,13 +729,11 @@ def create_table_for_html_display(
                 raw_submitted_answer = data["raw_submitted_answers"].get(
                     each_entry_name, None
                 )
-                display_array += f' <td> <input name= "{each_entry_name}" type="text" size="8" aria-label="Row {i + 1}, Column {j + 1}" '
-                if not editable:
-                    display_array += " disabled "
+                disabled = "disabled" if not editable else ""
+                value = ""
                 if raw_submitted_answer is not None:
-                    display_array += '  value= "'
-                    display_array += escape(raw_submitted_answer)
-                display_array += '" /> </td>'
+                    value = f'value="{escape(raw_submitted_answer)}"'
+                display_array += f' <td><input name="{each_entry_name}" type="text" size="8" aria-label="Row {i + 1}, Column {j + 1}" {disabled} {value}/></td>'
                 display_array += " </td> "
             display_array += "</tr>"
         display_array += "</table>"
diff --git a/docs/elements.md b/docs/elements.md
index 0636cefd7d9..26b9444efd5 100644
--- a/docs/elements.md
+++ b/docs/elements.md
@@ -1546,7 +1546,7 @@ def generate(data):
 
 #### Details
 
-Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the `rankdir` used in rendering. See the [Graphviz documentation on attributes](https://graphviz.org/doc/info/attrs.html) for more information on what attributes are supported.
+Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the `rankdir` used in rendering. See the [Graphviz documentation on attributes](https://graphviz.org/doc/info/attrs.html) for more information on what attributes are supported. The currently used Graphviz version is 2.44.0.
 
 #### Example implementations
 
diff --git a/exampleCourse/questions/element/graph/question.html b/exampleCourse/questions/element/graph/question.html
index 8c4916d111e..c15e1247917 100644
--- a/exampleCourse/questions/element/graph/question.html
+++ b/exampleCourse/questions/element/graph/question.html
@@ -115,7 +115,7 @@
         <p> The <code class="user-output">pl-graph</code> element has the ability to be extended on a course level to create graphs based on custom inputs. This course extension located in <code class="user-output">elementExtensions/pl-graph/edge-inc-matrix</code> adds the capability of rendering edge-incidence matrices as graphs. </p>
         <pl-matrix-latex params-name="edge-inc-mat"></pl-matrix-latex>
         <pl-graph params-name="edge-inc-mat" params-type="edge-inc-matrix"></pl-graph>
-        <p> For more information on how to create extensions, check the <a href="">extension documentation</a> and <a href="">graph element documentation</a>. </p>
+        <p> For more information on how to create extensions, check the <a href="https://prairielearn.readthedocs.io/en/latest/elementExtensions">extension documentation</a> and <a href="https://prairielearn.readthedocs.io/en/latest/elements/#pl-graph-element">graph element documentation</a>. </p>
     </pl-question-panel>
 </pl-card>
 
diff --git a/exampleCourse/questions/workshop/Lesson4_example1/server.py b/exampleCourse/questions/workshop/Lesson4_example1/server.py
index 19583ec6742..623cad369a1 100644
--- a/exampleCourse/questions/workshop/Lesson4_example1/server.py
+++ b/exampleCourse/questions/workshop/Lesson4_example1/server.py
@@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
     data["correct_answers"]["A"] = pl.to_json(A)
 
 
@@ -16,4 +17,5 @@ def grade(data):
         data["partial_scores"]["A"] = {"score": 1, "weight": 1}
         data["score"] = 1.0
     else:
+        data["partial_scores"]["A"] = {"score": 0, "weight": 1}
         data["score"] = 0


Reviews:
user: login: reteps
body: Fixes.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T05:32:15Z
user: login: jonatanschroeder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T13:50:12Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:36:06Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:36:53Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:37:31Z
user: login: jonatanschroeder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:38:10Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:41:11Z
user: login: jonatanschroeder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:41:32Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:50:56Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:59:31Z
user: login: nwalters512
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:03:16Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T16:05:06Z
user: login: jonatanschroeder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T16:18:03Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T16:31:12Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T16:31:27Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T16:31:45Z
user: login: nwalters512
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:36:13Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:18:11Z
user: login: jonatanschroeder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:24:32Z
user: login: nwalters512
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T17:33:27Z
user: login: eliotwrobson
body: One small nit but everything else looks reasonable. For future reference, I wouldn't spend too much time on the matrix component input upfront. The element code could use significant revision at some point, so I think smaller changes to code quality probably aren't worth the effort.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T15:16:06Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T18:52:40Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T18:52:50Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T18:57:14Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T19:00:35Z
user: login: nwalters512
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T21:47:13Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR addresses minor fixes across question generation, grading logic, accessibility, documentation links, and rendering behavior.

- Ensure the example matrix has the correct non-zero entry and consistent partial scoring.
- Update documentation links in HTML and note the Graphviz version in markdown.
- Refactor HTML-building code in the matrix input element for readability and simplify aria attributes.
- Strip XML/DOCTYPE declarations from SVG output in the graph element.

### Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| exampleCourse/questions/workshop/Lesson4_example1/server.py | Set A[0,1] in matrix generation and always populate `partial_scores["A"]` |
| exampleCourse/questions/element/graph/question.html | Added full URLs to extension and graph element docs |
| docs/elements.md | Appended the currently used Graphviz version to docs |
| apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py | Converted string concatenations to f-strings and simplified `aria-label`/`aria-labelledby` logic |
| apps/prairielearn/elements/pl-graph/pl-graph.py | Imported `re`, defined regex constants, and removed XML/DOCTYPE declarations from SVG output |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T21:53:30Z
user: login: nwalters512
body: LGTM. The thing I was most worried about here was all the changes to `pl-matrix-component-input`, but I tested that with `element/matrixComponentInput` in the example course and it looks good to me.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T21:59:39Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T22:30:34Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T22:32:08Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T22:32:41Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T22:33:02Z
user: login: jonatanschroeder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T13:17:41Z

Review comments:
diff_hunk: @@ -18,6 +19,7 @@
 NEGATIVE_WEIGHTS_DEFAULT = False
 DIRECTED_DEFAULT = True
 LOG_WARNINGS_DEFAULT = True
+DOCTYPE_DECLARATION = r"<!DOCTYPE svg [^>]*>"
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: You shouldn't include the doctype for svg elements. This caused the axe accessibility checker to choke.
created_at: 2025-05-13T05:31:05Z
author_association: MEMBER
diff_hunk: @@ -729,7 +729,8 @@ def create_table_for_html_display(
             if raw_submitted_answer is not None:
                 display_array += '  value= "'
                 display_array += escape(raw_submitted_answer)
-            display_array += '" /> </td>'
+                display_array += '" '
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: reteps
body: If `raw_submitted_answer` is None, this causes a rendering glitch.
created_at: 2025-05-13T05:31:40Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: reteps
body: The 'correct answer' isn't actually correct.
created_at: 2025-05-13T05:32:02Z
author_association: MEMBER
diff_hunk: @@ -115,7 +115,7 @@
         <p> The <code class="user-output">pl-graph</code> element has the ability to be extended on a course level to create graphs based on custom inputs. This course extension located in <code class="user-output">elementExtensions/pl-graph/edge-inc-matrix</code> adds the capability of rendering edge-incidence matrices as graphs. </p>
         <pl-matrix-latex params-name="edge-inc-mat"></pl-matrix-latex>
         <pl-graph params-name="edge-inc-mat" params-type="edge-inc-matrix"></pl-graph>
-        <p> For more information on how to create extensions, check the <a href="">extension documentation</a> and <a href="">graph element documentation</a>. </p>
+        <p> For more information on how to create extensions, check the <a href="https://prairielearn.readthedocs.io/en/latest/elementExtensions">extension documentation</a> and <a href="https://prairielearn.readthedocs.io/en/latest/elements/#pl-graph-element">graph element documentation</a>. </p>
path: exampleCourse/questions/element/graph/question.html
user: login: reteps
body: Missing links.
created_at: 2025-05-13T05:32:09Z
author_association: MEMBER
diff_hunk: @@ -197,5 +199,6 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if svg_bytes is None:
             raise TypeError("Graph was not returned.")
         svg = svg_bytes.decode("utf-8", "strict")
+        svg = re.sub(DOCTYPE_DECLARATION, "", svg)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: jonatanschroeder
body: It seems graphviz has a [new option to enable `svg_inline`](https://www.graphviz.org/docs/outputs/svg/), which would take care of this issue. I don't think we have this version installed, though. In any case, I believe the output typically also includes a `<?xml...` header which should probably be removed for similar reasons.

Also, I tested this [in production](https://us.prairielearn.com/pl/course_instance/4970/instructor/question/3118036/preview) and checked the generated source, and it doesn't seem like the production version includes the doctype. Could this be related to your version of graphviz? Production seems to use 2.44.0. In any case, if this is the issue, we should probably document/standardize the graphviz version in play.
created_at: 2025-05-13T13:50:12Z
author_association: MEMBER
diff_hunk: @@ -197,5 +199,6 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if svg_bytes is None:
             raise TypeError("Graph was not returned.")
         svg = svg_bytes.decode("utf-8", "strict")
+        svg = re.sub(DOCTYPE_DECLARATION, "", svg)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: ```
/opt/homebrew/opt/graphviz/bin/dot --version
dot - graphviz version 12.2.1 (20241206.2353)

>>> import pygraphviz as pgv
>>> pgv.__version__
'1.14'
>>> G = pgv.AGraph()
>>> G.add_edge("b", "c")
>>> G.draw(format="svg", prog='dot')
b'<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n<!-- Generated by graphviz version 12.2.1 (20241206.2353)\n -->\n<!-- Pages: 1 -->\n<svg width="62pt" height="116pt"\n viewBox="0.00 0.00 62.00 116.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 112)">\n<polygon fill="white" stroke="none" points="-4,4 -4,-112 58,-112 58,4 -4,4"/>\n<!-- b -->\n<g id="node1" class="node">\n<title>b</title>\n<ellipse fill="none" stroke="black" cx="27" cy="-90" rx="27" ry="18"/>\n<text text-anchor="middle" x="27" y="-84.95" font-family="Times,serif" font-size="14.00">b</text>\n</g>\n<!-- c -->\n<g id="node2" class="node">\n<title>c</title>\n<ellipse fill="none" stroke="black" cx="27" cy="-18" rx="27" ry="18"/>\n<text text-anchor="middle" x="27" y="-12.95" font-family="Times,serif" font-size="14.00">c</text>\n</g>\n<!-- b&#45;&#45;c -->\n<g id="edge1" class="edge">\n<title>b&#45;&#45;c</title>\n<path fill="none" stroke="black" d="M27,-71.7C27,-60.85 27,-46.92 27,-36.1"/>\n</g>\n</g>\n</svg>\n'

>>> G.draw(format="svg_inline", prog='dot')
b'<!-- Generated by graphviz version 12.2.1 (20241206.2353)\n -->\n<!-- Pages: 1 -->\n<svg width="62pt" height="116pt"\n viewBox="0.00 0.00 62.00 116.00">\n<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 112)">\n<polygon fill="white" stroke="none" points="-4,4 -4,-112 58,-112 58,4 -4,4"/>\n<!-- b -->\n<g id="node1" class="node">\n<title>b</title>\n<ellipse fill="none" stroke="black" cx="27" cy="-90" rx="27" ry="18"/>\n<text text-anchor="middle" x="27" y="-84.95" font-family="Times,serif" font-size="14.00">b</text>\n</g>\n<!-- c -->\n<g id="node2" class="node">\n<title>c</title>\n<ellipse fill="none" stroke="black" cx="27" cy="-18" rx="27" ry="18"/>\n<text text-anchor="middle" x="27" y="-12.95" font-family="Times,serif" font-size="14.00">c</text>\n</g>\n<!-- b&#45;&#45;c -->\n<g id="edge1" class="edge">\n<title>b&#45;&#45;c</title>\n<path fill="none" stroke="black" d="M27,-71.7C27,-60.85 27,-46.92 27,-36.1"/>\n</g>\n</g>\n</svg>\n'
```
created_at: 2025-05-13T15:36:06Z
author_association: MEMBER
diff_hunk: @@ -197,5 +199,6 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if svg_bytes is None:
             raise TypeError("Graph was not returned.")
         svg = svg_bytes.decode("utf-8", "strict")
+        svg = re.sub(DOCTYPE_DECLARATION, "", svg)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: Changed in [77062fa](https://github.com/PrairieLearn/PrairieLearn/pull/11977/commits/77062fa289226c458625b95db240fbf66e758289). Thanks, I wasn't aware of the `svg_inline` option.
created_at: 2025-05-13T15:36:53Z
author_association: MEMBER
diff_hunk: @@ -197,5 +199,6 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if svg_bytes is None:
             raise TypeError("Graph was not returned.")
         svg = svg_bytes.decode("utf-8", "strict")
+        svg = re.sub(DOCTYPE_DECLARATION, "", svg)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: It was missing from the pygraphviz documentation. https://pygraphviz.github.io/documentation/stable/reference/agraph.html#pygraphviz.AGraph.draw
created_at: 2025-05-13T15:37:31Z
author_association: MEMBER
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: jonatanschroeder
body: This doesn't work for me locally, have you tested it? As mentioned before, the svg_inline (not inline_svg) option is only available in newer versions of graphviz, which are not available on PL image or on the Ubuntu repo (which is the current installation suggestion for WSL).
created_at: 2025-05-13T15:38:10Z
author_association: MEMBER
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: Ah sorry, I just tested it on my local environment, not in the PL image / Ubuntu repo. Will test that now.
created_at: 2025-05-13T15:41:11Z
author_association: MEMBER
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: jonatanschroeder
body: Seems like the homebrew version is newer than the Ubuntu/Amazon Linux versions.
created_at: 2025-05-13T15:41:32Z
author_association: MEMBER
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: https://gitlab.com/graphviz/graphviz/-/merge_requests/4075
created_at: 2025-05-13T15:50:56Z
author_association: MEMBER
diff_hunk: @@ -729,7 +729,8 @@ def create_table_for_html_display(
             if raw_submitted_answer is not None:
                 display_array += '  value= "'
                 display_array += escape(raw_submitted_answer)
-            display_array += '" /> </td>'
+                display_array += '" '
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: nwalters512
body: Nit, while we're here, maybe condense the these three lines into `+= f' value="{escape(raw_submitted_answer)}"'?
created_at: 2025-05-13T15:53:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -729,7 +729,8 @@ def create_table_for_html_display(
             if raw_submitted_answer is not None:
                 display_array += '  value= "'
                 display_array += escape(raw_submitted_answer)
-            display_array += '" /> </td>'
+                display_array += '" '
+            display_array += " /> </td>"
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: nwalters512
body: Nit, unnecessary space. Both this and the above comment apply below as well.

```suggestion
            display_array += "/> </td>"
```
created_at: 2025-05-13T15:55:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: Confirmed that the regex ([444cbd8](https://github.com/PrairieLearn/PrairieLearn/pull/11977/commits/444cbd88e262e5d31dfa0d1856cdb335b9c3d901) and [879da81](https://github.com/PrairieLearn/PrairieLearn/pull/11977/commits/879da81918a4d02f045317ada8f768ade128ef20) at least works for me locally on my mac.
![CleanShot 2025-05-13 at 10 58 54@2x](https://github.com/user-attachments/assets/13100294-ed86-492d-aa4c-2d5770372ae6)

created_at: 2025-05-13T15:59:31Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: nwalters512
body: I don't think this `generate` function is even necessary in the first place since all the grading is done strictly by `grade()` below. Can we just remove it?
created_at: 2025-05-13T16:03:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: reteps
body: This is actually useful for determining the correct answer via tests.
created_at: 2025-05-13T16:05:06Z
author_association: MEMBER
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: jonatanschroeder
body: The updated version with regex expressions works for me as well. One suggestion is to include a comment that this could be fixed by using svg_inline, but it requires a newer version of graphviz that is currently not available for us (include a link to the docs). This should allow future implementations to potentially update this with a more robust option once graphviz becomes available.
created_at: 2025-05-13T16:18:03Z
author_association: MEMBER
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: Added some docs in f0f064a2a6830ed042a57c293ac544b023d20c79
created_at: 2025-05-13T16:31:12Z
author_association: MEMBER
diff_hunk: @@ -729,7 +729,8 @@ def create_table_for_html_display(
             if raw_submitted_answer is not None:
                 display_array += '  value= "'
                 display_array += escape(raw_submitted_answer)
-            display_array += '" /> </td>'
+                display_array += '" '
+            display_array += " /> </td>"
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: reteps
body: Changed in [5265d0f](https://github.com/PrairieLearn/PrairieLearn/pull/11977/commits/5265d0f8c13cec6774fc044f235e6a2d407fbd48).
created_at: 2025-05-13T16:31:27Z
author_association: MEMBER
diff_hunk: @@ -729,7 +729,8 @@ def create_table_for_html_display(
             if raw_submitted_answer is not None:
                 display_array += '  value= "'
                 display_array += escape(raw_submitted_answer)
-            display_array += '" /> </td>'
+                display_array += '" '
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: reteps
body: Changed in [5265d0f](https://github.com/PrairieLearn/PrairieLearn/pull/11977/commits/5265d0f8c13cec6774fc044f235e6a2d407fbd48).
created_at: 2025-05-13T16:31:45Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: nwalters512
body: Can `test()` itself generate the correct answer?

More broadly, as this is a workshop example, I'm far less interested in having the tests work than I am in having a short, easy-to-understand example. If I were in a workshop, I'd ask "why are we specifying a correct answer" and I think any of the possible answers ("I don't know", "Don't worry about it", "Ah! You see, we have this automated system for testing questions...") aren't really anything we want to bring up in a workshop.
created_at: 2025-05-13T16:36:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: reteps
body: What about displaying a sample correct answer to the student?
created_at: 2025-05-13T17:18:11Z
author_association: MEMBER
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: jonatanschroeder
body: I [commented in the commit](https://github.com/PrairieLearn/PrairieLearn/commit/f0f064a2a6830ed042a57c293ac544b023d20c79#r156933711), but those can sometimes be lost, so repeating here:

> Nit: https://gitlab.com/graphviz/graphviz/-/merge_requests/3404 may be a better link here. AFAIK Amazon Linux is not affected by the Debian issues.
created_at: 2025-05-13T17:24:31Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: nwalters512
body: You're possibly better off using LaTeX directly, along with some text like "here's one potential answer, though there are many". Note that in this question, the input is deliberately hidden in the answer panel.
created_at: 2025-05-13T17:33:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -713,29 +705,26 @@ def create_table_for_html_display(
         display_array += "<tr>"
         # Add first row
         display_array += (
-            '<td class="pl-matrix-component-input-close-left" rowspan="'
-            + str(m)
-            + '"></td>'
+            f'<td class="pl-matrix-component-input-close-left" rowspan="{m}"></td>'
         )
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+        display_array += f'<td style="width:4px" rowspan="{m}"></td>'
         for j in range(n):
             each_entry_name = name + str(j + 1)
             raw_submitted_answer = data["raw_submitted_answers"].get(
                 each_entry_name, None
             )
-            display_array += f' <td> <input name= "{each_entry_name}" type="text" size="8" aria-label="Row 1, Column {j + 1}" '
-            if not editable:
-                display_array += " disabled "
-            if raw_submitted_answer is not None:
-                display_array += '  value= "'
-                display_array += escape(raw_submitted_answer)
-            display_array += '" /> </td>'
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+            disabled = "disabled" if not editable else ""
+            value = (
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: eliotwrobson
body: Nit: I think it's a little cleaner to do this with an if statement instead of an inline if-else.
created_at: 2025-05-14T15:12:29Z
author_association: COLLABORATOR
diff_hunk: @@ -713,29 +705,26 @@ def create_table_for_html_display(
         display_array += "<tr>"
         # Add first row
         display_array += (
-            '<td class="pl-matrix-component-input-close-left" rowspan="'
-            + str(m)
-            + '"></td>'
+            f'<td class="pl-matrix-component-input-close-left" rowspan="{m}"></td>'
         )
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+        display_array += f'<td style="width:4px" rowspan="{m}"></td>'
         for j in range(n):
             each_entry_name = name + str(j + 1)
             raw_submitted_answer = data["raw_submitted_answers"].get(
                 each_entry_name, None
             )
-            display_array += f' <td> <input name= "{each_entry_name}" type="text" size="8" aria-label="Row 1, Column {j + 1}" '
-            if not editable:
-                display_array += " disabled "
-            if raw_submitted_answer is not None:
-                display_array += '  value= "'
-                display_array += escape(raw_submitted_answer)
-            display_array += '" /> </td>'
-        display_array += '<td style="width:4px" rowspan="' + str(m) + '"></td>'
+            disabled = "disabled" if not editable else ""
+            value = (
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: reteps
body: Changed in 1d3119a0b0bcd30053f2346c888c8fe3e1dd516b
created_at: 2025-05-14T18:52:40Z
author_association: MEMBER
diff_hunk: @@ -193,7 +193,7 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if not log_warnings:
             warnings.simplefilter("ignore")
 
-        svg_bytes = translated_dotcode.draw(format="svg", prog=engine)
+        svg_bytes = translated_dotcode.draw(format="inline_svg", prog=engine)
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: Changed in 1d3119a0b0bcd30053f2346c888c8fe3e1dd516b.
created_at: 2025-05-14T18:52:50Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: reteps
body: Since `test()` runs after the element code, it's way more difficult to do it this way -- now you have to define the raw correct/incorrect/invalid submission for the problem. This is a symptom of how `test()` is designed -- I think that deserves a rethink, but it's not particularly important to me right now.
created_at: 2025-05-14T18:57:14Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: reteps
body: I think generally, I would be excited for a discussion regarding more flexible control flow. E.g. a field you set of where you want the control flow to go. `data["next"] = "test_element"`. Not sure how exactly I would want that to work yet.
created_at: 2025-05-14T19:00:34Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,7 @@
 
 def generate(data):
     A = np.zeros((2, 2))
+    A[0, 1] = 1
path: exampleCourse/questions/workshop/Lesson4_example1/server.py
user: login: nwalters512
body: Fair. I don't want this to hold up this PR; it doesn't make the question any worse than it already is!
created_at: 2025-05-14T21:47:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1546,7 +1546,7 @@ def generate(data):
 
 #### Details
 
-Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the `rankdir` used in rendering. See the [Graphviz documentation on attributes](https://graphviz.org/doc/info/attrs.html) for more information on what attributes are supported.
+Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the `rankdir` used in rendering. See the [Graphviz documentation on attributes](https://graphviz.org/doc/info/attrs.html) for more information on what attributes are supported. The currently used Graphviz version is 2.44.0.
path: docs/elements.md
user: login: nwalters512
body: Whoa - are we really 11 major version behind? I guess it's up to AL2023 to determine when they pull in a new version, which would probably at this point be in AL2025?
created_at: 2025-05-14T21:51:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -559,24 +559,20 @@ def create_table_for_html_display(
 ) -> str:
     editable = data["editable"]
 
-    label_attr = (
-        f'aria-label="{aria_label}"'
-        if aria_label is not None
-        else (
-            f'aria-labelledby="pl-matrix-component-input-{label_uuid}-label"'
-            if label
-            else ""
-        )
-    )
+    label_attr = ""
+
+    if aria_label is not None:
+        label_attr = f'aria-label="{aria_label}"'
+    elif label is not None:
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: Copilot
body: Using `label is not None` will include empty-string labels. Consider checking truthiness (e.g. `elif label:`) to avoid rendering an aria-labelledby attribute when the label is empty.
```suggestion
    elif label:
```
created_at: 2025-05-14T21:53:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,3 +1,4 @@
+import re
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: Copilot
body: You could precompile `XML_DECLARATION` and `DOCTYPE_DECLARATION` with `re.compile` at module load to avoid recompiling the patterns on every render call.
created_at: 2025-05-14T21:53:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,5 +200,9 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if svg_bytes is None:
             raise TypeError("Graph was not returned.")
         svg = svg_bytes.decode("utf-8", "strict")
+        # https://gitlab.com/graphviz/graphviz/-/merge_requests/3404
+        # We can switch to svg_inline when we have use graphviz >= 10.0.1
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: Copilot
body: [nitpick] It may help future maintainers to include a brief explanation of why XML/DOCTYPE declarations must be stripped (e.g. compatibility details or embedding requirements).
```suggestion
        # We can switch to svg_inline when we have use graphviz >= 10.0.1
        # Strip XML and DOCTYPE declarations to ensure compatibility with embedding
        # requirements in HTML documents. These declarations can cause rendering
        # issues in some web browsers. For more details, see the linked Graphviz
        # merge request above.
```
created_at: 2025-05-14T21:53:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -559,24 +559,20 @@ def create_table_for_html_display(
 ) -> str:
     editable = data["editable"]
 
-    label_attr = (
-        f'aria-label="{aria_label}"'
-        if aria_label is not None
-        else (
-            f'aria-labelledby="pl-matrix-component-input-{label_uuid}-label"'
-            if label
-            else ""
-        )
-    )
+    label_attr = ""
+
+    if aria_label is not None:
+        label_attr = f'aria-label="{aria_label}"'
+    elif label is not None:
path: apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py
user: login: reteps
body: LABEL_DEFAULT is None, so this should be fine.
created_at: 2025-05-14T22:30:34Z
author_association: MEMBER
diff_hunk: @@ -1546,7 +1546,7 @@ def generate(data):
 
 #### Details
 
-Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the `rankdir` used in rendering. See the [Graphviz documentation on attributes](https://graphviz.org/doc/info/attrs.html) for more information on what attributes are supported.
+Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the `rankdir` used in rendering. See the [Graphviz documentation on attributes](https://graphviz.org/doc/info/attrs.html) for more information on what attributes are supported. The currently used Graphviz version is 2.44.0.
path: docs/elements.md
user: login: reteps
body: Yeah, but apparently AL2025 isn't going to happen till mid-2026 😬 https://www.reddit.com/r/aws/comments/1jjr4ps/comment/mjpyvq1/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
created_at: 2025-05-14T22:32:08Z
author_association: MEMBER
diff_hunk: @@ -197,5 +200,9 @@ def render(element_html: str, data: pl.QuestionData) -> str:
         if svg_bytes is None:
             raise TypeError("Graph was not returned.")
         svg = svg_bytes.decode("utf-8", "strict")
+        # https://gitlab.com/graphviz/graphviz/-/merge_requests/3404
+        # We can switch to svg_inline when we have use graphviz >= 10.0.1
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: I think that the note about `svg_inline` is sufficient.
created_at: 2025-05-14T22:32:41Z
author_association: MEMBER
diff_hunk: @@ -1,3 +1,4 @@
+import re
path: apps/prairielearn/elements/pl-graph/pl-graph.py
user: login: reteps
body: We could, this also doesn't seem necessary.
created_at: 2025-05-14T22:33:02Z
author_association: MEMBER
diff_hunk: @@ -1546,7 +1546,7 @@ def generate(data):
 
 #### Details
 
-Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the `rankdir` used in rendering. See the [Graphviz documentation on attributes](https://graphviz.org/doc/info/attrs.html) for more information on what attributes are supported.
+Note that using networkx for rendering, attributes from the input networkx graph are retained when creating a Graphviz DOT visualization. As a result, it is possible to set node and edge properties such as color, line weight, as part of the input graph and have these reflected in the rendering. These include global properties of the graph, such as the `rankdir` used in rendering. See the [Graphviz documentation on attributes](https://graphviz.org/doc/info/attrs.html) for more information on what attributes are supported. The currently used Graphviz version is 2.44.0.
path: docs/elements.md
user: login: jonatanschroeder
body: Ubuntu is also [in 2.42.4 in 25.04](https://packages.ubuntu.com/plucky/graphviz) (i.e., the Ubuntu version released last month). I wouldn't hold my breath for this version to be updated in the repo itself, if we want a newer version we may need to [retrieve it explicitly](https://gitlab.com/graphviz/graphviz/-/releases).

Also, I was checking online and [some reddit thread](https://www.reddit.com/r/aws/comments/1jjr4ps/comment/mjz8kms/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) mentioned that it's unlikely there'll be AL2025, given the lack of communication in this regard. I know this isn't reliable info, but I haven't found anything better.

And we might want to do it anyway... There's a [CVE for this version of graphviz](https://www.cve.org/CVERecord?id=CVE-2023-46045) (though it doesn't seem something easily exploitable in our context).
created_at: 2025-05-15T13:17:41Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-13T05:30:28Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

```markdown
## Walkthrough

This update removes XML and DOCTYPE declarations from SVG output in the graph element, refactors HTML input tag generation in the matrix component input for readability, updates documentation hyperlinks in an example question, and explicitly assigns zero partial score in a workshop lesson's grading logic. No public API signatures were changed.

## Changes

| Files/Paths                                                                                              | Change Summary                                                                                                                      |
|----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| apps/prairielearn/elements/pl-graph/pl-graph.py                                                          | Added regex removal of XML and DOCTYPE declarations from SVG string after graphviz rendering.                                       |
| apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py                         | Refactored HTML table cell and input element string construction using f-strings instead of concatenation for clarity.              |
| exampleCourse/questions/element/graph/question.html                                                       | Updated two empty hyperlink references to point to PrairieLearn ReadTheDocs documentation URLs.                                     |
| exampleCourse/questions/workshop/Lesson4_example1/server.py                                               | Set matrix element explicitly in `generate`; explicitly assigned zero partial score in `grade` else branch for completeness.        |
| docs/elements.md                                                                                          | Added Graphviz version (2.44.0) information to the pl-graph element documentation.                                                  |

## Suggested labels

`accessibility`

## Suggested reviewers

- nwalters512
- reteps
- eliotwrobson
```

</details>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f0f064a2a6830ed042a57c293ac544b023d20c79 and 1d3119a0b0bcd30053f2346c888c8fe3e1dd516b.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `apps/prairielearn/elements/pl-graph/pl-graph.py` (3 hunks)
* `apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py` (6 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/prairielearn/elements/pl-matrix-component-input/pl-matrix-component-input.py

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/prairielearn/elements/pl-graph/pl-graph.py

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: build-core-images / build-images (ubuntu-24.04-arm)
* GitHub Check: build-core-images / build-images (ubuntu-24.04)
* GitHub Check: native-checks-js
* GitHub Check: native-checks
* GitHub Check: test-prairielearn

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=PrairieLearn/PrairieLearn&utm_content=11977):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-13T05:32:23Z
body: ## [Codecov](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11977?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 12.13%. Comparing base ([`38a29c0`](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/commit/38a29c0df631a08c83edd7e17e34062e878323e8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn)) to head ([`1d3119a`](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/commit/1d3119a0b0bcd30053f2346c888c8fe3e1dd516b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn)).
:warning: Report is 419 commits behind head on master.
> :exclamation:  There is a different number of reports uploaded between BASE (38a29c0) and HEAD (1d3119a). Click for more details.
> 
> <details><summary>HEAD has 2 uploads less than BASE</summary>
>
>| Flag | BASE (38a29c0) | HEAD (1d3119a) |
>|------|------|------|
>|python|2|1|
>|javascript|2|1|
></details>

<details><summary>Additional details and impacted files</summary>



```diff
@@             Coverage Diff             @@
##           master   #11977       +/-   ##
===========================================
- Coverage   59.95%   12.13%   -47.82%     
===========================================
  Files         695      695               
  Lines      106704   106777       +73     
  Branches     6815     1299     -5516     
===========================================
- Hits        63975    12960    -51015     
- Misses      42218    93806    +51588     
+ Partials      511       11      -500     
```

| [Flag](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11977/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | Coverage Δ | |
|---|---|---|
| [javascript](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11977/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | `10.65% <ø> (-48.85%)` | :arrow_down: |
| [python](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11977/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | `81.04% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn#carryforward-flags-in-the-pull-request-comment) to find out more.
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11977?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn).
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T05:34:28Z
body: ## All images

| Image | Platform | Old Size | New Size | Change |
| --- | --- | --- | --- | --- |
| [prairielearn/executor:1d3119a0b0bcd30053f2346c888c8fe3e1dd516b](https://hub.docker.com/layers/prairielearn/executor/1d3119a0b0bcd30053f2346c888c8fe3e1dd516b/images/sha256-78d6bd5a4d4eea3efebc2018ec102c0e283983e854197eeb25bbe506d8f5611a?context=explore) | linux/amd64 | 1218.71 MB | 1218.71 MB | -0.00% |
| [prairielearn/executor:1d3119a0b0bcd30053f2346c888c8fe3e1dd516b](https://hub.docker.com/layers/prairielearn/executor/1d3119a0b0bcd30053f2346c888c8fe3e1dd516b/images/sha256-d7b6c9b9f0f6cd501ba4e59c8f9271b68b7d3c7a4ea5a37a562aa704a4534441?context=explore) | linux/arm64 | 1232.00 MB | 1232.00 MB | -0.00% |
| [prairielearn/prairielearn:1d3119a0b0bcd30053f2346c888c8fe3e1dd516b](https://hub.docker.com/layers/prairielearn/prairielearn/1d3119a0b0bcd30053f2346c888c8fe3e1dd516b/images/sha256-69ccd437bc812b49f637b804f787e017e532c8e26ee0093232e83b9d1b944fb7?context=explore) | linux/amd64 | 1218.71 MB | 1218.71 MB | -0.00% |
| [prairielearn/prairielearn:1d3119a0b0bcd30053f2346c888c8fe3e1dd516b](https://hub.docker.com/layers/prairielearn/prairielearn/1d3119a0b0bcd30053f2346c888c8fe3e1dd516b/images/sha256-566bcf5173089dca47606cdd401cf6f3798bb01e74cded5362cb40fd30eb3d66?context=explore) | linux/arm64 | 1232.00 MB | 1232.00 MB | -0.00% |
author_association: CONTRIBUTOR
user: login: reteps
created_at: 2025-05-13T16:32:23Z
body: I haven't tested the `pl-matrix-component-input` changes and won't have time till tomorrow to finish this.
author_association: MEMBER