-
Notifications
You must be signed in to change notification settings - Fork 2
Add support for workspaces and serverFilesQuestion #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
922b3c8
Add support for workspaceoptions key
Bluesy1 266827d
Update file copying for question generation
Bluesy1 09c445b
Fix copy files for public variants of questions
Bluesy1 e9aad34
Implement workspace question type
Bluesy1 3045c4c
Validate workspaceOptions.port type as integer
Bluesy1 f17f055
Add new test for workspaces
Bluesy1 30b8226
Remove custom error in favor of just using `FileNotFoundError`
Bluesy1 5fbdd73
Add files to test serverFilesQuestion
Bluesy1 43424f6
Merge branch 'main' into add-workspace-support
Bluesy1 5c92c91
Merge branch 'main' into add-workspace-support
Bluesy1 638fb84
Fix tests
Bluesy1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -457,6 +457,7 @@ def write_info_json(output_path, parsed_question): | |
"singleVariant", | ||
"showCorrectAnswer", | ||
"externalGradingOptions", | ||
"workspaceOptions" | ||
} | ||
|
||
# Add tags based on part type | ||
|
@@ -489,6 +490,15 @@ def write_info_json(output_path, parsed_question): | |
} | ||
) | ||
|
||
if "workspaceOptions" in info_json: # validate workspaceOptions contains the required keys if it exists | ||
image = "image" in info_json["workspaceOptions"] | ||
port = "port" in info_json["workspaceOptions"] | ||
home = "home" in info_json["workspaceOptions"] | ||
if not (image and port and home): | ||
raise SyntaxError("workspaceOptions must contain image, port, and home keys") | ||
if not isinstance(info_json["workspaceOptions"]["port"], int): | ||
raise TypeError(f"workspaceOptions.port must be an integer, got {type(info_json['workspaceOptions']['port'])!r} instead") | ||
|
||
# End add tags | ||
with pathlib.Path(output_path / "info.json").open("w") as output_file: | ||
json.dump(info_json, output_file, indent=4) | ||
|
@@ -874,6 +884,31 @@ def process_string_input(part_name, parsed_question, data_dict): | |
return replace_tags(html) | ||
|
||
|
||
def process_workspace(part_name, parsed_question, data_dict): | ||
"""Processes markdown format of workspace questions and returns PL HTML | ||
Args: | ||
part_name (string): Name of the question part being processed (e.g., part1, part2, etc...) | ||
parsed_question (dict): Dictionary of the MD-parsed question (output of `read_md_problem`) | ||
data_dict (dict): Dictionary of the `data` dict created after running server.py using `exec()` | ||
|
||
Returns: | ||
html: A string of HTML that is part of the final PL question.html file. | ||
""" | ||
if "pl-customizations" in parsed_question["header"][part_name]: | ||
if len(parsed_question["header"][part_name]["pl-customizations"]) > 0: | ||
raise ValueError("pl-customizations are not supported for workspace questions") | ||
Bluesy1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
html = f"""<pl-question-panel>\n<markdown>{parsed_question['body_parts_split'][part_name]['content']}</markdown>\n</pl-question-panel>\n\n""" | ||
|
||
html += f"""<pl-workspace></pl-workspace>""" | ||
|
||
if parsed_question["header"][part_name].get("gradingMethod", None) == "External": | ||
html += f"""<pl-submission-panel>\n\t<pl-external-grader-results></pl-external-grader-results>\n\t<pl-file-preview></pl-file-preview></pl-submission-panel>""" | ||
Bluesy1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return replace_tags(html) | ||
|
||
|
||
def process_matrix_component_input(part_name, parsed_question, data_dict): | ||
"""Processes markdown format of matrix-component-input questions and returns PL HTML | ||
Args: | ||
|
@@ -1171,24 +1206,56 @@ def str_presenter(dumper, data2): | |
encoding="utf8", | ||
) | ||
|
||
# Move image assets | ||
# Create the file errors list | ||
os_errors = [] | ||
|
||
# Move client assets (generally images) | ||
files_to_copy = header.get("assets") | ||
if files_to_copy: | ||
[ | ||
copy2(pathlib.Path(source_filepath).parent / fl, output_path.parent) | ||
for fl in files_to_copy | ||
] | ||
pl_path = output_path.parent | ||
for file in files_to_copy: | ||
try: | ||
copy2(pathlib.Path(source_filepath).parent / file, pl_path / file) | ||
except (FileExistsError, FileNotFoundError, IsADirectoryError, PermissionError) as e: | ||
os_errors.append(str(e)) | ||
|
||
# Move server assets | ||
files_to_copy = header.get("serverFiles") | ||
if files_to_copy and instructor: | ||
pl_path = output_path.parent | ||
for file in files_to_copy: | ||
try: | ||
copy2(pathlib.Path(source_filepath).parent / file, pl_path / file) | ||
except (FileExistsError, FileNotFoundError, IsADirectoryError, PermissionError) as e: | ||
os_errors.append(str(e)) | ||
Bluesy1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Move autograde py test files | ||
files_to_copy = header.get("autogradeTestFiles") | ||
if files_to_copy: | ||
pl_path = output_path.parent / "tests" | ||
pl_path.mkdir(parents=True, exist_ok=True) | ||
[ | ||
copy2(pathlib.Path(source_filepath).parent / "tests" / fl, pl_path / fl) | ||
for fl in files_to_copy | ||
if (instructor or fl == "starter_code.py") | ||
] | ||
for file in files_to_copy: | ||
if file != "starter_code.py" and not instructor: | ||
continue | ||
try: | ||
copy2(pathlib.Path(source_filepath).parent / "tests" / file, pl_path / file) | ||
except (FileExistsError, FileNotFoundError, IsADirectoryError, PermissionError) as e: | ||
os_errors.append(str(e)) | ||
|
||
# Move workspace files | ||
files_to_copy = header.get("workspaceFiles") | ||
if files_to_copy: | ||
pl_path = output_path.parent / "workspace" | ||
pl_path.mkdir(parents=True, exist_ok=True) | ||
for file in files_to_copy: | ||
try: | ||
copy2(pathlib.Path(source_filepath).parent / "workspace" / file, pl_path / file) | ||
except (FileExistsError, FileNotFoundError, IsADirectoryError, PermissionError) as e: | ||
os_errors.append(str(e)) | ||
|
||
if os_errors: | ||
error_msg = "\n ".join(os_errors) | ||
raise FileNotFoundError(f"Error(s) copying specified files:\n {error_msg}") | ||
Comment on lines
+1255
to
+1258
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea behind the try-except system here is to show all file copy errors to the best of our ability at once, instead of having it show up one file at a time. The output of this should look something as follows, as an example: FileNotFoundError: Error(s) copying specified files:
[Errno 2] No such file or directory: 'test0'
[Errno 2] No such file or directory: 'test1'
[Errno 2] No such file or directory: 'test2'
[Errno 2] No such file or directory: 'test3'
[Errno 2] No such file or directory: 'test4'
[Errno 2] No such file or directory: 'test5'
[Errno 2] No such file or directory: 'test6'
[Errno 2] No such file or directory: 'test7'
[Errno 2] No such file or directory: 'test8'
[Errno 2] No such file or directory: 'test9' |
||
|
||
|
||
def process_question_pl(source_filepath, output_path=None, dev=False): | ||
|
@@ -1310,6 +1377,8 @@ def process_question_pl(source_filepath, output_path=None, dev=False): | |
question_html += process_string_input(part, parsed_q, data2) | ||
elif "matching" in q_type: | ||
question_html += process_matching(part, parsed_q, data2) | ||
elif "workspace" in q_type: | ||
question_html += process_workspace(part, parsed_q, data2) | ||
elif "matrix-component-input" in q_type: | ||
question_html += process_matrix_component_input(part, parsed_q, data2) | ||
elif "matrix-input" in q_type: | ||
|
@@ -1364,25 +1433,56 @@ def process_question_pl(source_filepath, output_path=None, dev=False): | |
# Write server.py file | ||
write_server_py(output_path, parsed_q) | ||
|
||
# Move image assets | ||
# Create the file errors list | ||
os_errors = [] | ||
|
||
# Move client assets (generally images) | ||
files_to_copy = parsed_q["header"].get("assets") | ||
if files_to_copy: | ||
pl_path = output_path / "clientFilesQuestion" | ||
pl_path.mkdir(parents=True, exist_ok=True) | ||
[ | ||
copy2(pathlib.Path(source_filepath).parent / fl, pl_path / fl) | ||
for fl in files_to_copy | ||
] | ||
for file in files_to_copy: | ||
try: | ||
copy2(pathlib.Path(source_filepath).parent / file, pl_path / file) | ||
except (FileExistsError, FileNotFoundError, IsADirectoryError, PermissionError) as e: | ||
os_errors.append(str(e)) | ||
|
||
# Move server assets | ||
files_to_copy = parsed_q["header"].get("serverFiles") | ||
if files_to_copy: | ||
pl_path = output_path / "serverFilesQuestion" | ||
pl_path.mkdir(parents=True, exist_ok=True) | ||
for file in files_to_copy: | ||
try: | ||
copy2(pathlib.Path(source_filepath).parent / file, pl_path / file) | ||
except (FileExistsError, FileNotFoundError, IsADirectoryError, PermissionError) as e: | ||
os_errors.append(str(e)) | ||
|
||
# Move autograde py test files | ||
files_to_copy = parsed_q["header"].get("autogradeTestFiles") | ||
if files_to_copy: | ||
pl_path = output_path / "tests" | ||
pl_path.mkdir(parents=True, exist_ok=True) | ||
[ | ||
copy2(pathlib.Path(source_filepath).parent / "tests" / fl, pl_path / fl) | ||
for fl in files_to_copy | ||
] | ||
for file in files_to_copy: | ||
try: | ||
copy2(pathlib.Path(source_filepath).parent / "tests" / file, pl_path / file) | ||
except (FileExistsError, FileNotFoundError, IsADirectoryError, PermissionError) as e: | ||
os_errors.append(str(e)) | ||
|
||
# Move workspace files | ||
files_to_copy = parsed_q["header"].get("workspaceFiles") | ||
if files_to_copy: | ||
pl_path = output_path / "workspace" | ||
pl_path.mkdir(parents=True, exist_ok=True) | ||
for file in files_to_copy: | ||
try: | ||
copy2(pathlib.Path(source_filepath).parent / "workspace" / file, pl_path / file) | ||
except (FileExistsError, FileNotFoundError, IsADirectoryError, PermissionError) as e: | ||
os_errors.append(str(e)) | ||
|
||
if os_errors: | ||
error_msg = "\n ".join(os_errors) | ||
raise FileNotFoundError(f"Error(s) copying specified files:\n {error_msg}") | ||
|
||
|
||
def pl_image_path(html): | ||
|
@@ -1433,3 +1533,4 @@ def validate_header(header_dict): | |
|
||
if topics.get(topic := header_dict["topic"], None) is None: | ||
raise ValueError(f"topic '{topic}' is not listed in the learning outcomes") | ||
|
18 changes: 18 additions & 0 deletions
18
tests/test_question_templates/question_expected_outputs/instructor/q16_workspaces/file1.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
Gk44G5S2cov8PgPpy9n7r0v4FYY7zU/7i8UNDrRYLU1GjpVdCPIzdbUUcln3CqNlBTEKjn75+3qs | ||
DUvteohtwFF+4BYwAyfv7yjzX9CPsyfHjBYC+b+fMHPMFns1vdlGUvwDEePE4k1EnJECUZKTkebJ | ||
ymKhR6BY4IUnO5cVsRARExtVkYddHUcNAhgLpN9E/SNln9gJ9fN4WRat6l2QSDaH2OJ96+lDM/yR | ||
EVttiFtCynzgguVYQy06tlpLXk9a9MwPlVGJvH/qH28pHkIac2H6UpWnDcBptsVJTVfTVv9Gp9/s | ||
IQmeZRDIUy6163Ga9Uke6o1W7dVeohjvwY/KQHApZcK15RDkb+UiBd/9VM26N7eMoW41Sg7SKtQZ | ||
+i8vEu2H6whEnvZCnYg+sOh9fxaDlOQhh7qhu4g2z1TORCjWzgELzZEmPJVrsf45cG0tz0xKoF/d | ||
Uw/YoHF8RJexASnLZWj4125sNCQ6XfoEl1DLzUULxH7SeWM9owpouV5RZC3LZmCgyDUvbI0oMk+l | ||
9GTae6lcMLOKD1R2q4NksQAt/osyWXQZw1g6pcqNSz1TdirbiQsTY6g+JZ3pYN2tK2w4WqVnLPix | ||
lI9ktBec6XknnEEvrI7NhPLEXdxS7mUXuapCV6EfMfDPMvW523EUQPE5MWYBvHTmlIW5pemWgsAd | ||
vuCFOIjs73L0es+88D3gCrSGSyk7KydSY3f2XCD8PLf78ZamVWi9yRpQryc33bRB2CaGhHhmbKto | ||
4nsb+ToaG0aZE2FtIjOHTyC8jjsqgLRbHfRXJVFnngKVT+DEQnrfLRuC8+PtMrDDpZ4VarpK0aLd | ||
BMtHPOsg+cOhwhqU021s2nq6G6XKfSDHeRR/2T/R0loeMr8/6UCVQKXitWl2f//tfnBYYMQObys3 | ||
tYsJC46pR+Gp9ytUxc+//NKKmhn1J4Sm8anhGeePEmxroVbm5O16jk62u+L2jNJ0HOGYXNKizuKP | ||
aVSReke4ZqdxJCCLZjLS6q4B5v5s7XK2TRFUb7BUeOVvezG698khGjCdPDOZH1gYxRoKR5VrkM1z | ||
Mr8VPMi6io9GhaGehNz+N5ygXp6IG9VOht+j7H1y76rgooUtmtNKLuySTkf4nxK/FocuyCsDxP0r | ||
jIZIgLEn1CD6R+ewbVZbFqwJt5RfJRpGss3Lmn3SdF4VEYQNHos6ytL0JzejC0Q7Foy5YC0cC0QC | ||
fjpFZcbNXCJSnq6fX+KKq8MdHdivisU2Nd+5TCeqDdpuz2Plxs0o6wMlrYxIJKH+caxzvtlKdmRh | ||
u1Ko/xz1A4VkljPJAudnrWXuvgfxVCacQ0DNJ/I8fA== |
18 changes: 18 additions & 0 deletions
18
tests/test_question_templates/question_expected_outputs/instructor/q16_workspaces/file2.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
DBVUxxDUlMoztjq4PiJSvgtwoY56pePoFur5Pz64DnE3tf2d7ffUtDBqluysfK62jpBzD9Ws6sTp | ||
HZj7GIV1UQZloqbT2jPwyi5HGs1/aDfwTJA+ISur9mSEKLGEHADFqMsC9B5OW1P6hpzWiIjTGzde | ||
VBOE5LfCy03VEsHfu90Ouk+PjEkxCz7rdIjjmxIDTp0GVmy73UwjvPT9Qwu70i2qSA1MDpCfAFmu | ||
daTha8axoUcTnhNFlT2jVf4e/4cITNEsi/T/mMbpAtbXQXXLfflv7btJDTnY7L05AJetLmIXhZXB | ||
OCwkA0d4gPMjG6p5Lkq3Vs3aeO8CVgSLeKEWz8Pru1pbSe0xDtt6EGZXK4axQ/gpZQfymEdqF4x5 | ||
V3qqlVMDYcwIHkZiLdmxZOBX6J62yHtU7VdMD8zaT2Ds+fnqc4O0kRuUl+wZb+aQ1+ScZ17mxZ4B | ||
6//1Ped29UUHuZQcJlOfrbtoXH4ZNO01OzZbQQ9YHlHLpOKQNqPXyBBH0bTz53ta5QX0ItMZ42eo | ||
61fMjWm4baU4UmB1s9jlQGMYoVoo8URROfuZn7kirS9J5YLOpAuny+dBZsS8ezjwFput3Hh5JyYo | ||
qIiC7oIVjHsHoYgAELxxFzLR3RreRxbKcoNxvdBu/4RngKzahXypIVPx0ldMiWIVnoEZlJtq5WE5 | ||
S49N+ojExqOOWPPgXxcUP8dHkhfXoDgTph4M0ZFb4H6FJuwX/R7CqNHnqHbgYiCj1tZG4LBwVO/v | ||
kamJudzm/SBvLlUycL9JrMgedLnUmVZorUgj4IVDju8lyHcz8RRBFHCBTh4LII+W+PBQ21LBCsH8 | ||
2BZPOh8MLXFb8G7stl/n0rMkqC+tmRw2CAjwmbz/54nwuzm8o7k7cGXhou6blkx6tBlC54muSBWd | ||
X7vNSctuY+JwwgLXk5JfeHIF2JdaCNaGyyBFgPer2rTG9PuoHD3QRibXOJSHy+NlCLrdrZXB89HQ | ||
WNCpeBjWjlclG/O7LL2yuvrkvM5eohPjRBIHa4lxzEecL5Y+SPUOy7vIg0DChpL5CzpbYzgM15mS | ||
Zd/ai7rkBzfdJMlmxTXNewMVaf4drtx9Cg9HJAWCjIjaX1lqOfku2vqKetfLQytz1qnNYo8oPcKE | ||
PmaHbGkubdHI3+dDbqva3LWgrKjUNdVaQq+ICo57Pdo1FycScSyWW58Peldh0mIZodxVt8IiDiR3 | ||
hjVJqGkRuxT0T07Dc2fXHKtylz8L2VdAQeeqqdaLvJnNVRU/CRzjdLLnWKUzC4f3CiFVeHW1MiRn | ||
hWLsmy+tcEvZnFh29K1Ei55NFBcQjUvBQvmZgg+SiQ== |
18 changes: 18 additions & 0 deletions
18
...s/test_question_templates/question_expected_outputs/instructor/q16_workspaces/filebad.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
a5HQSUh3ZVyvZxZvF2t+Pi3BLjHQ8MTJJ/2I7rqOQQ+mPC3AWAD5WHR5OZ+XwE6qfVjThv0rH1Lk | ||
3N9phhpM+4Ij7g2KwPK6sb076LbD6PRVNpIVjEfEuzdi8pcZ4hDvYbPOSZ3FSpqvDGbFCBRSobSp | ||
npEnw2DkyABjlvh5JKsv732jrZBBXHqgtBE7RgVeT+wlXo6nu8h1330fOYQ3y0F8UCWELtqfHseU | ||
A3T/yB56wOG33k8jBvpAzAYmM+zr3xfFKhp36Ak2NvKH2oikeUi5wB1q8kaVVbHKCEoCYYW/mYnf | ||
FFaAh2zMYOhVEeNUaee8RrSQvIXar2vgOim9W4vJxgKRUpjp09YFDw9lOgu4ddru60pR7QLNmCP5 | ||
ht1jImO1p0TZ+A/Vwp6jKskDFqKy7ZZBTHYT99scrUWd7OEyyIbWF7OSIeq8wubAqcbAJ6NMUBRv | ||
JqPxsbNNhkm/Xw4zvhQyhsI5P9QIjBpzL8SwxdhD5DdGfP8JGQLaLlvW8GlgBhteNY2k5lpSL2YJ | ||
UsT4BT6fkeZnaWNfxJR69eAI98PeDiVW8KedvtE/wU4WlhmXi/bY7lenZi6sBSzqiXxaJujszLIG | ||
c/bahugWAMXEBHGBduaYBOI6JGdFY2219qqKlY1aM2WUBTh/IF+Ig9PZDSMGW/Qo4HEo8HYge1sl | ||
JkbcIL2zNhLpUJR8eCaZ06QI8KOa2LvtVLRBFRCdLXhdelD3uZDYWRVh+0ykASdCNURqayIxC3ys | ||
Uwq95Qt5uB0teliY/xQ+JRvu6YnU+kStJGrEgaeS8ZjF85ogFCbNUnqvub1mvek/y23Z5SSYSKm/ | ||
KcUpAIVfAg+W2YriERzUEsyzMjTKS1MP4mEeWUGgBIR4yGU2hivyd4P0eeIAti/zCTaKQBTRwXJX | ||
4nar06DgLUj4HHdcd7aaLTxt4bHA646IXO+2qYxQAZxvKkgWfZ6QWFakXVROR/6lfqJDLD3QVFD0 | ||
0d25EOhPbxe/Q7L8TR7rMsmE7cpJvA/Mjv36g6f2l0PngzPlm/EiJ0i0wn/3Al4A5+AEfxDwRMFo | ||
rNkaJEyEkRBDo5UeVdnTVXH1z70RLSju0n4oJe3NS47nv+gP5brMCkPxEh0pY1vtSFskaLHqJZwV | ||
NWFSdij8THFL0dlfKG5wyPwtwCuQqCz029wRjfJNFEgfqWOGGXZLLC+7n4wdSr2QO+ppZdKqB/Bl | ||
t6i2u5iwH+XrYf4j9THtXCQ6w/FEere2q8EBkCaTSOhKC+Gyp1rAnyuuYIy/dxprJKI4EzhgVHYo | ||
171ZFv6aNT1ETGgB8ejaU9025ozLjRHvcbuHIZsm3g== |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.