PyKubeGrader 0.3.1__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.1
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=Ltd2skxEP1OdZA_D3Kb-WyTpDVtv4e35v6_XiMFZ9Vo,85378
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=wyRPCEdI6GorPX3HzSbDiT_IHr34mFOUHzUN085-d88,4621
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.1.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
33
- PyKubeGrader-0.3.1.dist-info/METADATA,sha256=fqlGOEpvYY7AyGQV7ngPA01-D00cnsja365LanEQoFE,2806
34
- PyKubeGrader-0.3.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
35
- PyKubeGrader-0.3.1.dist-info/entry_points.txt,sha256=BbLXpFZObpOXA8e3p3GcFkL-sHdUnDLUcnYmc6zx3NI,201
36
- PyKubeGrader-0.3.1.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
37
- PyKubeGrader-0.3.1.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,,
@@ -1376,7 +1376,7 @@ def extract_SELECT_MANY(ipynb_file):
1376
1376
  # if question_text_match
1377
1377
  # else None
1378
1378
  # )
1379
-
1379
+
1380
1380
  # Extract question text enable multiple lines
1381
1381
  question_text = extract_question(markdown_content)
1382
1382
 
@@ -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
@@ -30,20 +30,21 @@ def MultiSelect(
30
30
  for question, option_set in zip(descriptions, options):
31
31
  # Process descriptions through `process_questions_and_codes`
32
32
  processed_titles, code_blocks = process_questions_and_codes(question)
33
-
33
+
34
34
  # Create an HTML pane for the title
35
- title_pane = pn.pane.HTML(
35
+ title_pane = pn.pane.HTML(
36
36
  f"<hr style='border:1px solid lightgray; width:100%;'>"
37
37
  f"<div style='text-align: left; width: {desc_width};'><b>{processed_titles[0]}</b></div>"
38
38
  )
39
39
  # Add the title and code block in a row
40
40
  if code_blocks[0]:
41
- desc_widget = pn.Column(title_pane, code_blocks[0], sizing_mode="stretch_width")
41
+ desc_widget = pn.Column(
42
+ title_pane, code_blocks[0], sizing_mode="stretch_width"
43
+ )
42
44
  else:
43
45
  desc_widget = title_pane
44
-
46
+
45
47
  # # Create description widget with separator
46
-
47
48
 
48
49
  # Create checkboxes for current question
49
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