Skip to content

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 11 commits into from
May 19, 2024
139 changes: 120 additions & 19 deletions src/problem_bank_scripts/problem_bank_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ def write_info_json(output_path, parsed_question):
"singleVariant",
"showCorrectAnswer",
"externalGradingOptions",
"workspaceOptions"
}

# Add tags based on part type
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")


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>"""

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:
Expand Down Expand Up @@ -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))

# 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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")

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==
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==
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==
Loading