PyKubeGrader 0.3.7__py3-none-any.whl → 0.3.9__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.
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/METADATA +2 -1
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/RECORD +22 -16
- pykubegrader/build/api_notebook_builder.py +43 -27
- pykubegrader/build/build_folder.py +29 -15
- pykubegrader/grade_reports/assignments.py +184 -0
- pykubegrader/grade_reports/class_grade_report.py +137 -0
- pykubegrader/grade_reports/grade_report.py +353 -0
- pykubegrader/grade_reports/grading_config.py +70 -0
- pykubegrader/grade_reports/test.ipynb +43 -0
- pykubegrader/grading_tester.ipynb +53 -2
- pykubegrader/submit/submit_assignment.py +4 -3
- pykubegrader/telemetry.py +88 -84
- pykubegrader/tokens/generator.py +75 -0
- pykubegrader/tokens/token_panel.py +2 -2
- pykubegrader/tokens/tokens.py +0 -1
- pykubegrader/widgets_base/multi_select.py +1 -1
- pykubegrader/widgets_base/select.py +1 -1
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/LICENSE.txt +0 -0
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/WHEEL +0 -0
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/entry_points.txt +0 -0
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/top_level.txt +0 -0
- /pykubegrader/grade_reports/{grade_reports.py → __grade_reports.py} +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: PyKubeGrader
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.9
|
4
4
|
Summary: Add a short description here!
|
5
5
|
Home-page: https://github.com/pyscaffold/pyscaffold/
|
6
6
|
Author: jagar2
|
@@ -26,6 +26,7 @@ Requires-Dist: ruff
|
|
26
26
|
Requires-Dist: setuptools
|
27
27
|
Requires-Dist: sphinx
|
28
28
|
Requires-Dist: types-python-dateutil
|
29
|
+
Requires-Dist: types-pyyaml
|
29
30
|
Requires-Dist: types-requests
|
30
31
|
Requires-Dist: types-setuptools
|
31
32
|
Provides-Extra: testing
|
@@ -1,24 +1,30 @@
|
|
1
1
|
pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
|
2
|
-
pykubegrader/grading_tester.ipynb,sha256=
|
2
|
+
pykubegrader/grading_tester.ipynb,sha256=D26O0fgIV1aIYDnIJQspu72lOOQfNS_boIx93apjKDQ,20025
|
3
3
|
pykubegrader/initialize.py,sha256=Bwu1q18l18FB9lGppvt-L41D5gzr3S8t6zC0_UbrASw,3994
|
4
|
-
pykubegrader/telemetry.py,sha256=
|
4
|
+
pykubegrader/telemetry.py,sha256=0Is-LhpVoKcEjmgqEjCfNLCKJVGAmJaQmD2VHF8KI9w,16396
|
5
5
|
pykubegrader/utils.py,sha256=jlJklKvRhY3O7Hz2aaU1m0y3p_n9eMAXNnAF7LUEaPY,1275
|
6
6
|
pykubegrader/validate.py,sha256=OKnItGyd-L8QPKcsE0KRuwBI_IxKiJzMLJKZiA2j3II,11184
|
7
7
|
pykubegrader/build/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
8
|
-
pykubegrader/build/api_notebook_builder.py,sha256=
|
9
|
-
pykubegrader/build/build_folder.py,sha256=
|
8
|
+
pykubegrader/build/api_notebook_builder.py,sha256=FVLnycCRLmZKbimw1FVwDVO-ufKUiAkcnb0O1NrUCEk,25997
|
9
|
+
pykubegrader/build/build_folder.py,sha256=ecriZoXp5SzpCvP532vXfox61gyJOg9YAYNVoiNq4Ck,87803
|
10
10
|
pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
|
11
11
|
pykubegrader/build/collate.py,sha256=cVvF7tf2U3iiH4R_dbghTcieedIx5w3Fyw9L_llInM8,6754
|
12
12
|
pykubegrader/build/markdown_questions.py,sha256=cSh8mkHK3hh-etJdgrZu9UQi1WPrKQtofkzLCUp1Z-w,4676
|
13
|
-
pykubegrader/grade_reports/
|
13
|
+
pykubegrader/grade_reports/__grade_reports.py,sha256=n8H_n9jdZRSPn2zlIf-GQt_Y8w91p6M8ZbdVH76Sg5k,2303
|
14
|
+
pykubegrader/grade_reports/assignments.py,sha256=Wr5jvTeQ0a7Fl89e6EzXKqjhLrsA6KyObOdrJdN1V0Y,7614
|
15
|
+
pykubegrader/grade_reports/class_grade_report.py,sha256=9Uuvy-xoABtjTjv-05VYhmowebgTrgEFSgh1Grws-cQ,5078
|
16
|
+
pykubegrader/grade_reports/grade_report.py,sha256=wxgSGjvX9rcE0s9Up2zxPACGv78sJ3TvP8px3bc7t-c,13993
|
17
|
+
pykubegrader/grade_reports/grading_config.py,sha256=_Wki6ju6OkqIuwEl-YMpYJcG5Yeu5wid-NEUN5Oxshs,2332
|
18
|
+
pykubegrader/grade_reports/test.ipynb,sha256=NkMZHcfBd-uJw3i1Y9ux-epBP5GiVFEhda5wxEbK0cU,808
|
14
19
|
pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
15
20
|
pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
|
16
21
|
pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
17
22
|
pykubegrader/log_parser/parse.ipynb,sha256=5e-9dzUbJk2M8kPP55lVeksm86lSY5ocKfWOP2RSWH0,11921
|
18
23
|
pykubegrader/log_parser/parse.py,sha256=dXzTEOTI6VTRNoHFDAjg6hZUhvB3kHtMb10_KW3NPrw,7641
|
19
|
-
pykubegrader/submit/submit_assignment.py,sha256=
|
20
|
-
pykubegrader/tokens/
|
21
|
-
pykubegrader/tokens/
|
24
|
+
pykubegrader/submit/submit_assignment.py,sha256=73bbCYNfeWHjxZw-TUuva737WEPQNA9DB6czeP5BIOM,2656
|
25
|
+
pykubegrader/tokens/generator.py,sha256=zqfO8GX4Xkhkxi0HGkGwiwuXTmL4ON_hcH8f-9Gg5qc,2648
|
26
|
+
pykubegrader/tokens/token_panel.py,sha256=jWwOUx4mr67iWyoIyjITMtwf9HtbAd_L5b-2x_Fif3g,1377
|
27
|
+
pykubegrader/tokens/tokens.py,sha256=5hEcNMUCpFtps0OjWV1V93zdk2cuIAwCfhBEp3PiuEI,1473
|
22
28
|
pykubegrader/tokens/validate_token.py,sha256=kvHX0NJBm21xzb2p67j7vq1La6J1XbmobEJQ3fTMdZA,3289
|
23
29
|
pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
24
30
|
pykubegrader/widgets/multiple_choice.py,sha256=ag6W-HN7isHkIUmB4BxtK8T1JhuV3FBLUBAhcV6rN80,2729
|
@@ -30,12 +36,12 @@ pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs
|
|
30
36
|
pykubegrader/widgets/true_false.py,sha256=QllIhHuJstJft_RuShkxI_fFFTaDAlzNZOFNs00HLIM,2842
|
31
37
|
pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
|
32
38
|
pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
33
|
-
pykubegrader/widgets_base/multi_select.py,sha256=
|
39
|
+
pykubegrader/widgets_base/multi_select.py,sha256=WhpS7a8V3BOuEfEyFPzcDhMbgr7p1a4FFh_mKU1HLbI,4226
|
34
40
|
pykubegrader/widgets_base/reading.py,sha256=ChUS3NOTa_HLtNpxR8hGX80LPKMvYMypnR6dFknfxus,5430
|
35
|
-
pykubegrader/widgets_base/select.py,sha256=
|
36
|
-
PyKubeGrader-0.3.
|
37
|
-
PyKubeGrader-0.3.
|
38
|
-
PyKubeGrader-0.3.
|
39
|
-
PyKubeGrader-0.3.
|
40
|
-
PyKubeGrader-0.3.
|
41
|
-
PyKubeGrader-0.3.
|
41
|
+
pykubegrader/widgets_base/select.py,sha256=qP31bjTWIn8LEgKwtNUJbgJnum6P7bx6A_At-u1ivFk,2750
|
42
|
+
PyKubeGrader-0.3.9.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
|
43
|
+
PyKubeGrader-0.3.9.dist-info/METADATA,sha256=fPAObIviDbz7vOFs5JCQm_IBTI1lT8W_l2TRIqhkPhM,2757
|
44
|
+
PyKubeGrader-0.3.9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
45
|
+
PyKubeGrader-0.3.9.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
|
46
|
+
PyKubeGrader-0.3.9.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
|
47
|
+
PyKubeGrader-0.3.9.dist-info/RECORD,,
|
@@ -1,11 +1,11 @@
|
|
1
1
|
import ast
|
2
|
+
import base64
|
2
3
|
import json
|
3
4
|
import re
|
4
5
|
import shutil
|
6
|
+
import sys
|
5
7
|
from dataclasses import dataclass
|
6
8
|
from pathlib import Path
|
7
|
-
from typing import Optional
|
8
|
-
import base64
|
9
9
|
from typing import Any, Optional
|
10
10
|
|
11
11
|
import nbformat
|
@@ -15,17 +15,17 @@ import nbformat
|
|
15
15
|
class FastAPINotebookBuilder:
|
16
16
|
notebook_path: str
|
17
17
|
temp_notebook: Optional[str] = None
|
18
|
-
assignment_tag:
|
19
|
-
require_key:
|
20
|
-
verbose:
|
18
|
+
assignment_tag: str = ""
|
19
|
+
require_key: bool = False
|
20
|
+
verbose: bool = False
|
21
21
|
|
22
22
|
def __post_init__(self) -> None:
|
23
23
|
self.root_path, self.filename = FastAPINotebookBuilder.get_filename_and_root(
|
24
24
|
self.notebook_path
|
25
25
|
)
|
26
|
-
self.total_points = 0
|
26
|
+
self.total_points = 0.0
|
27
27
|
|
28
|
-
self.max_question_points = {}
|
28
|
+
self.max_question_points: dict[str, float] = {}
|
29
29
|
self.run()
|
30
30
|
|
31
31
|
def run(self) -> None:
|
@@ -87,20 +87,24 @@ class FastAPINotebookBuilder:
|
|
87
87
|
for i, question in enumerate(self.max_question_points.keys()):
|
88
88
|
index, source = self.find_question_description(question)
|
89
89
|
try:
|
90
|
-
modified_source
|
90
|
+
modified_source = FastAPINotebookBuilder.add_text_after_double_hash(
|
91
|
+
source,
|
92
|
+
f"Question {i + 1} (Points: {self.max_question_points[question]}):",
|
93
|
+
)
|
91
94
|
self.replace_cell_source(index, modified_source)
|
92
|
-
except:
|
95
|
+
except Exception as e:
|
96
|
+
print(f"Error adding question description: {e}", file=sys.stderr)
|
93
97
|
pass
|
94
98
|
|
95
99
|
for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
|
96
|
-
if self.verbose:
|
100
|
+
if self.verbose:
|
97
101
|
print(
|
98
102
|
f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
|
99
103
|
)
|
100
104
|
|
101
105
|
cell = self.get_cell(cell_index)
|
102
106
|
cell_source = FastAPINotebookBuilder.add_import_statements_to_tests(
|
103
|
-
cell["source"], require_key=self.require_key,
|
107
|
+
cell["source"], require_key=self.require_key, assignment_tag = self.assignment_tag,
|
104
108
|
)
|
105
109
|
|
106
110
|
cell_source = FastAPINotebookBuilder.conceal_tests(cell_source)
|
@@ -146,41 +150,47 @@ class FastAPINotebookBuilder:
|
|
146
150
|
FastAPINotebookBuilder.construct_update_responses(cell_dict)
|
147
151
|
)
|
148
152
|
|
149
|
-
self.replace_cell_source(cell_index, updated_cell_source)
|
153
|
+
self.replace_cell_source(cell_index, updated_cell_source)
|
150
154
|
|
151
155
|
def find_question_description(self, search_string):
|
152
|
-
with open(self.temp_notebook,
|
156
|
+
with open(self.temp_notebook, "r", encoding="utf-8") as f:
|
153
157
|
nb_data = json.load(f)
|
154
158
|
|
155
159
|
found_raw = False
|
156
160
|
|
157
161
|
for idx, cell in enumerate(nb_data.get("cells", [])):
|
158
|
-
if
|
162
|
+
if (
|
163
|
+
cell["cell_type"] == "raw"
|
164
|
+
and any("# BEGIN QUESTION" in line for line in cell.get("source", []))
|
165
|
+
and any(search_string in line for line in cell.get("source", []))
|
166
|
+
):
|
159
167
|
found_raw = True
|
160
168
|
elif found_raw and cell["cell_type"] == "markdown":
|
161
|
-
return idx, cell.get(
|
169
|
+
return idx, cell.get(
|
170
|
+
"source", []
|
171
|
+
) # Return the index of the first matching markdown cell
|
162
172
|
|
163
|
-
return None, None # Return None if no such markdown cell is found
|
173
|
+
return None, None # Return None if no such markdown cell is found
|
164
174
|
|
165
175
|
def add_total_points_to_notebook(self) -> None:
|
166
176
|
self.max_question_points.keys()
|
167
177
|
|
168
178
|
def get_max_question_points(self, cell_dict) -> float:
|
169
179
|
return sum(
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
180
|
+
cell["points"]
|
181
|
+
for cell in self.assertion_tests_dict.values()
|
182
|
+
if cell["question"] == cell_dict["question"]
|
183
|
+
)
|
174
184
|
|
175
185
|
@staticmethod
|
176
186
|
def add_text_after_double_hash(markdown_source, insert_text):
|
177
187
|
"""
|
178
188
|
Adds insert_text immediately after the first '##' in the first line that starts with '##'.
|
179
|
-
|
189
|
+
|
180
190
|
Args:
|
181
191
|
- markdown_source (list of str): The list of lines in the markdown cell.
|
182
192
|
- insert_text (str): The text to be inserted.
|
183
|
-
|
193
|
+
|
184
194
|
Returns:
|
185
195
|
- list of str: The modified markdown cell content.
|
186
196
|
"""
|
@@ -189,7 +199,9 @@ class FastAPINotebookBuilder:
|
|
189
199
|
|
190
200
|
for line in markdown_source:
|
191
201
|
if not inserted and line.startswith("## "):
|
192
|
-
modified_source.append(
|
202
|
+
modified_source.append(
|
203
|
+
f"## {insert_text} {line[3:]}"
|
204
|
+
) # Insert text after '##'
|
193
205
|
inserted = True # Ensure it only happens once
|
194
206
|
else:
|
195
207
|
modified_source.append(line)
|
@@ -204,7 +216,9 @@ class FastAPINotebookBuilder:
|
|
204
216
|
max_question_points = self.get_max_question_points(cell_dict)
|
205
217
|
|
206
218
|
# store the max points for the question
|
207
|
-
self.max_question_points[f"{cell_dict[
|
219
|
+
self.max_question_points[f"{cell_dict['question']}"] = (
|
220
|
+
max_question_points
|
221
|
+
)
|
208
222
|
|
209
223
|
self.total_points += max_question_points
|
210
224
|
|
@@ -224,7 +238,7 @@ class FastAPINotebookBuilder:
|
|
224
238
|
|
225
239
|
if self.require_key:
|
226
240
|
first_cell_header.append(
|
227
|
-
"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
|
241
|
+
f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment='{self.assignment_tag}')\n"
|
228
242
|
)
|
229
243
|
|
230
244
|
short_filename = self.filename.split(".")[0].replace("_temp", "")
|
@@ -330,7 +344,9 @@ class FastAPINotebookBuilder:
|
|
330
344
|
return original_list[:index] + insert_list + original_list[index:]
|
331
345
|
|
332
346
|
@staticmethod
|
333
|
-
def add_import_statements_to_tests(
|
347
|
+
def add_import_statements_to_tests(
|
348
|
+
cell_source: list[str], require_key: bool = False
|
349
|
+
) -> list[str]:
|
334
350
|
"""
|
335
351
|
Adds the necessary import statements to the first cell of the notebook.
|
336
352
|
"""
|
@@ -353,7 +369,7 @@ class FastAPINotebookBuilder:
|
|
353
369
|
|
354
370
|
if require_key:
|
355
371
|
imports.append(
|
356
|
-
"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
|
372
|
+
f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment='{assignment_tag}')\n"
|
357
373
|
)
|
358
374
|
|
359
375
|
for i, line in enumerate(cell_source):
|
@@ -83,7 +83,7 @@ class NotebookProcessor:
|
|
83
83
|
self.require_key = assignment.get("require_key", False)
|
84
84
|
self.assignment_tag = assignment.get(
|
85
85
|
"assignment_tag",
|
86
|
-
f"week{assignment.get(
|
86
|
+
f"week{assignment.get('week')}-{self.assignment_type}",
|
87
87
|
)
|
88
88
|
else:
|
89
89
|
self.assignment_type = self.assignment_tag.split("-")[0].lower()
|
@@ -578,7 +578,7 @@ class NotebookProcessor:
|
|
578
578
|
|
579
579
|
if self.require_key:
|
580
580
|
# Add an additional line for validate_token()
|
581
|
-
validate_token_line =f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment = '{self.assignment_tag}')\n"
|
581
|
+
validate_token_line = f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment = '{self.assignment_tag}')\n"
|
582
582
|
|
583
583
|
# Define the Code cell
|
584
584
|
code_cell = nbformat.v4.new_code_cell(
|
@@ -681,7 +681,7 @@ class NotebookProcessor:
|
|
681
681
|
self.week,
|
682
682
|
self.assignment_type,
|
683
683
|
require_key=self.require_key,
|
684
|
-
assignment_tag
|
684
|
+
assignment_tag=self.assignment_tag,
|
685
685
|
)
|
686
686
|
|
687
687
|
NotebookProcessor.replace_temp_in_notebook(
|
@@ -712,7 +712,7 @@ class NotebookProcessor:
|
|
712
712
|
self.week,
|
713
713
|
self.assignment_type,
|
714
714
|
require_key=self.require_key,
|
715
|
-
assignment_tag
|
715
|
+
assignment_tag=self.assignment_tag,
|
716
716
|
)
|
717
717
|
NotebookProcessor.replace_temp_no_otter(
|
718
718
|
temp_notebook_path, temp_notebook_path
|
@@ -744,7 +744,9 @@ class NotebookProcessor:
|
|
744
744
|
nbformat.write(notebook, f)
|
745
745
|
|
746
746
|
@staticmethod
|
747
|
-
def add_validate_token_cell(
|
747
|
+
def add_validate_token_cell(
|
748
|
+
notebook_path: str, require_key: bool, **kwargs
|
749
|
+
) -> None:
|
748
750
|
"""
|
749
751
|
Adds a new code cell at the top of a Jupyter notebook if require_key is True.
|
750
752
|
|
@@ -758,8 +760,12 @@ class NotebookProcessor:
|
|
758
760
|
if not require_key:
|
759
761
|
print("require_key is False. No changes made to the notebook.")
|
760
762
|
return
|
761
|
-
|
762
|
-
NotebookProcessor.add_validate_block(
|
763
|
+
|
764
|
+
NotebookProcessor.add_validate_block(
|
765
|
+
notebook_path,
|
766
|
+
require_key,
|
767
|
+
assignment_tag=kwargs.get("assignment_tag", None),
|
768
|
+
)
|
763
769
|
|
764
770
|
# Load the notebook
|
765
771
|
with open(notebook_path, "r", encoding="utf-8") as f:
|
@@ -768,13 +774,13 @@ class NotebookProcessor:
|
|
768
774
|
# Create the new code cell
|
769
775
|
if kwargs.get("assignment_tag", None):
|
770
776
|
new_cell = nbformat.v4.new_code_cell(
|
771
|
-
|
772
|
-
|
777
|
+
"from pykubegrader.tokens.validate_token import validate_token\n"
|
778
|
+
f"validate_token('type the key provided by your instructor here', assignment = '{kwargs.get('assignment_tag')}')\n"
|
773
779
|
)
|
774
780
|
else:
|
775
781
|
new_cell = nbformat.v4.new_code_cell(
|
776
|
-
|
777
|
-
|
782
|
+
"from pykubegrader.tokens.validate_token import validate_token\n"
|
783
|
+
"validate_token('type the key provided by your instructor here')\n"
|
778
784
|
)
|
779
785
|
|
780
786
|
# Add the new cell to the top of the notebook
|
@@ -785,7 +791,7 @@ class NotebookProcessor:
|
|
785
791
|
nbformat.write(notebook, f)
|
786
792
|
|
787
793
|
@staticmethod
|
788
|
-
def add_validate_block(notebook_path: str, require_key: bool, **kwargs) -> None:
|
794
|
+
def add_validate_block(notebook_path: str, require_key: bool, assignment_tag = None, **kwargs) -> None:
|
789
795
|
"""
|
790
796
|
Modifies the first code cell of a Jupyter notebook to add the validate_token call if require_key is True.
|
791
797
|
|
@@ -804,7 +810,7 @@ class NotebookProcessor:
|
|
804
810
|
notebook = nbformat.read(f, as_version=4)
|
805
811
|
|
806
812
|
# Prepare the validation code
|
807
|
-
validation_code = "validate_token()\n"
|
813
|
+
validation_code = f"validate_token(assignment = '{assignment_tag}')\n"
|
808
814
|
|
809
815
|
# Modify the first cell if it's a code cell, otherwise insert a new one
|
810
816
|
if notebook.cells and notebook.cells[0].cell_type == "code":
|
@@ -819,7 +825,11 @@ class NotebookProcessor:
|
|
819
825
|
|
820
826
|
@staticmethod
|
821
827
|
def add_initialization_code(
|
822
|
-
notebook_path,
|
828
|
+
notebook_path,
|
829
|
+
week,
|
830
|
+
assignment_type,
|
831
|
+
require_key=False,
|
832
|
+
**kwargs,
|
823
833
|
):
|
824
834
|
# finds the first code cell
|
825
835
|
index, cell = find_first_code_cell(notebook_path)
|
@@ -831,7 +841,11 @@ class NotebookProcessor:
|
|
831
841
|
replace_cell_source(notebook_path, index, cell)
|
832
842
|
|
833
843
|
if require_key:
|
834
|
-
NotebookProcessor.add_validate_token_cell(
|
844
|
+
NotebookProcessor.add_validate_token_cell(
|
845
|
+
notebook_path,
|
846
|
+
require_key,
|
847
|
+
assignment_tag=kwargs.get("assignment_tag", None),
|
848
|
+
)
|
835
849
|
|
836
850
|
def multiple_choice_parser(self, temp_notebook_path, new_notebook_path):
|
837
851
|
### Parse the notebook for multiple choice questions
|
@@ -0,0 +1,184 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from dateutil import parser
|
3
|
+
from datetime import datetime
|
4
|
+
from pykubegrader.graders.late_assignments import calculate_late_submission
|
5
|
+
|
6
|
+
|
7
|
+
class assignment_type:
|
8
|
+
"""
|
9
|
+
Base class for assignment types.
|
10
|
+
|
11
|
+
Attributes:
|
12
|
+
weight (float): The weight of the assignment in the overall grade.
|
13
|
+
|
14
|
+
Methods:
|
15
|
+
__init__(name: str, weekly: bool, weight: float):
|
16
|
+
Initializes an instance of the assignment_type class.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, name: str, weekly: bool, weight: float):
|
20
|
+
"""Initializes an instance of the assignment_type class.
|
21
|
+
Args:
|
22
|
+
name (str): The name of the assignment.
|
23
|
+
weekly (bool): Indicates if the assignment is weekly.
|
24
|
+
weight (float): The weight of the assignment in the overall grade."""
|
25
|
+
self.name = name
|
26
|
+
self.weekly = weekly
|
27
|
+
self.weight = weight
|
28
|
+
|
29
|
+
|
30
|
+
class Assignment(assignment_type):
|
31
|
+
"""
|
32
|
+
Class for storing and updating assignment scores.
|
33
|
+
|
34
|
+
Attributes:
|
35
|
+
week (int, optional): The week number of the assignment.
|
36
|
+
exempted (bool): Indicates if the assignment is exempted.
|
37
|
+
graded (bool): Indicates if the assignment has been graded.
|
38
|
+
late_adjustment (bool): Indicates if late submissions are allowed.
|
39
|
+
students_exempted (list): List of student IDs exempted from the assignment.
|
40
|
+
due_date (datetime, optional): The due date of the assignment.
|
41
|
+
max_score (float, optional): The maximum score possible for the assignment.
|
42
|
+
grade_adjustment_func (callable, optional): Function to adjust the grade for late or exempted submissions.
|
43
|
+
|
44
|
+
Methods:
|
45
|
+
add_exempted_students(students):
|
46
|
+
Add students to the exempted list.
|
47
|
+
|
48
|
+
update_score(submission=None):
|
49
|
+
Update the score of the assignment based on the submission.
|
50
|
+
|
51
|
+
grade_adjustment(submission):
|
52
|
+
Apply the adjustment function if provided.
|
53
|
+
"""
|
54
|
+
|
55
|
+
def __init__(
|
56
|
+
self,
|
57
|
+
name: str,
|
58
|
+
weekly: bool,
|
59
|
+
weight: float,
|
60
|
+
score: float,
|
61
|
+
grade_adjustment_func=None,
|
62
|
+
**kwargs,
|
63
|
+
):
|
64
|
+
"""
|
65
|
+
Initializes an instance of the Assignment class.
|
66
|
+
|
67
|
+
weekly (bool): Indicates if the assignment is weekly.
|
68
|
+
grade_adjustment_func (callable, optional): Used to calculate the grade in the case of late or exempted submissions. Defaults to None.
|
69
|
+
**kwargs: Additional keyword arguments.
|
70
|
+
week (int, optional): The week number of the assignment. Defaults to None.
|
71
|
+
exempted (bool, optional): Indicates if the assignment is exempted. Defaults to False.
|
72
|
+
graded (bool, optional): Indicates if the assignment is graded. Defaults to False.
|
73
|
+
late_adjustment (bool, optional): Indicates if late adjustment is applied. Defaults to True.
|
74
|
+
students_exempted (list, optional): List of students exempted from the assignment. Defaults to an empty list.
|
75
|
+
due_date (datetime, optional): The due date of the assignment. Defaults to None.
|
76
|
+
max_score (float, optional): The maximum score possible for the assignment. Defaults to None.
|
77
|
+
"""
|
78
|
+
super().__init__(name, weekly, weight)
|
79
|
+
self.score = score
|
80
|
+
self._score = score
|
81
|
+
self.week = kwargs.get("week", None)
|
82
|
+
self.exempted = kwargs.get("exempted", False)
|
83
|
+
self.graded = kwargs.get("graded", False)
|
84
|
+
self.late_adjustment = kwargs.get("late_adjustment", True)
|
85
|
+
self.students_exempted = kwargs.get("students_exempted", [])
|
86
|
+
self.due_date = kwargs.get("due_date", None)
|
87
|
+
self.max_score = kwargs.get("max_score", None)
|
88
|
+
|
89
|
+
# Store the function for later use
|
90
|
+
self.grade_adjustment_func = grade_adjustment_func
|
91
|
+
|
92
|
+
def add_exempted_students(self, students):
|
93
|
+
"""
|
94
|
+
Add students to the exempted list.
|
95
|
+
Args:
|
96
|
+
students (list): List of student IDs to exempt from the assignment.
|
97
|
+
"""
|
98
|
+
self.students_exempted.extend(students)
|
99
|
+
|
100
|
+
def update_score(self, submission=None):
|
101
|
+
"""Updates the assignment score based on the given submission.
|
102
|
+
|
103
|
+
This method adjusts the score using the `grade_adjustment` function if a submission
|
104
|
+
is provided. If the submission results in a higher score than the current score,
|
105
|
+
the assignment score is updated. If no submission is provided and the student is
|
106
|
+
not exempted, the score is set to zero. If the student is exempted, the score
|
107
|
+
is set to NaN.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
submission (dict, optional): The submission data, expected to contain relevant
|
111
|
+
details for grading. Defaults to None.
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
float: The updated assignment score. If exempted, returns NaN. If no submission
|
115
|
+
is provided, returns 0.
|
116
|
+
"""
|
117
|
+
if self.exempted:
|
118
|
+
self.score = np.nan
|
119
|
+
|
120
|
+
# Saves a table with the score of the exempted assignment still recorded.
|
121
|
+
try:
|
122
|
+
# Adjust the score based on submission
|
123
|
+
score_ = self.grade_adjustment(submission)
|
124
|
+
if score_ > self._score:
|
125
|
+
self._score = score_
|
126
|
+
except:
|
127
|
+
pass
|
128
|
+
return self.score
|
129
|
+
elif submission is not None:
|
130
|
+
# Adjust the score based on submission
|
131
|
+
score_ = self.grade_adjustment(submission)
|
132
|
+
|
133
|
+
# Update the score only if the new score is higher
|
134
|
+
if score_ > self.score:
|
135
|
+
self.score = score_
|
136
|
+
self._score = score_
|
137
|
+
|
138
|
+
return self.score
|
139
|
+
else:
|
140
|
+
# Set the score to zero if not exempted and no submission
|
141
|
+
self.score = 0
|
142
|
+
self._score = 0
|
143
|
+
return self.score
|
144
|
+
|
145
|
+
def grade_adjustment(self, submission):
|
146
|
+
"""Applies adjustments to the submission score based on grading policies.
|
147
|
+
|
148
|
+
This method applies any provided grade adjustment function to the raw score.
|
149
|
+
If no custom function is given, it determines the final score by considering
|
150
|
+
lateness penalties based on the submission timestamp and due date.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
submission (dict): A dictionary containing:
|
154
|
+
- `"raw_score"` (float): The initial unadjusted score.
|
155
|
+
- `"timestamp"` (str): The submission timestamp in a parsable format.
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
float: The adjusted score, incorporating lateness penalties if applicable.
|
159
|
+
Returns 0 for late submissions if no late adjustment policy is defined.
|
160
|
+
"""
|
161
|
+
score = submission["raw_score"]
|
162
|
+
entry_date = parser.parse(submission["timestamp"])
|
163
|
+
|
164
|
+
if self.grade_adjustment_func:
|
165
|
+
return self.grade_adjustment_func(score)
|
166
|
+
else:
|
167
|
+
if self.late_adjustment:
|
168
|
+
# Convert due date to datetime object
|
169
|
+
due_date = datetime.fromisoformat(self.due_date.replace("Z", "+00:00"))
|
170
|
+
|
171
|
+
late_modifier = calculate_late_submission(
|
172
|
+
due_date.strftime("%Y-%m-%d %H:%M:%S"),
|
173
|
+
entry_date.strftime("%Y-%m-%d %H:%M:%S"),
|
174
|
+
)
|
175
|
+
|
176
|
+
# Apply late modifier and normalize score
|
177
|
+
return (score / self.max_score) * late_modifier
|
178
|
+
else:
|
179
|
+
# Return normalized score if on time
|
180
|
+
if entry_date < self.due_date:
|
181
|
+
return score / self.max_score
|
182
|
+
# Assign zero score for late submissions without a late adjustment policy
|
183
|
+
else:
|
184
|
+
return 0
|