PyKubeGrader 0.0.4__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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