PyKubeGrader 0.3.0__py3-none-any.whl → 0.3.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyKubeGrader
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -1,13 +1,15 @@
1
1
  pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
+ pykubegrader/grading_tester.ipynb,sha256=2c1JgnV-zgP405Q3rlEkOlMJPm_9Qga0Z0MVWpFk5No,18531
2
3
  pykubegrader/initialize.py,sha256=Bwu1q18l18FB9lGppvt-L41D5gzr3S8t6zC0_UbrASw,3994
3
4
  pykubegrader/telemetry.py,sha256=50Qp5WXeF7PD5FxDLFXWFAnQ2Yobj-wL3Dxh0Hz_vh0,6552
4
- pykubegrader/utils.py,sha256=FrxuZ3gtTBTm5FQeH5c0bF9kFjA_AVtE5AYeFhzwKZ0,827
5
+ pykubegrader/utils.py,sha256=jlJklKvRhY3O7Hz2aaU1m0y3p_n9eMAXNnAF7LUEaPY,1275
5
6
  pykubegrader/validate.py,sha256=OKnItGyd-L8QPKcsE0KRuwBI_IxKiJzMLJKZiA2j3II,11184
6
7
  pykubegrader/build/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
8
  pykubegrader/build/api_notebook_builder.py,sha256=dlcVrGgsvxnt6GlAUN3e-FrpsPNJKXSHni1fstRCBik,20311
8
- pykubegrader/build/build_folder.py,sha256=WTdPFsV1PRJ9U_730ckcWezQfMEL2oCH4ZDoflmr30M,83138
9
+ pykubegrader/build/build_folder.py,sha256=Asc-VdhXgxQfOfFIWJShhXrF2EITJOIZQ5Dz_2y-P2I,85358
9
10
  pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
10
11
  pykubegrader/build/markdown_questions.py,sha256=cSh8mkHK3hh-etJdgrZu9UQi1WPrKQtofkzLCUp1Z-w,4676
12
+ pykubegrader/grade_reports/grade_reports.py,sha256=TmT_F2enezU_BIFPBQ0oZ4l5nBgPw4PSBo-8wJ_gt4w,5915
11
13
  pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
12
14
  pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
13
15
  pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
@@ -17,21 +19,21 @@ pykubegrader/submit/submit_assignment.py,sha256=UgJXKWw5b8-bRSFnba4iHAyXnujULHcW
17
19
  pykubegrader/tokens/tokens.py,sha256=X9f3SzrGCrAJp_BXhr6VJn5f0LxtgQ7HLPBw7zEF2BY,1198
18
20
  pykubegrader/tokens/validate_token.py,sha256=MQtgz_USvSZ9JahJ48ybjp74F5aYz64lhtvuwVc4kQw,2712
19
21
  pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
20
- pykubegrader/widgets/multiple_choice.py,sha256=CE7y6DPIhm4UuD8I1nwWPF2l9sKtKvYhbyPpeZ1qmQc,2686
21
- pykubegrader/widgets/question_processor.py,sha256=59R9oBiemuVJP0qzsR1kY8MeqDq4Kh99AYRq9RGujsg,1223
22
+ pykubegrader/widgets/multiple_choice.py,sha256=ag6W-HN7isHkIUmB4BxtK8T1JhuV3FBLUBAhcV6rN80,2729
23
+ pykubegrader/widgets/question_processor.py,sha256=fFH2ffMPYAJHsDn1RweEBnibfoZlSvTANUxYT3EPb5w,1375
22
24
  pykubegrader/widgets/reading_question.py,sha256=y30_swHwzH8LrT8deWTnxctAAmR8BSxTlXAqMgUrAT4,3031
23
- pykubegrader/widgets/select_many.py,sha256=l7YQ8QT5k71j36KC1f5LmKIAX2bXpvMDGc6nqIJ1PeQ,4116
25
+ pykubegrader/widgets/select_many.py,sha256=7Bq1Je1Lx8BY3c_lVZixN-Ijw67IWO6EX3qWtcJ2NmM,4625
24
26
  pykubegrader/widgets/student_info.py,sha256=xhQgKehk1r5e6N_hnjAIovLdPvQju6ZqQTOiPG0aevg,3568
25
27
  pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs,1656
26
28
  pykubegrader/widgets/true_false.py,sha256=QllIhHuJstJft_RuShkxI_fFFTaDAlzNZOFNs00HLIM,2842
27
29
  pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
28
30
  pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
29
- pykubegrader/widgets_base/multi_select.py,sha256=Btr09qjl2g2-wtKEtI3RYwo1Xm1dfFnHnDzw_1Yfqf4,4148
30
- pykubegrader/widgets_base/reading.py,sha256=xmvN1UIXwk32v9S-JhsXwDc7axPlgpvoxSeM3II8sxY,5393
31
- pykubegrader/widgets_base/select.py,sha256=RgSieRtBapAwXLd_ByJtT1L1EeUSi-9pFTuIm7zwDVE,2649
32
- PyKubeGrader-0.3.0.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
33
- PyKubeGrader-0.3.0.dist-info/METADATA,sha256=W0PXYFxnn7vuAKI6_E7gMHfYnoftvYbYNorwwJwSfP0,2806
34
- PyKubeGrader-0.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
35
- PyKubeGrader-0.3.0.dist-info/entry_points.txt,sha256=BbLXpFZObpOXA8e3p3GcFkL-sHdUnDLUcnYmc6zx3NI,201
36
- PyKubeGrader-0.3.0.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
37
- PyKubeGrader-0.3.0.dist-info/RECORD,,
31
+ pykubegrader/widgets_base/multi_select.py,sha256=JgjhHQJL8Pf0-1T_wdZCecAK1IgVJrZBCbR6b3jvDtk,4181
32
+ pykubegrader/widgets_base/reading.py,sha256=ChUS3NOTa_HLtNpxR8hGX80LPKMvYMypnR6dFknfxus,5430
33
+ pykubegrader/widgets_base/select.py,sha256=tEDg7GEjsZnz1646YTthTeamujVRS5jDJWMsXhmOQbI,2705
34
+ PyKubeGrader-0.3.2.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
35
+ PyKubeGrader-0.3.2.dist-info/METADATA,sha256=le0n8bDcxtGgZ-XG4-2YXPVpq6nDgai_EKdeRdcDLcg,2806
36
+ PyKubeGrader-0.3.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
37
+ PyKubeGrader-0.3.2.dist-info/entry_points.txt,sha256=BbLXpFZObpOXA8e3p3GcFkL-sHdUnDLUcnYmc6zx3NI,201
38
+ PyKubeGrader-0.3.2.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
39
+ PyKubeGrader-0.3.2.dist-info/RECORD,,
@@ -616,6 +616,21 @@ class NotebookProcessor:
616
616
  shutil.copy("./keys/.client_private_key.bin", client_private_key)
