PyKubeGrader 0.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, M3-Learning: Multifunctional Materials and Machine Learning
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.1
2
+ Name: PyKubeGrader
3
+ Version: 0.0.4
4
+ Summary: Add a short description here!
5
+ Home-page: https://github.com/pyscaffold/pyscaffold/
6
+ Author: jagar2
7
+ Author-email: jca92@drexel.edu
8
+ License: MIT
9
+ Project-URL: Documentation, https://pyscaffold.org/
10
+ Platform: any
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Description-Content-Type: text/x-rst; charset=UTF-8
14
+ License-File: LICENSE.txt
15
+ Requires-Dist: importlib-metadata; python_version < "3.8"
16
+ Provides-Extra: testing
17
+ Requires-Dist: setuptools; extra == "testing"
18
+ Requires-Dist: pytest; extra == "testing"
19
+ Requires-Dist: pytest-cov; extra == "testing"
20
+
21
+ .. These are examples of badges you might want to add to your README:
22
+ please update the URLs accordingly
23
+
24
+ .. image:: https://api.cirrus-ci.com/github/<USER>/PyKubeGrader.svg?branch=main
25
+ :alt: Built Status
26
+ :target: https://cirrus-ci.com/github/<USER>/PyKubeGrader
27
+ .. image:: https://readthedocs.org/projects/PyKubeGrader/badge/?version=latest
28
+ :alt: ReadTheDocs
29
+ :target: https://PyKubeGrader.readthedocs.io/en/stable/
30
+ .. image:: https://img.shields.io/coveralls/github/<USER>/PyKubeGrader/main.svg
31
+ :alt: Coveralls
32
+ :target: https://coveralls.io/r/<USER>/PyKubeGrader
33
+ .. image:: https://img.shields.io/pypi/v/PyKubeGrader.svg
34
+ :alt: PyPI-Server
35
+ :target: https://pypi.org/project/PyKubeGrader/
36
+ .. image:: https://img.shields.io/conda/vn/conda-forge/PyKubeGrader.svg
37
+ :alt: Conda-Forge
38
+ :target: https://anaconda.org/conda-forge/PyKubeGrader
39
+ .. image:: https://pepy.tech/badge/PyKubeGrader/month
40
+ :alt: Monthly Downloads
41
+ :target: https://pepy.tech/project/PyKubeGrader
42
+ .. image:: https://img.shields.io/twitter/url/http/shields.io.svg?style=social&label=Twitter
43
+ :alt: Twitter
44
+ :target: https://twitter.com/PyKubeGrader
45
+
46
+ .. image:: https://img.shields.io/badge/-PyScaffold-005CA0?logo=pyscaffold
47
+ :alt: Project generated with PyScaffold
48
+ :target: https://pyscaffold.org/
49
+
50
+ |
51
+
52
+ ============
53
+ PyKubeGrader
54
+ ============
55
+
56
+
57
+ Add a short description here!
58
+
59
+
60
+ A longer description of your project goes here...
61
+
62
+
63
+ .. _pyscaffold-notes:
64
+
65
+ Note
66
+ ====
67
+
68
+ This project has been set up using PyScaffold 4.6. For details and usage
69
+ information on PyScaffold see https://pyscaffold.org/.
@@ -0,0 +1,17 @@
1
+ pykubegrader/__init__.py,sha256=JHme-EVdE2QbS9_Q2qdHiy5bEsyeQDbZ8aP2OfoSyiw,583
2
+ pykubegrader/widgets/info_widget.py,sha256=x73heTqmqJQ9XkaE3eW4eLXTGJlmmTdGEkGtbIcY7uY,3532
3
+ pykubegrader/widgets/mc_widget.py,sha256=eOFuZwEIc8NtgZS-Xnc9e49C1i0qYJvwFPyDVy7jq0c,2025
4
+ pykubegrader/widgets/misc.py,sha256=dKw6SyRYU3DWRgD3xER7wq-C9e1daWPkqr901LpcwiQ,642
5
+ pykubegrader/widgets/multi_select_base.py,sha256=GJQ-xFJTErar94cFwTxsSmLKPPmywJRUQELhAbv-uFs,3102
6
+ pykubegrader/widgets/reading_base.py,sha256=vSoyThl2vFbnbgB_LDc-DDRPWCM2xGbazEptDjNn4uA,5311
7
+ pykubegrader/widgets/reading_widget.py,sha256=e8lXSzGI9dHCxW7fIvwJUU6j8qWQgHnKR06ke2W1k7w,3022
8
+ pykubegrader/widgets/select_base.py,sha256=cyTWKYRbsGYsYJh6ntW2vPub4r15c8IgcsMFSCG3St4,2121
9
+ pykubegrader/widgets/select_many_widget.py,sha256=Rmb9Es42qzSdPfhU6ypcKnw_30PwuARX2UKVwgwbQhg,3772
10
+ pykubegrader/widgets/telemetry.py,sha256=3GhItJ9dw18XcFutfQEk4z-DllCVtKXCKKLesf_jxzQ,3194
11
+ pykubegrader/widgets/types_widget.py,sha256=FOrnR2x1eNM-pcFNIC8JnQdylgSUSUzuACmiIRuFTbQ,2244
12
+ pykubegrader/widgets/validate.py,sha256=guEj0yeu9A_-Ig6S8diOfaNEWxud3k7t4tI-i4Steak,10585
13
+ PyKubeGrader-0.0.4.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
14
+ PyKubeGrader-0.0.4.dist-info/METADATA,sha256=iMrtcscGbzaKzGx_dDYNVrPG6XFcuNIGpuTTkjz2rrY,2357
15
+ PyKubeGrader-0.0.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
16
+ PyKubeGrader-0.0.4.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
17
+ PyKubeGrader-0.0.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pykubegrader
@@ -0,0 +1,16 @@
1
+ import sys
2
+
3
+ if sys.version_info[:2] >= (3, 8):
4
+ # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
5
+ from importlib.metadata import PackageNotFoundError, version # pragma: no cover
6
+ else:
7
+ from importlib_metadata import PackageNotFoundError, version # pragma: no cover
8
+
9
+ try:
10
+ # Change here if project is renamed and does not equal the package name
11
+ dist_name = "PyKubeGrader"
12
+ __version__ = version(dist_name)
13
+ except PackageNotFoundError: # pragma: no cover
14
+ __version__ = "unknown"
15
+ finally:
16
+ del version, PackageNotFoundError
@@ -0,0 +1,108 @@
1
+ import os
2
+ import re
3
+ import socket
4
+
5
+ import numpy as np
6
+ import panel as pn
7
+
8
+ from .telemetry import ensure_responses, update_responses
9
+
10
+ EMAIL_PATTERN = re.compile(r"[a-z]+\d+@drexel\.edu")
11
+
12
+ KEYS = [
13
+ "first_name",
14
+ "last_name",
15
+ "drexel_id",
16
+ "drexel_email",
17
+ "hostname",
18
+ "ip_address",
19
+ "jupyter_user",
20
+ "seed",
21
+ ]
22
+
23
+
24
+ class StudentInfoForm:
25
+ def __init__(self, **kwargs) -> None:
26
+ self.first_name = kwargs.get("first_name", "")
27
+ self.last_name = kwargs.get("last_name", "")
28
+ self.drexel_id = kwargs.get("drexel_id", "")
29
+ self.drexel_email = kwargs.get("drexel_email", "")
30
+ self.hostname = kwargs.get("hostname", "")
31
+ self.ip_address = kwargs.get("ip_address", "")
32
+ self.jupyter_user = kwargs.get("jupyter_user", "")
33
+ self.seed = kwargs.get("seed", np.random.randint(0, 100))
34
+
35
+ self.first_name_widget = pn.widgets.TextInput(
36
+ name="First Name", value=self.first_name
37
+ )
38
+ self.last_name_widget = pn.widgets.TextInput(
39
+ name="Last Name", value=self.last_name
40
+ )
41
+ self.drexel_id_widget = pn.widgets.TextInput(
42
+ name="Drexel ID", value=self.drexel_id
43
+ )
44
+ self.drexel_email_widget = pn.widgets.TextInput(
45
+ name="Drexel Email", value=self.drexel_email
46
+ )
47
+
48
+ self.submit_button = pn.widgets.Button(
49
+ name="Submit", button_type="primary", styles=dict(margin_top="1.5em")
50
+ )
51
+ self.submit_button.on_click(self.submit)
52
+
53
+ self.message = pn.pane.Str("") # Placeholder for status message
54
+
55
+ self.layout = pn.Column(
56
+ "# Student Information Form",
57
+ self.first_name_widget,
58
+ self.last_name_widget,
59
+ self.drexel_id_widget,
60
+ self.drexel_email_widget,
61
+ self.submit_button,
62
+ self.message,
63
+ )
64
+
65
+ def submit(self, _) -> None:
66
+ info = ensure_responses()
67
+
68
+ info["first_name"] = self.first_name_widget.value.strip()
69
+ info["last_name"] = self.last_name_widget.value.strip()
70
+ info["drexel_id"] = self.drexel_id_widget.value.strip()
71
+ info["drexel_email"] = self.drexel_email_widget.value.strip()
72
+
73
+ info["hostname"] = socket.gethostname()
74
+ info["jupyter_user"] = os.environ.get("JUPYTERHUB_USER", "Not on JupyterHub")
75
+
76
+ if "seed" not in info:
77
+ info["seed"] = np.random.randint(0, 100)
78
+
79
+ try:
80
+ info["ip_address"] = socket.gethostbyname(info["hostname"])
81
+ except socket.gaierror:
82
+ info["ip_address"] = "IP unavailable"
83
+
84
+ try:
85
+ for key in KEYS:
86
+ if info[key] == "":
87
+ raise ValueError(f"Missing form input: {key}")
88
+
89
+ if not EMAIL_PATTERN.fullmatch(info["drexel_email"]):
90
+ raise ValueError(f"Invalid email format: {info['drexel_email']}")
91
+
92
+ email_prefix = info["drexel_email"].split("@")[0]
93
+ if info["drexel_id"] != email_prefix:
94
+ raise ValueError(
95
+ f"Drexel ID {info['drexel_id']} does not match email {info['drexel_email']}"
96
+ )
97
+
98
+ for key in KEYS:
99
+ update_responses(key, info[key])
100
+
101
+ self.message.object = "Student info recorded successfully!"
102
+ self.message.style = {"color": "green"}
103
+ except ValueError as e:
104
+ self.message.object = str(e)
105
+ self.message.style = {"color": "red"}
106
+
107
+ def show(self):
108
+ return self.layout
@@ -0,0 +1,72 @@
1
+ from typing import Tuple
2
+
3
+ import panel as pn
4
+
5
+ from .misc import list_of_lists
6
+ from .select_base import SelectQuestion
7
+
8
+
9
+ def MCQ(
10
+ descriptions: list[str],
11
+ options: list[str] | list[list[str]],
12
+ initial_vals: list[str],
13
+ ) -> Tuple[list[pn.pane.HTML], list[pn.widgets.RadioButtonGroup]]:
14
+ desc_width = "350px"
15
+
16
+ desc_widgets = [
17
+ pn.pane.HTML(
18
+ f"<div style='text-align: left; width: {desc_width};'><b>{desc}</b></div>"
19
+ )
20
+ for desc in descriptions
21
+ ]
22
+
23
+ radio_buttons = [
24
+ pn.widgets.RadioButtonGroup(
25
+ options=option,
26
+ value=value,
27
+ width=300,
28
+ )
29
+ for value, option in zip(
30
+ initial_vals,
31
+ options if list_of_lists(options) else [options] * len(initial_vals),
32
+ )
33
+ ]
34
+
35
+ return desc_widgets, radio_buttons
36
+
37
+
38
+ class MCQuestion(SelectQuestion):
39
+ def __init__(
40
+ self,
41
+ title="Select the option that matches the definition:",
42
+ style=MCQ,
43
+ question_number=2,
44
+ keys=["MC1", "MC2", "MC3", "MC4"],
45
+ options=[
46
+ ["List", "Dictionary", "Tuple", "Set"],
47
+ ["return", "continue", "pass", "break"],
48
+ ["*", "^", "**", "//"],
49
+ [
50
+ "list.add(element)",
51
+ "list.append(element)",
52
+ "list.insert(element)",
53
+ "list.push(element)",
54
+ ],
55
+ ],
56
+ descriptions=[
57
+ "Which of the following stores key:value pairs?",
58
+ "The following condition returns to the next iteration of the loop",
59
+ "Which operator is used for exponentiation in Python?",
60
+ "Which method is used to add an element to the end of a list in Python?",
61
+ ],
62
+ points=2,
63
+ ):
64
+ super().__init__(
65
+ title=title,
66
+ style=style,
67
+ question_number=question_number,
68
+ keys=keys,
69
+ options=options,
70
+ descriptions=descriptions,
71
+ points=points,
72
+ )
@@ -0,0 +1,29 @@
1
+ import random
2
+ from typing import Tuple
3
+
4
+ import panel as pn
5
+
6
+
7
+ def list_of_lists(options: list) -> bool:
8
+ return all(isinstance(elem, list) for elem in options)
9
+
10
+
11
+ def shuffle_options(options, seed: int):
12
+ random.seed(seed)
13
+ random.shuffle(options)
14
+
15
+ return options
16
+
17
+
18
+ def shuffle_questions(
19
+ desc_widgets: list[pn.pane.HTML],
20
+ dropdowns: list[pn.widgets.Select] | list[pn.Column],
21
+ seed: int,
22
+ ) -> list[Tuple[pn.pane.HTML, pn.widgets.Select | pn.Column]]:
23
+ random.seed(seed)
24
+
25
+ # Combine widgets into pairs
26
+ widget_pairs = list(zip(desc_widgets, dropdowns))
27
+
28
+ random.shuffle(widget_pairs)
29
+ return widget_pairs
@@ -0,0 +1,99 @@
1
+ from typing import Callable, Tuple
2
+
3
+ import panel as pn
4
+
5
+ from .misc import shuffle_questions
6
+ from .telemetry import ensure_responses, update_responses
7
+
8
+
9
+ class MultiSelectQuestion:
10
+ def __init__(
11
+ self,
12
+ title: str,
13
+ style: Callable[
14
+ [list[str], list[list[str]], list[bool]],
15
+ Tuple[list[pn.pane.HTML], list[pn.Column]],
16
+ ],
17
+ question_number: int,
18
+ keys: list[str],
19
+ options: list[list[str]],
20
+ descriptions: list[str],
21
+ points: int,
22
+ ):
23
+ responses = ensure_responses()
24
+
25
+ self.points = points
26
+ self.question_number = question_number
27
+ self.style = style
28
+
29
+ flat_index = 0
30
+ self.keys: list[str] = []
31
+ for i, _ in enumerate(keys):
32
+ for _ in options[i]:
33
+ flat_index += 1 # Start at 1
34
+ self.keys.append(f"q{question_number}_{flat_index}")
35
+
36
+ try:
37
+ seed: int = responses["seed"]
38
+ except ValueError:
39
+ raise ValueError(
40
+ "You must submit your student info before starting the exam"
41
+ )
42
+
43
+ # Dynamically assigning attributes based on keys, with default values from responses
44
+ for key in self.keys:
45
+ setattr(self, key, responses.get(key, False))
46
+
47
+ self.initial_vals = [getattr(self, key) for key in self.keys]
48
+
49
+ description_widgets, self.widgets = style(
50
+ descriptions, options, self.initial_vals
51
+ )
52
+
53
+ self.submit_button = pn.widgets.Button(name="Submit")
54
+ self.submit_button.on_click(self.submit)
55
+
56
+ widget_pairs = shuffle_questions(description_widgets, self.widgets, seed)
57
+
58
+ # Panel layout
59
+ question_header = pn.pane.HTML(
60
+ f"<h2>Question {self.question_number}: {title}</h2>"
61
+ )
62
+ question_body = pn.Column(
63
+ *[
64
+ pn.Row(desc_widget, checkbox_set)
65
+ for desc_widget, checkbox_set in widget_pairs
66
+ ]
67
+ )
68
+
69
+ self.layout = pn.Column(question_header, question_body, self.submit_button)
70
+
71
+ def submit(self, _) -> None:
72
+ responses_flat: list[bool] = []
73
+ self.responses_nested: list[list[bool]] = []
74
+
75
+ for row in self.widgets:
76
+ next_selections = []
77
+
78
+ for widget in row.objects:
79
+ # Skip HTML widgets
80
+ if isinstance(widget, pn.pane.HTML):
81
+ continue
82
+
83
+ if isinstance(widget, pn.widgets.Checkbox):
84
+ next_selections.append(widget.value)
85
+ responses_flat.append(widget.value) # For flat list of responses
86
+
87
+ # Append all responses for this widget at once, forming a list of lists
88
+ self.responses_nested.append(next_selections)
89
+
90
+ self.record_responses(responses_flat)
91
+
92
+ def record_responses(self, responses_flat: list[bool]) -> None:
93
+ for key, value in zip(self.keys, responses_flat):
94
+ update_responses(key, value)
95
+
96
+ print("Responses recorded successfully")
97
+
98
+ def show(self):
99
+ return self.layout
@@ -0,0 +1,168 @@
1
+ import copy
2
+ from typing import Optional
3
+
4
+ import panel as pn
5
+
6
+ from .misc import shuffle_options
7
+ from .telemetry import ensure_responses, update_responses
8
+
9
+
10
+ class ReadingPython:
11
+ def __init__(
12
+ self,
13
+ title: str,
14
+ question_number: int,
15
+ options: dict,
16
+ ) -> None:
17
+ # Load responses from JSON (or create file if it doesn't exist)
18
+ responses = ensure_responses()
19
+
20
+ self.question_number = question_number
21
+
22
+ default = None
23
+
24
+ # Dynamically assign attributes based on keys, with default values from responses
25
+ for num in range(len(options["lines_to_comment"]) + options["n_rows"]):
26
+ key = f"q{question_number}_{num+1}"
27
+
28
+ # Dynamically assign the value from the responses file for persistence
29
+ if num < len(options["lines_to_comment"]):
30
+ setattr(self, key, responses.get(key, default))
31
+ else:
32
+ setattr(
33
+ self,
34
+ key,
35
+ responses.get(key, [default] * (len(options["table_headers"]) - 1)),
36
+ )
37
+
38
+ # Checks that a seed was assigned to responses
39
+ try:
40
+ seed: int = responses["seed"]
41
+ except ValueError:
42
+ raise ValueError(
43
+ "You must submit your student info before starting the exam"
44
+ )
45
+
46
+ #
47
+ # Question title
48
+ #
49
+
50
+ question_title = pn.pane.HTML(f"<h2>Question {question_number}: {title}</h2>")
51
+
52
+ #
53
+ # Comment dropdowns
54
+ #
55
+
56
+ self.dropdowns_for_comments: dict[str, pn.widgets.Select] = {
57
+ line: pn.widgets.Select(
58
+ options=shuffle_options(options["comments_options"], seed),
59
+ name=f"Line {line}:",
60
+ value=getattr(self, f"q{question_number}_{i_comments+1}"),
61
+ width=600,
62
+ )
63
+ for i_comments, line in enumerate(options["lines_to_comment"])
64
+ }
65
+
66
+ comment_dropdowns = pn.Column(*self.dropdowns_for_comments.values())
67
+
68
+ #
69
+ # Execution dropdowns
70
+ #
71
+
72
+ # Instructions
73
+ execution_instructions = pn.pane.HTML(
74
+ "<h3>For each step, select the appropriate response:</h3>"
75
+ )
76
+
77
+ # Header row
78
+ header_row = pn.Row(
79
+ *[
80
+ pn.pane.HTML(f"<strong>{header}</strong>", width=150)
81
+ for header in options["table_headers"]
82
+ ]
83
+ )
84
+
85
+ # Make a deep copy of the lines to comment and add a null value to the beginning
86
+ # This is to provide the null response to the question
87
+ line_comment: list[int | str] = copy.deepcopy(options["lines_to_comment"])
88
+ line_comment.insert(0, "")
89
+
90
+ dropdown_options = [
91
+ line_comment,
92
+ options["variables_changed"],
93
+ options["current_values"],
94
+ options["datatypes"],
95
+ ]
96
+
97
+ # Function to create a row with dropdowns
98
+ def create_row(step: int) -> pn.Row:
99
+ widgets_list = [
100
+ pn.pane.HTML(f"Step {step+1}", width=150)
101
+ if i == 0
102
+ else pn.widgets.Select(
103
+ options=dropdown_options[i - 1],
104
+ value=getattr(
105
+ self,
106
+ f'q{question_number}_{len(options["lines_to_comment"])+step+1}',
107
+ )[i - 1],
108
+ width=150,
109
+ )
110
+ for i in range(len(options["table_headers"]))
111
+ ]
112
+
113
+ return pn.Row(*widgets_list)
114
+
115
+ # Generate rows dynamically based on n_rows
116
+ self.rows = [create_row(step) for step in range(options["n_rows"])]
117
+
118
+ # Combine header and rows
119
+ execution_steps = pn.Column(header_row, *self.rows)
120
+
121
+ # Submit button
122
+ self.submit_button = pn.widgets.Button(name="Submit")
123
+ self.submit_button.on_click(self.submit)
124
+
125
+ # Combine everything into a single layout
126
+ self.layout = pn.Column(
127
+ question_title,
128
+ comment_dropdowns,
129
+ execution_instructions,
130
+ execution_steps,
131
+ self.submit_button,
132
+ )
133
+
134
+ def submit(self, _) -> None:
135
+ # Get section 1 responses
136
+ self.output_comments: list[str] = []
137
+ for out in self.dropdowns_for_comments.values():
138
+ self.output_comments.append(out.value if isinstance(out.value, str) else "")
139
+
140
+ # Get section 2 responses
141
+ self.output_execution: list[list[Optional[str | int]]] = []
142
+
143
+ for row in self.rows[:]:
144
+ row_value: list[Optional[str | int]] = []
145
+
146
+ for box in row.objects:
147
+ if isinstance(box, pn.widgets.Select):
148
+ row_value.append(box.value)
149
+
150
+ if not any(row_value):
151
+ continue
152
+
153
+ self.output_execution.append(row_value)
154
+
155
+ # Persist responses to JSON
156
+ i = 0
157
+ for comment_val in self.output_comments:
158
+ i += 1
159
+ update_responses(f"q{self.question_number}_{i}", comment_val)
160
+
161
+ for exec_val in self.output_execution:
162
+ i += 1
163
+ update_responses(f"q{self.question_number}_{i}", exec_val)
164
+
165
+ print("Responses recorded successfully")
166
+
167
+ def show(self):
168
+ return self.layout