617
617
  shutil.copy("./keys/.server_public_key.bin", server_public_key)
618
618
 
619
+ # Extract the assignment config
620
+ config = extract_config_from_notebook(temp_notebook_path)
621
+
622
+ files = extract_files(config)
623
+
624
+ # print(f"Files: {files}, from {temp_notebook_path}")
625
+
626
+ if files:
627
+ for file in files:
628
+ print(f"Copying {file} to {os.path.join(notebook_subfolder, file)}")
629
+ shutil.copy(
630
+ os.path.join(self.root_folder, file),
631
+ os.path.join(notebook_subfolder, file),
632
+ )
633
+
619
634
  client_private_key = os.path.join(
620
635
  notebook_subfolder,
621
636
  ".client_private_key.bin",
@@ -1352,15 +1367,18 @@ def extract_SELECT_MANY(ipynb_file):
1352
1367
  1 # Increment subquestion number for each question
1353
1368
  )
1354
1369
 
1355
- # Extract question text (### heading)
1356
- question_text_match = re.search(
1357
- r"^###\s*\*\*(.+)\*\*", markdown_content, re.MULTILINE
1358
- )
1359
- question_text = (
1360
- question_text_match.group(1).strip()
1361
- if question_text_match
1362
- else None
1363
- )
1370
+ # # Extract question text (### heading)
1371
+ # question_text_match = re.search(
1372
+ # r"^###\s*\*\*(.+)\*\*", markdown_content, re.MULTILINE
1373
+ # )
1374
+ # question_text = (
1375
+ # question_text_match.group(1).strip()
1376
+ # if question_text_match
1377
+ # else None
1378
+ # )
1379
+
1380
+ # Extract question text enable multiple lines
1381
+ question_text = extract_question(markdown_content)
1364
1382
 
1365
1383
  # Extract OPTIONS (lines after #### options)
1366
1384
  options_match = re.search(
@@ -2126,6 +2144,57 @@ def update_initialize_assignment(
2126
2144
  print(f"No matching lines found in '{notebook_path}'.")
2127
2145
 
2128
2146
 
2147
+ def extract_config_from_notebook(notebook_path):
2148
+ """
2149
+ Extract configuration text from a Jupyter Notebook.
2150
+
2151
+ Parameters:
2152
+ notebook_path (str): Path to the Jupyter Notebook file.
2153
+
2154
+ Returns:
2155
+ str: The configuration text if found, otherwise an empty string.
2156
+ """
2157
+ with open(notebook_path, "r", encoding="utf-8") as f:
2158
+ notebook_data = json.load(f)
2159
+
2160
+ # Iterate through cells to find the configuration text
2161
+ config_text = ""
2162
+ for cell in notebook_data.get("cells", []):
2163
+ if cell.get("cell_type") == "raw": # Check for code cells
2164
+ source = "".join(cell.get("source", []))
2165
+ if "# ASSIGNMENT CONFIG" in source:
2166
+ config_text = source
2167
+ break
2168
+
2169
+ return config_text
2170
+
2171
+
2172
+ def extract_files(config_text):
2173
+ """
2174
+ Extract the list of files from the given configuration text, excluding .bin files.
2175
+
2176
+ Parameters:
2177
+ config_text (str): The configuration text to process.
2178
+
2179
+ Returns:
2180
+ list: A list of file names excluding .bin files.
2181
+ """
2182
+ # Regular expression to extract files list
2183
+ file_pattern = re.search(r"files:\s*\[(.*?)\]", config_text, re.DOTALL)
2184
+
2185
+ if file_pattern:
2186
+ files = file_pattern.group(1)
2187
+ # Split the list into individual file names and exclude .bin files
2188
+ file_list = [
2189
+ file.strip()
2190
+ for file in files.split(",")
2191
+ if not file.strip().endswith(".bin")
2192
+ ]
2193
+ return file_list
2194
+ else:
2195
+ return []
2196
+
2197
+
2129
2198
  def main():
2130
2199
  parser = argparse.ArgumentParser(
2131
2200
  description="Recursively process Jupyter notebooks with '# ASSIGNMENT CONFIG', move them to a solutions folder, and run otter assign."
@@ -0,0 +1,171 @@
1
+ import pandas as pd
2
+ import requests
3
+ from requests.auth import HTTPBasicAuth
4
+
5
+ from ..build.passwords import password, user
6
+
7
+
8
+ def format_assignment_table(assignments):
9
+ # Create DataFrame
10
+ df = pd.DataFrame(assignments)
11
+
12
+ # Replacements for normalization
13
+ replacements = {
14
+ "practicequiz": "practice quiz",
15
+ "practice-quiz": "practice quiz",
16
+ "attend": "attendance",
17
+ "attendance": "attendance",
18
+ }
19
+
20
+ # Remove assignments of type 'test'
21
+ remove_assignments = ["test"]
22
+
23
+ # Apply replacements
24
+ df["assignment_name"] = df["assignment_type"].replace(replacements)
25
+
26
+ # Filter out specific assignment types
27
+ df = df[~df["assignment_type"].isin(remove_assignments)]
28
+
29
+ # Sort by week number and assignment name
30
+ df = df.sort_values(by=["assignment_name", "week_number"]).reset_index(drop=True)
31
+
32
+ return df
33
+
34
+
35
+ def get_student_grades(
36
+ student_username, api_base_url="https://engr-131-api.eastus.cloudapp.azure.com/"
37
+ ):
38
+ params = {"username": student_username}
39
+ res = requests.get(
40
+ url=api_base_url.rstrip("/") + "/student-grades-testing",
41
+ params=params,
42
+ auth=HTTPBasicAuth(user(), password()),
43
+ )
44
+
45
+ [assignments, sub] = res.json()
46
+
47
+ assignments_df = format_assignment_table(assignments)
48
+
49
+ return assignments_df, pd.DataFrame(sub)
50
+
51
+
52
+ def filter_assignments(df, max_week=None, exclude_types=None):
53
+ """
54
+ Remove assignments with week_number greater than max_week
55
+ or with specific assignment types.
56
+
57
+ :param df: DataFrame containing assignments.
58
+ :param max_week: Maximum allowed week_number (int).
59
+ :param exclude_types: A single assignment type or a list of assignment types to exclude.
60
+ :return: Filtered DataFrame.
61
+ """
62
+ if max_week is not None:
63
+ df = df[df["week_number"] <= max_week]
64
+
65
+ if exclude_types is not None:
66
+ # Ensure exclude_types is a list
67
+ if not isinstance(exclude_types, (list, tuple, set)):
68
+ exclude_types = [exclude_types]
69
+ df = df[~df["assignment_type"].isin(exclude_types)]
70
+
71
+ return df
72
+
73
+
74
+ # import os
75
+ # import numpy as np
76
+ # import pandas as pd
77
+ # import socket
78
+ # import requests
79
+ # from IPython.core.interactiveshell import ExecutionInfo
80
+ # from requests import Response
81
+ # from requests.auth import HTTPBasicAuth
82
+ # from requests.exceptions import RequestException
83
+ # from pykubegrader.graders.late_assignments import calculate_late_submission
84
+
85
+
86
+ # api_base_url = os.getenv("DB_URL")
87
+ # student_user = "admin" # os.getenv("user_name_student")
88
+ # student_pw = "TrgpUuadm2PWtdgtC7Yt" # os.getenv("keys_student")
89
+
90
+ # from_hostname = socket.gethostname().removeprefix("jupyter-")
91
+ # from_env = os.getenv("JUPYTERHUB_USER")
92
+ # params = {"username": from_env}
93
+
94
+ # letteronly = lambda s: re.sub(r'[^a-zA-Z]', '', s)
95
+ # start_date='2025-01-06'
96
+
97
+ # # get assignment information
98
+ # res = requests.get(
99
+ # url=api_base_url.rstrip("/") + "/assignments",
100
+ # auth=HTTPBasicAuth(student_user, student_pw),)
101
+ # res.raise_for_status()
102
+ # assignments = res.json()
103
+
104
+ # # get submission information
105
+ # res = requests.get(
106
+ # url=api_base_url.rstrip("/") + "/testing/get-all-assignment-subs",
107
+ # auth=HTTPBasicAuth('testing', 'Vok8WzmuCMVYULw3tqzJ'),
108
+ # )
109
+ # subs = res.json()
110
+ # student_subs = [sub for sub in subs if sub['student_email']==from_env]
111
+
112
+ # # set up new df format
113
+ # weights = {'homework':0.15, 'lab':0.15, 'lecture':0.15, 'quiz':0.15, 'readings':0.15,
114
+ # # 'midterm':0.15, 'final':0.2
115
+ # 'labattendance':0.05, 'practicequiz':0.05, }
116
+ # assignment_types = list(set([a['assignment_type'] for a in assignments]))+['Running Avg']
117
+ # inds = [f'week{i+1}' for i in range(11)]+['Running Avg']
118
+ # restruct_grades = {k: np.zeros(len(inds)) for k in assignment_types}
119
+ # restruct_grades['inds']=inds
120
+ # new_weekly_grades = pd.DataFrame(restruct_grades)
121
+
122
+ # for assignment in assignments:
123
+ # # get the assignment from all submissions
124
+ # subs = [ sub for sub in student_subs if \
125
+ # letteronly(sub['assignment_type'])==letteronly(assignment['assignment_type']) and \
126
+ # sub['week_number']==assignment['week_number'] ]
127
+ # if len(subs)==0: continue
128
+ # if len(subs)>1:
129
+
130
+ # # get due date from assignment
131
+ # due_date = datetime.datetime.strptime(assignment['due_date'], "%Y-%m-%d %H:%M:%S")
132
+ # for sub in subs:
133
+ # entry_date = datetime.strptime(sub['timestamp'], '%Y-%m-%dT%H:%M:%SZ')
134
+ # if entry_date <= due_date:
135
+ # else after_due).append(entry)
136
+ # calculate_late_submission(due = due_date, # '2025-01-21T18:59:59Z'.
137
+ # submitted = subs"%Y-%m-%d %H:%M:%S".
138
+ # - Q0 (float): Initial value (default is 100).
139
+ # - Q_min (float): Minimum value (default is 40).
140
+ # - k (float): Decay constant per minute (default is 6.88e-5).
141
+
142
+ # # get max from before due date
143
+
144
+ # # get max score from after due date and calculate
145
+ # print(sub['assignment'])
146
+ # print(subs)
147
+ # return
148
+ # # fill out grades
149
+ # new_weekly_grades.set_index('inds',inplace=True)
150
+ # splitted = [col_name.split('-')+[grades[col_name][0]] for col_name in grades.columns]
151
+ # for week,assignment,grade in splitted: new_weekly_grades.loc[week,assignment] = grade
152
+
153
+ # # Calculate the current week (1-based indexing)
154
+ # start_date = datetime.strptime(start_date, "%Y-%m-%d")
155
+ # today = datetime.now()
156
+ # days_since_start = (today - start_date).days
157
+ # current_week = days_since_start // 7 + 1
158
+
159
+ # # Get average until current week
160
+ # new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[:current_week-1].mean()
161
+
162
+ # # make new dataframe with the midterm, final, and running average
163
+ # max_key_length = max(len(k) for k in weights.keys())
164
+ # total = 0
165
+ # for k, v in weights.items():
166
+ # grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
167
+ # total+=grade*v
168
+ # print(f'{k:<{max_key_length}}:\t {grade:.2f}')
169
+ # print(f'\nTotal: {total}') # exclude midterm and final
170
+
171
+ # return new_out
@@ -0,0 +1,477 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "from datetime import datetime\n",
10
+ "\n",
11
+ "import pandas as pd\n",
12
+ "import requests\n",
13
+ "from build.passwords import password, user\n",
14
+ "from grade_reports.grade_reports import filter_assignments, get_student_grades\n",
15
+ "from requests.auth import HTTPBasicAuth\n",
16
+ "\n",
17
+ "from pykubegrader.graders.late_assignments import calculate_late_submission\n",
18
+ "\n",
19
+ "api_base_url = \"https://engr-131-api.eastus.cloudapp.azure.com/\"\n",
20
+ "\n",
21
+ "\n",
22
+ "def get_all_students():\n",
23
+ " res = requests.get(\n",
24
+ " url=api_base_url.rstrip(\"/\") + \"/get-all-submission-emails\",\n",
25
+ " auth=HTTPBasicAuth(user(), password()),\n",
26
+ " )\n",
27
+ "\n",
28
+ " return res.json()\n",
29
+ "\n",
30
+ "\n",
31
+ "# def get_student_grades(student_id):\n",
32
+ "# params = {\"username\": student_id}\n",
33
+ "# res = requests.get(\n",
34
+ "# url=api_base_url.rstrip(\"/\") + \"/student-grades-testing\",\n",
35
+ "# params=params,\n",
36
+ "# auth=HTTPBasicAuth(user(), password()),\n",
37
+ "# )\n",
38
+ "\n",
39
+ "# [assignments, sub] = res.json()\n",
40
+ "\n",
41
+ "# return assignments, sub\n",
42
+ "\n",
43
+ "\n",
44
+ "def get_student(student):\n",
45
+ " print(student)\n",
46
+ " # Get assignments and submissions for the student (assumed functions)\n",
47
+ " assignments, submissions = get_student_grades(student)\n",
48
+ "\n",
49
+ " # Recalculate grades and get a grades dictionary\n",
50
+ " grades_dict = recalculate_best_grades(assignments, submissions)\n",
51
+ "\n",
52
+ " # Calculate averages and build a row for the student\n",
53
+ " row = calculate_averages(grades_dict, student)\n",
54
+ "\n",
55
+ " # Convert the row (a dictionary) into a DataFrame\n",
56
+ " # row_df = pd.DataFrame([row])\n",
57
+ "\n",
58
+ " return row\n",
59
+ "\n",
60
+ "\n",
61
+ "def get_all_student_grades():\n",
62
+ " # Initialize an empty DataFrame to hold all student grades\n",
63
+ " df = pd.DataFrame()\n",
64
+ "\n",
65
+ " # Get all students (assuming get_all_students() is a defined function)\n",
66
+ " students = get_all_students()\n",
67
+ "\n",
68
+ " for student in students:\n",
69
+ " row_df = get_student(student)\n",
70
+ "\n",
71
+ " # Append the row to the DataFrame\n",
72
+ " df = pd.concat([df, row_df], ignore_index=True)\n",
73
+ "\n",
74
+ " return df\n",
75
+ "\n",
76
+ "\n",
77
+ "def get_max_deadline(assignments, assignment_name, week_number):\n",
78
+ " matching_rows = assignments[\n",
79
+ " (assignments[\"week_number\"] == week_number)\n",
80
+ " & (assignments[\"assignment_name\"] == assignment_name)\n",
81
+ " ]\n",
82
+ "\n",
83
+ " max_timestamp = matching_rows[\"due_date\"].max()\n",
84
+ " return max_timestamp\n",
85
+ "\n",
86
+ "\n",
87
+ "def calculate_averages(grades_dict, student_id):\n",
88
+ " # Create a DataFrame from the dictionary\n",
89
+ " df = pd.DataFrame(\n",
90
+ " grades_dict\n",
91
+ " ).T # Transpose the dictionary to make assignments rows\n",
92
+ " df[\"score\"] = df[\"score\"].astype(float) # Ensure the scores are floats\n",
93
+ "\n",
94
+ " # Create a dictionary for the row data\n",
95
+ " row_data = {\"student_id\": student_id}\n",
96
+ "\n",
97
+ " # Add scores for each assignment as separate columns\n",
98
+ " for assignment, details in grades_dict.items():\n",
99
+ " column_name = (\n",
100
+ " f\"{details['assignment_name']}_week_{details['week_number']}\".replace(\n",
101
+ " \"_\", \" \"\n",
102
+ " )\n",
103
+ " )\n",
104
+ " row_data[column_name] = float(details[\"score\"])\n",
105
+ "\n",
106
+ " # Calculate and add average scores for each assignment category\n",
107
+ " averages = df.groupby(\"assignment_name\")[\"score\"].mean()\n",
108
+ " for assignment_name, avg_score in averages.items():\n",
109
+ " row_data[f\"{assignment_name}_average\".replace(\"_\", \" \")] = avg_score\n",
110
+ "\n",
111
+ " # # Course Percentage Performance\n",
112
+ " row_data[\"Current Course Percentage\"] = (\n",
113
+ " (row_data[\"quiz average\"] + row_data[\"practice quiz average\"] * 0.1) * 15\n",
114
+ " + (row_data[\"homework average\"]) * 15\n",
115
+ " + row_data[\"lab average\"] * 15\n",
116
+ " + row_data[\"lecture average\"] * 15\n",
117
+ " + row_data[\"readings average\"] * 15\n",
118
+ " + row_data[\"attendance average\"] * 5\n",
119
+ " ) / (15 + 15 + 15 + 15 + 15 + 5)\n",
120
+ "\n",
121
+ " # Create a single-row DataFrame\n",
122
+ " single_row_df = pd.DataFrame([row_data])\n",
123
+ " return single_row_df\n",
124
+ "\n",
125
+ "\n",
126
+ "def recalculate_best_grades(\n",
127
+ " assignments,\n",
128
+ " submissions,\n",
129
+ " custom_order=[\n",
130
+ " \"lecture\",\n",
131
+ " \"homework\",\n",
132
+ " \"lab\",\n",
133
+ " \"attendance\",\n",
134
+ " \"readings\",\n",
135
+ " \"quiz\",\n",
136
+ " \"practice quiz\",\n",
137
+ " ],\n",
138
+ "):\n",
139
+ " assignments[\"assignment_name\"] = pd.Categorical(\n",
140
+ " assignments[\"assignment_name\"], categories=custom_order, ordered=True\n",
141
+ " )\n",
142
+ "\n",
143
+ " # Sort by 'Category' (custom order) and then by 'Value'\n",
144
+ " sorted_df = assignments.sort_values(by=[\"assignment_name\", \"week_number\"])\n",
145
+ "\n",
146
+ " week_number = 3\n",
147
+ "\n",
148
+ " exclude_types = None\n",
149
+ "\n",
150
+ " sorted_df = filter_assignments(sorted_df, exclude_types=None, max_week=week_number)\n",
151
+ "\n",
152
+ " # Get unique pairs of 'week_number' and 'assignment_name'\n",
153
+ " unique_pairs = sorted_df[[\"week_number\", \"assignment_name\"]].drop_duplicates()\n",
154
+ "\n",
155
+ " # Build an iterator over the unique pairs\n",
156
+ " pair_iterator = unique_pairs.itertuples(index=False, name=None)\n",
157
+ "\n",
158
+ " grade_dictionary = {}\n",
159
+ "\n",
160
+ " # Filter rows matching each pair\n",
161
+ " for week_number, assignment_name in pair_iterator:\n",
162
+ " matching_rows = sorted_df[\n",
163
+ " (sorted_df[\"week_number\"] == week_number)\n",
164
+ " & (sorted_df[\"assignment_name\"] == assignment_name)\n",
165
+ " ]\n",
166
+ "\n",
167
+ " grade_dictionary[f\"Week {week_number} {assignment_name}\"] = {\n",
168
+ " \"week_number\": week_number,\n",
169
+ " \"assignment_name\": assignment_name,\n",
170
+ " \"score\": 0,\n",
171
+ " }\n",
172
+ "\n",
173
+ " # # extracts the names of the assignments grades which are associated with a given assignment, this helps if two assignments have the same name.\n",
174
+ " # title = matching_rows[\"title\"]\n",
175
+ "\n",
176
+ " # print(title)\n",
177
+ "\n",
178
+ " # gets the most recent assignment due date.\n",
179
+ " assignment_due_date = get_max_deadline(\n",
180
+ " assignments, assignment_name, week_number\n",
181
+ " )\n",
182
+ " max_score = matching_rows[\"max_score\"].min()\n",
183
+ "\n",
184
+ " # extracts the names of the assignments grades which are associated with a given assignment, this helps if two assignments have the same name.\n",
185
+ " for assignment_db_name in matching_rows[\"title\"]:\n",
186
+ " assignment_submission = submissions[\n",
187
+ " submissions[\"assignment\"] == assignment_db_name.replace(\" \", \"\").lower()\n",
188
+ " ]\n",
189
+ "\n",
190
+ " # Now we need to get the best score for an assignment based on the submission time.\n",
191
+ " for index, submission in assignment_submission.iterrows():\n",
192
+ " raw_score = submission[\"raw_score\"] / max_score\n",
193
+ "\n",
194
+ " assignment_due_date = datetime.fromisoformat(\n",
195
+ " assignment_due_date\n",
196
+ " ).strftime(\"%Y-%m-%d %H:%M:%S\")\n",
197
+ " submission_timestamp = datetime.fromisoformat(\n",
198
+ " submission[\"timestamp\"]\n",
199
+ " ).strftime(\"%Y-%m-%d %H:%M:%S\")\n",
200
+ "\n",
201
+ " deflation_fraction = calculate_late_submission(\n",
202
+ " assignment_due_date, submission_timestamp\n",
203
+ " )\n",
204
+ "\n",
205
+ " score = raw_score * deflation_fraction\n",
206
+ "\n",
207
+ " if (\n",
208
+ " score\n",
209
+ " > grade_dictionary[f\"Week {week_number} {assignment_name}\"][\"score\"]\n",
210
+ " ):\n",
211
+ " grade_dictionary[f\"Week {week_number} {assignment_name}\"][\n",
212
+ " \"score\"\n",
213
+ " ] = score\n",
214
+ "\n",
215
+ " return grade_dictionary"
216
+ ]
217
+ },
218
+ {
219
+ "cell_type": "code",
220
+ "execution_count": null,
221
+ "metadata": {},
222
+ "outputs": [],
223
+ "source": [
224
+ "import requests\n",
225
+ "from build.passwords import password, user\n",
226
+ "from requests.auth import HTTPBasicAuth\n",
227
+ "\n",
228
+ "api_base_url = \"https://engr-131-api.eastus.cloudapp.azure.com/\"\n",
229
+ "\n",
230
+ "\n",
231
+ "def get_all_students():\n",
232
+ " res = requests.get(\n",
233
+ " url=api_base_url.rstrip(\"/\") + \"/get-all-submission-emails\",\n",
234
+ " auth=HTTPBasicAuth(user(), password()),\n",
235
+ " )\n",
236
+ "\n",
237
+ " return res.json()\n",
238
+ "\n",
239
+ "\n",
240
+ "# def get_student_grades(student_id):\n",
241
+ "# params = {\"username\": student_id}\n",
242
+ "# res = requests.get(\n",
243
+ "# url=api_base_url.rstrip(\"/\") + \"/student-grades-testing\",\n",
244
+ "# params=params,\n",
245
+ "# auth=HTTPBasicAuth(user(), password()),\n",
246
+ "# )\n",
247
+ "\n",
248
+ "# [assignments, sub] = res.json()\n",
249
+ "\n",
250
+ "# return assignments, sub\n",
251
+ "\n",
252
+ "\n",
253
+ "def get_all_student_grades():\n",
254
+ " # Initialize an empty DataFrame to hold all student grades\n",
255
+ " df = pd.DataFrame()\n",
256
+ "\n",
257
+ " # Get all students (assuming get_all_students() is a defined function)\n",
258
+ " students = get_all_students()\n",
259
+ "\n",
260
+ " for student in students:\n",
261
+ " print(student)\n",
262
+ " # Get assignments and submissions for the student (assumed functions)\n",
263
+ " assignments, submissions = get_student_grades(student)\n",
264
+ "\n",
265
+ " # Recalculate grades and get a grades dictionary\n",
266
+ " grades_dict = recalculate_best_grades(assignments, submissions)\n",
267
+ "\n",
268
+ " # Calculate averages and build a row for the student\n",
269
+ " row = calculate_averages(grades_dict, student)\n",
270
+ "\n",
271
+ " # # Convert the row (a dictionary) into a DataFrame\n",
272
+ " # row_df = pd.DataFrame([row])\n",
273
+ "\n",
274
+ " # Append the row to the DataFrame\n",
275
+ " df = pd.concat([df, row], ignore_index=True)\n",
276
+ "\n",
277
+ " return df\n",
278
+ "\n",
279
+ "\n",
280
+ "def get_max_deadline(assignments, assignment_name, week_number):\n",
281
+ " matching_rows = assignments[\n",
282
+ " (assignments[\"week_number\"] == week_number)\n",
283
+ " & (assignments[\"assignment_name\"] == assignment_name)\n",
284
+ " ]\n",
285
+ "\n",
286
+ " max_timestamp = matching_rows[\"due_date\"].max()\n",
287
+ " return max_timestamp\n",
288
+ "\n",
289
+ "\n",
290
+ "def calculate_averages(grades_dict, student_id):\n",
291
+ " # Create a DataFrame from the dictionary\n",
292
+ " df = pd.DataFrame(\n",
293
+ " grades_dict\n",
294
+ " ).T # Transpose the dictionary to make assignments rows\n",
295
+ " df[\"score\"] = df[\"score\"].astype(float) # Ensure the scores are floats\n",
296
+ "\n",
297
+ " # Create a dictionary for the row data\n",
298
+ " row_data = {\"student_id\": student_id}\n",
299
+ "\n",
300
+ " # Add scores for each assignment as separate columns\n",
301
+ " for assignment, details in grades_dict.items():\n",
302
+ " column_name = (\n",
303
+ " f\"{details['assignment_name']}_week_{details['week_number']}\".replace(\n",
304
+ " \"_\", \" \"\n",
305
+ " )\n",
306
+ " )\n",
307
+ " row_data[column_name] = float(details[\"score\"])\n",
308
+ "\n",
309
+ " # Calculate and add average scores for each assignment category\n",
310
+ " averages = df.groupby(\"assignment_name\")[\"score\"].mean()\n",
311
+ " for assignment_name, avg_score in averages.items():\n",
312
+ " row_data[f\"{assignment_name}_average\".replace(\"_\", \" \")] = avg_score\n",
313
+ "\n",
314
+ " # # Course Percentage Performance\n",
315
+ " row_data[\"Current Course Percentage\"] = (\n",
316
+ " (row_data[\"quiz average\"] + row_data[\"practice quiz average\"] * 0.1) * 15\n",
317
+ " + (row_data[\"homework average\"]) * 15\n",
318
+ " + row_data[\"lab average\"] * 15\n",
319
+ " + row_data[\"lecture average\"] * 15\n",
320
+ " + row_data[\"readings average\"] * 15\n",
321
+ " + row_data[\"attendance average\"] * 5\n",
322
+ " ) / (15 + 15 + 15 + 15 + 15 + 5)\n",
323
+ "\n",
324
+ " # Create a single-row DataFrame\n",
325
+ " single_row_df = pd.DataFrame([row_data])\n",
326
+ " return single_row_df\n",
327
+ "\n",
328
+ "\n",
329
+ "def recalculate_best_grades(\n",
330
+ " assignments,\n",
331
+ " submissions,\n",
332
+ " custom_order=[\n",
333
+ " \"lecture\",\n",
334
+ " \"homework\",\n",
335
+ " \"lab\",\n",
336
+ " \"attendance\",\n",
337
+ " \"readings\",\n",
338
+ " \"quiz\",\n",
339
+ " \"practice quiz\",\n",
340
+ " ],\n",
341
+ "):\n",
342
+ " assignments[\"assignment_name\"] = pd.Categorical(\n",
343
+ " assignments[\"assignment_name\"], categories=custom_order, ordered=True\n",
344
+ " )\n",
345
+ "\n",
346
+ " # Sort by 'Category' (custom order) and then by 'Value'\n",
347
+ " sorted_df = assignments.sort_values(by=[\"assignment_name\", \"week_number\"])\n",
348
+ "\n",
349
+ " week_number = 3\n",
350
+ "\n",
351
+ " exclude_types = None\n",
352
+ "\n",
353
+ " sorted_df = filter_assignments(sorted_df, exclude_types=None, max_week=week_number)\n",
354
+ "\n",
355
+ " # Get unique pairs of 'week_number' and 'assignment_name'\n",
356
+ " unique_pairs = sorted_df[[\"week_number\", \"assignment_name\"]].drop_duplicates()\n",
357
+ "\n",
358
+ " # Build an iterator over the unique pairs\n",
359
+ " pair_iterator = unique_pairs.itertuples(index=False, name=None)\n",
360
+ "\n",
361
+ " grade_dictionary = {}\n",
362
+ "\n",
363
+ " # Filter rows matching each pair\n",
364
+ " for week_number, assignment_name in pair_iterator:\n",
365
+ " matching_rows = sorted_df[\n",
366
+ " (sorted_df[\"week_number\"] == week_number)\n",
367
+ " & (sorted_df[\"assignment_name\"] == assignment_name)\n",
368
+ " ]\n",
369
+ "\n",
370
+ " grade_dictionary[f\"Week {week_number} {assignment_name}\"] = {\n",
371
+ " \"week_number\": week_number,\n",
372
+ " \"assignment_name\": assignment_name,\n",
373
+ " \"score\": 0,\n",
374
+ " }\n",
375
+ "\n",
376
+ " # # extracts the names of the assignments grades which are associated with a given assignment, this helps if two assignments have the same name.\n",
377
+ " # title = matching_rows[\"title\"]\n",
378
+ "\n",
379
+ " # print(title)\n",
380
+ "\n",
381
+ " # gets the most recent assignment due date.\n",
382
+ " assignment_due_date = get_max_deadline(\n",
383
+ " assignments, assignment_name, week_number\n",
384
+ " )\n",
385
+ " max_score = matching_rows[\"max_score\"].min()\n",
386
+ "\n",
387
+ " # extracts the names of the assignments grades which are associated with a given assignment, this helps if two assignments have the same name.\n",
388
+ " for assignment_db_name in matching_rows[\"title\"]:\n",
389
+ " assignment_submission = submissions[\n",
390
+ " submissions[\"assignment\"] == assignment_db_name.replace(\" \", \"\").lower()\n",
391
+ " ]\n",
392
+ "\n",
393
+ " # Now we need to get the best score for an assignment based on the submission time.\n",
394
+ " for index, submission in assignment_submission.iterrows():\n",
395
+ " raw_score = submission[\"raw_score\"] / max_score\n",
396
+ "\n",
397
+ " assignment_due_date = datetime.fromisoformat(\n",
398
+ " assignment_due_date\n",
399
+ " ).strftime(\"%Y-%m-%d %H:%M:%S\")\n",
400
+ " submission_timestamp = datetime.fromisoformat(\n",
401
+ " submission[\"timestamp\"]\n",
402
+ " ).strftime(\"%Y-%m-%d %H:%M:%S\")\n",
403
+ "\n",
404
+ " deflation_fraction = calculate_late_submission(\n",
405
+ " assignment_due_date, submission_timestamp\n",
406
+ " )\n",
407
+ "\n",
408
+ " score = raw_score * deflation_fraction\n",
409
+ "\n",
410
+ " if (\n",
411
+ " score\n",
412
+ " > grade_dictionary[f\"Week {week_number} {assignment_name}\"][\"score\"]\n",
413
+ " ):\n",
414
+ " grade_dictionary[f\"Week {week_number} {assignment_name}\"][\n",
415
+ " \"score\"\n",
416
+ " ] = score\n",
417
+ "\n",
418
+ " return grade_dictionary"
419
+ ]
420
+ },
421
+ {
422
+ "cell_type": "code",
423
+ "execution_count": null,
424
+ "metadata": {},
425
+ "outputs": [],
426
+ "source": [
427
+ "get_student(\"jab864\")"
428
+ ]
429
+ },
430
+ {
431
+ "cell_type": "code",
432
+ "execution_count": null,
433
+ "metadata": {},
434
+ "outputs": [],
435
+ "source": [
436
+ "df = get_all_student_grades()"
437
+ ]
438
+ },
439
+ {
440
+ "cell_type": "code",
441
+ "execution_count": null,
442
+ "metadata": {},
443
+ "outputs": [],
444
+ "source": [
445
+ "df.to_csv(\"grades\")"
446
+ ]
447
+ },
448
+ {
449
+ "cell_type": "code",
450
+ "execution_count": null,
451
+ "metadata": {},
452
+ "outputs": [],
453
+ "source": []
454
+ }
455
+ ],
456
+ "metadata": {
457
+ "kernelspec": {
458
+ "display_name": "engr131_dev",
459
+ "language": "python",
460
+ "name": "python3"
461
+ },
462
+ "language_info": {
463
+ "codemirror_mode": {
464
+ "name": "ipython",
465
+ "version": 3
466
+ },
467
+ "file_extension": ".py",
468
+ "mimetype": "text/x-python",
469
+ "name": "python",
470
+ "nbconvert_exporter": "python",
471
+ "pygments_lexer": "ipython3",
472
+ "version": "3.12.7"
473
+ }
474
+ },
475
+ "nbformat": 4,
476
+ "nbformat_minor": 2
477
+ }
pykubegrader/utils.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import random
3
- from typing import Optional, Tuple
3
+ from typing import Any, Optional
4
4
 
5
5
  import panel as pn
6
6
 
@@ -9,22 +9,38 @@ student_user = os.getenv("user_name_student")
9
9
  student_pw = os.getenv("keys_student")
10
10
 
11
11
 
12
- def list_of_lists(options: list) -> bool:
12
+ def list_of_lists(options: list[Any]) -> bool:
13
13
  return all(isinstance(elem, list) for elem in options)
14
14
 
15
15
 
16
- def shuffle_options(options: list[Optional[str]], seed: int) -> list[Optional[str]]:
16
+ def shuffle_options(options: list[Any], seed: int) -> None:
17
+ """
18
+ Shuffle options in list[Optional[str]] or list[list[Optional[str]]].
19
+ Shuffling is done in place.
20
+ We annotate options as list[Any] just to keep Mypy happy.
21
+
22
+ Args:
23
+ options (list[Any]): List of options to shuffle
24
+ seed (int): Seed for RNG
25
+
26
+ Returns:
27
+ None
28
+ """
17
29
  random.seed(seed)
18
- random.shuffle(options)
19
30
 
20
- return options
31
+ if list_of_lists(options):
32
+ for i in range(len(options)):
33
+ inner_list: list[Optional[str]] = options[i]
34
+ random.shuffle(inner_list)
35
+ else:
36
+ random.shuffle(options)
21
37
 
22
38
 
23
39
  def shuffle_questions(
24
40
  desc_widgets: list[pn.pane.HTML],
25
41
  dropdowns: list[pn.widgets.Select] | list[pn.Column],
26
42
  seed: int,
27
- ) -> list[Tuple[pn.pane.HTML, pn.widgets.Select | pn.Column]]:
43
+ ) -> list[tuple[pn.pane.HTML, pn.widgets.Select | pn.Column]]:
28
44
  random.seed(seed)
29
45
 
30
46
  # Combine widgets into pairs
@@ -15,12 +15,12 @@ def MCQ(
15
15
  descriptions: list[str],
16
16
  options: list[str] | list[list[str]],
17
17
  initial_vals: list[str],
18
- ) -> Tuple[list[pn.pane.HTML], list[pn.widgets.RadioButtonGroup]]:
18
+ ) -> Tuple[list[pn.Column], list[pn.widgets.RadioBoxGroup]]:
19
19
  # Process descriptions through `process_questions_and_codes`
20
20
  processed_titles, code_blocks = process_questions_and_codes(descriptions)
21
21
 
22
22
  # Create rows for each description and its code block
23
- desc_widgets = []
23
+ desc_widgets: list[pn.Column] = []
24
24
  for title, code_block in zip(processed_titles, code_blocks):
25
25
  # Create an HTML pane for the title
26
26
  title_pane = pn.pane.HTML(
@@ -34,7 +34,7 @@ def MCQ(
34
34
  else:
35
35
  desc_widgets.append(pn.Column(title_pane, sizing_mode="stretch_width"))
36
36
 
37
- radio_buttons = [
37
+ radio_buttons: list[pn.widgets.RadioBoxGroup] = [
38
38
  pn.widgets.RadioBoxGroup(
39
39
  options=option,
40
40
  value=value,
@@ -1,13 +1,17 @@
1
+ from typing import Optional
2
+
1
3
  import panel as pn
2
4
 
3
5
 
4
- def process_questions_and_codes(titles):
6
+ def process_questions_and_codes(
7
+ titles: str | list[str],
8
+ ) -> tuple[list[str], list[Optional[pn.pane.Markdown]]]:
5
9
  # Ensure titles is a list
6
10
  if isinstance(titles, str):
7
11
  titles = [titles]
8
12
 
9
- processed_titles = []
10
- code_blocks = []
13
+ processed_titles: list[str] = []
14
+ code_blocks: list[Optional[pn.pane.Markdown]] = []
11
15
 
12
16
  for title in titles:
13
17
  # Split the title at the "```python" delimiter
@@ -2,6 +2,7 @@ import panel as pn
2
2
 
3
3
  from ..widgets.style import drexel_colors, raw_css
4
4
  from ..widgets_base.multi_select import MultiSelectQuestion
5
+ from .question_processor import process_questions_and_codes
5
6
 
6
7
  # Pass the custom CSS to Panel
7
8
  pn.extension(design="material", global_css=[drexel_colors], raw_css=[raw_css])
@@ -24,15 +25,26 @@ def MultiSelect(
24
25
  separator = pn.pane.HTML("<hr style='border:1px solid lightgray; width:100%;'>")
25
26
 
26
27
  i = 0
28
+ desc_width = "500px"
27
29
 
28
30
  for question, option_set in zip(descriptions, options):
29
- desc_width = "500px"
31
+ # Process descriptions through `process_questions_and_codes`
32
+ processed_titles, code_blocks = process_questions_and_codes(question)
30
33
 
31
- # Create description widget with separator
32
- desc_widget = pn.pane.HTML(
34
+ # Create an HTML pane for the title
35
+ title_pane = pn.pane.HTML(
33
36
  f"<hr style='border:1px solid lightgray; width:100%;'>"
34
- f"<div style='text-align: left; width: {desc_width};'><b>{question}</b></div>"
37
+ f"<div style='text-align: left; width: {desc_width};'><b>{processed_titles[0]}</b></div>"
35
38
  )
39
+ # Add the title and code block in a row
40
+ if code_blocks[0]:
41
+ desc_widget = pn.Column(
42
+ title_pane, code_blocks[0], sizing_mode="stretch_width"
43
+ )
44
+ else:
45
+ desc_widget = title_pane
46
+
47
+ # # Create description widget with separator
36
48
 
37
49
  # Create checkboxes for current question
38
50
  checkbox_set = [
@@ -1,10 +1,10 @@
1
1
  import time
2
- from typing import Callable, Tuple
2
+ from typing import Callable, Optional, Tuple
3
3
 
4
4
  import panel as pn
5
5
 
6
6
  from ..telemetry import ensure_responses, score_question, update_responses
7
- from ..utils import shuffle_questions
7
+ from ..utils import shuffle_options, shuffle_questions
8
8
  from ..widgets.style import drexel_colors, raw_css
9
9
 
10
10
  # Pass the custom CSS to Panel
@@ -19,12 +19,12 @@ class MultiSelectQuestion:
19
19
  self,
20
20
  title: str,
21
21
  style: Callable[
22
- [list[str], list[list[str]], list[bool]],
22
+ [list[str], list[list[Optional[str]]], list[bool]],
23
23
  Tuple[list[pn.pane.HTML], list[pn.Column]],
24
24
  ],
25
25
  question_number: int,
26
26
  keys: list[str],
27
- options: list[list[str]],
27
+ options: list[list[Optional[str]]],
28
28
  descriptions: list[str],
29
29
  points: int,
30
30
  ):
@@ -56,8 +56,8 @@ class MultiSelectQuestion:
56
56
 
57
57
  self.initial_vals = [getattr(self, key) for key in self.keys]
58
58
 
59
- # # add shuffle options to multi_select.py
60
- # options = shuffle_options(options, seed)
59
+ # add shuffle options to multi_select.py
60
+ shuffle_options(options, seed)
61
61
 
62
62
  description_widgets, self.widgets = style(
63
63
  descriptions, options, self.initial_vals
@@ -53,9 +53,11 @@ class ReadingPython:
53
53
  # Comment dropdowns
54
54
  #
55
55
 
56
+ shuffle_options(options["comments_options"], seed)
57
+
56
58
  self.dropdowns_for_comments: dict[str, pn.widgets.Select] = {
57
59
  line: pn.widgets.Select(
58
- options=shuffle_options(options["comments_options"], seed),
60
+ options=options["comments_options"],
59
61
  name=f"Line {line}:",
60
62
  value=getattr(self, f"q{question_number}_{i_comments + 1}"),
61
63
  width=600,
@@ -1,10 +1,10 @@
1
1
  import time
2
- from typing import Callable, Tuple
2
+ from typing import Callable, Optional, Tuple
3
3
 
4
4
  import panel as pn
5
5
 
6
6
  from ..telemetry import ensure_responses, score_question, update_responses
7
- from ..utils import shuffle_questions
7
+ from ..utils import shuffle_options, shuffle_questions
8
8
  from ..widgets.style import drexel_colors
9
9
 
10
10
  # Pass custom CSS to Panel
@@ -21,7 +21,7 @@ class SelectQuestion:
21
21
  ],
22
22
  question_number: int,
23
23
  keys: list[str],
24
- options: list,
24
+ options: list[Optional[str]] | list[list[Optional[str]]],
25
25
  descriptions: list[str],
26
26
  points: int,
27
27
  shuffle_answers: bool = True,
@@ -46,8 +46,8 @@ class SelectQuestion:
46
46
 
47
47
  self.initial_vals: list = [getattr(self, key) for key in self.keys]
48
48
 
49
- # if shuffle_answers:
50
- # options = shuffle_options(options, seed)
49
+ if shuffle_answers:
50
+ shuffle_options(options, seed)
51
51
 
52
52
  desc_widgets, self.widgets = style(descriptions, options, self.initial_vals)
53
53