Skip to content

Commit

Permalink
Adding new features required to facilitate tests (group creation and …
Browse files Browse the repository at this point in the history
…shadow assignment management).
  • Loading branch information
krulis-martin committed May 15, 2024
1 parent 6b31456 commit 0fa6d15
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 57 deletions.
76 changes: 55 additions & 21 deletions recodex/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ def fork_exercise(self, exercise_id, group_id):
})

def add_exercise_attachments(self, exercise_id, file_ids):
self.post("/exercises/{}/attachment-files".format(exercise_id), data={"files": file_ids})
self.post("/exercises/{}/attachment-files".format(exercise_id),
data={"files": file_ids})

def get_exercise_attachments(self, exercise_id):
return self.get("/exercises/{}/attachment-files".format(exercise_id))

def add_exercise_files(self, exercise_id, file_ids):
self.post("/exercises/{}/supplementary-files".format(exercise_id), data={"files": file_ids})
self.post("/exercises/{}/supplementary-files".format(exercise_id),
data={"files": file_ids})

def get_exercise_files(self, exercise_id):
return self.get("/exercises/{}/supplementary-files".format(exercise_id))
Expand All @@ -109,13 +111,16 @@ def update_exercise(self, exercise_id, details):
self.post('/exercises/{}'.format(exercise_id), data=details)

def set_exercise_archived(self, exercise_id, archived):
self.post('/exercises/{}/archived'.format(exercise_id), data={"archived": archived})
self.post('/exercises/{}/archived'.format(exercise_id),
data={"archived": archived})

def set_exercise_author(self, exercise_id, author):
self.post('/exercises/{}/author'.format(exercise_id), data={"author": author})
self.post('/exercises/{}/author'.format(exercise_id),
data={"author": author})

def set_exercise_admins(self, exercise_id, admins_ids):
self.post('/exercises/{}/admins'.format(exercise_id), data={"admins": admins_ids})
self.post('/exercises/{}/admins'.format(exercise_id),
data={"admins": admins_ids})

def delete_exercise(self, exercise_id):
self.delete('/exercises/{}'.format(exercise_id))
Expand Down Expand Up @@ -146,10 +151,12 @@ def get_exercise_config(self, exercise_id):
return self.get("/exercises/{}/config".format(exercise_id))

def update_exercise_config(self, exercise_id, config):
self.post("/exercises/{}/config".format(exercise_id), data={"config": config})
self.post("/exercises/{}/config".format(exercise_id),
data={"config": config})

def set_exercise_tests(self, exercise_id, tests):
self.post("/exercises/{}/tests".format(exercise_id), data={"tests": tests})
self.post("/exercises/{}/tests".format(exercise_id),
data={"tests": tests})

def get_exercise_tests(self, exercise_id):
return self.get("/exercises/{}/tests".format(exercise_id))
Expand All @@ -159,7 +166,8 @@ def update_limits(self, exercise_id, environment_id, hwgroup_id, limits):
data={"limits": limits})

def evaluate_reference_solutions(self, exercise_id):
self.post("/reference-solutions/exercise/{}/evaluate".format(exercise_id), data={})
self.post(
"/reference-solutions/exercise/{}/evaluate".format(exercise_id), data={})

def resubmit_reference_solution(self, ref_solution_id, debug=False):
return self.post("/reference-solutions/{}/resubmit".format(ref_solution_id), data={"debug": debug})
Expand Down Expand Up @@ -243,23 +251,35 @@ def get_users_list(self, user_ids):
def get_all_groups(self, archived=False):
return self.get("/groups?{}".format('archived=1' if archived else ''))

def create_group(self, data):
return self.post("/groups/", data=data)

def get_group(self, group_id):
return self.get("/groups/{}".format(group_id))

def get_group_assignments(self, group_id):
return self.get("/groups/{}/assignments".format(group_id))

def get_group_shadow_assignments(self, group_id):
return self.get("/groups/{}/shadow-assignments".format(group_id))

def group_add_student(self, group_id, user_id):
return self.post("/groups/{}/students/{}".format(group_id, user_id))

def group_remove_student(self, group_id, user_id):
return self.delete("/groups/{}/students/{}".format(group_id, user_id))

def group_attach_exercise(self, group_id, exercise_id):
return self.post("/exercises/{}/groups/{}".format(exercise_id, group_id))
return self.post("/exercises/{}/groups/{}"
.format(exercise_id, group_id))

def group_detach_exercise(self, group_id, exercise_id):
return self.delete("/exercises/{}/groups/{}".format(exercise_id, group_id))
return self.delete("/exercises/{}/groups/{}"
.format(exercise_id, group_id))

def set_group_exam_flag(self, group_id, is_exam=True):
return self.post("/groups/{}/exam".format(group_id),
data={"value": is_exam})

# Assignments and related stuff...

Expand Down Expand Up @@ -304,18 +324,30 @@ def delete_solution(self, solution_id):

# Shadow Assignments

def create_shadow_assignment(self, group_id):
return self.post("/shadow-assignments/", data={
'groupId': group_id,
})

def get_shadow_assignment(self, assignment_id):
return self.get("/shadow-assignments/{}".format(assignment_id))

def create_shadow_assignment_points(self, assignment_id, user_id, points, note, awarded_at=None):
return self.post("/shadow-assignments/{}/create-points/".format(assignment_id), data={
'userId': user_id,
'points': points,
'note': note,
'awardedAt': awarded_at,
})

def update_shadow_assignment_points(self, points_id, points, note, awarded_at=None):
def update_shadow_assignment(self, assignment_id, data):
return self.post("/shadow-assignments/{}".format(assignment_id),
data=data)

def create_shadow_assignment_points(self, assignment_id, user_id, points,
note, awarded_at=None):
return self.post("/shadow-assignments/{}/create-points/"
.format(assignment_id), data={
'userId': user_id,
'points': points,
'note': note,
'awardedAt': awarded_at,
})

def update_shadow_assignment_points(self, points_id, points, note,
awarded_at=None):
return self.post("/shadow-assignments/points/{}".format(points_id), data={
'points': points,
'note': note,
Expand Down Expand Up @@ -358,11 +390,13 @@ def extract_payload(response):
try:
json = response.json()
except JSONDecodeError:
logging.error("Loading JSON response failed, see full response below:")
logging.error(
"Loading JSON response failed, see full response below:")
logging.error(response.text)
raise RuntimeError("Loading JSON response failed")

if not json["success"]:
raise RuntimeError("Received error from API: " + json["error"]["message"])
raise RuntimeError("Received error from API: " +
json["error"]["message"])

return json["payload"]
4 changes: 2 additions & 2 deletions recodex/plugins/exercises/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def add_localization(api: ApiClient, locale, exercise_id, include_name):


def _add_reference_solution(api: ApiClient, exercise_id, note, runtime_environment, files):
uploaded_files = [api.upload_file(file, open(file, "r"))[
uploaded_files = [api.upload_file(file, open(file, "rb"))[
"id"] for file in files]

preflight = api.presubmit_check(exercise_id, uploaded_files)
Expand Down Expand Up @@ -329,7 +329,7 @@ def set_config(api: ApiClient, exercise_id, file_name, useJson):
"""
Load a JSON or YAML from a file and set it as configuration.
"""
with open(file_name, 'r') as stream:
with open(file_name, 'r', encoding="utf-8") as stream:
if useJson:
config = json.load(stream)
else:
Expand Down
133 changes: 99 additions & 34 deletions recodex/plugins/groups/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,41 @@ def all(api: ApiClient, useJson, archived):


@cli.command()
@click.argument("group_id")
@click.option("--json/--yaml", "useJson", default=False)
@click.argument("parent_id")
@pass_api_client
def create(api: ApiClient, parent_id):
"""
Create a new group.
"""
parent = api.get_group(parent_id)
data = {
"isPublic": False,
"publicStats": False,
"isOrganizational": False,
"detaining": False,
"externalId": "",
"hasThreshold": False,
"localizedTexts": [],
"noAdmin": False,
}

data_input = sys.stdin.read().strip()
data_json = json.loads(data_input)
for [key, value] in data_json.items():
if key in data:
data[key] = value

data["instanceId"] = parent["privateData"]["instanceId"]
data["parentGroupId"] = parent_id

group = api.create_group(data)
click.echo(group['id'])


@ cli.command()
@ click.argument("group_id")
@ click.option("--json/--yaml", "useJson", default=False)
@ pass_api_client
def detail(api: ApiClient, group_id, useJson):
"""
Read detailed data about given group
Expand All @@ -47,61 +79,59 @@ def detail(api: ApiClient, group_id, useJson):
json.dump(group, sys.stdout, sort_keys=True, indent=4)
elif useJson is False:
yaml = YAML(typ="safe")
yaml.dump(group, sys.stdout)
yaml.dump(group, sys.stdout)


@cli.command()
@click.argument("group_id")
@click.argument("user_id")
@pass_api_client
@ cli.command()
@ click.argument("group_id")
@ click.argument("user_id")
@ pass_api_client
def join(api: ApiClient, group_id, user_id):
"""
Add user as a member (student) of a group
"""

api.group_add_student(group_id, user_id)


@cli.command()
@click.argument("group_id")
@click.argument("user_id")
@pass_api_client
def leave(api: ApiClient, group_id, user_id):
"""
@ cli.command()
@ click.argument("group_id")
@ click.argument("user_id")
@ pass_api_client
def leave(api: ApiClient, group_id, user_id):
"""
Remove user (student) from a group
"""

api.group_remove_student(group_id, user_id)


@cli.command()
@click.argument("group_id")
@click.argument("exercise_id")
@pass_api_client
@ cli.command()
@ click.argument("group_id")
@ click.argument("exercise_id")
@ pass_api_client
def attach(api: ApiClient, group_id, exercise_id):
"""
Attach exercise to a group of residence
"""

api.group_attach_exercise(group_id, exercise_id)


@cli.command()
@click.argument("group_id")
@click.argument("exercise_id")
@pass_api_client
def detach(api: ApiClient, group_id, exercise_id):
"""
@ cli.command()
@ click.argument("group_id")
@ click.argument("exercise_id")
@ pass_api_client
def detach(api: ApiClient, group_id, exercise_id):
"""
Detach exercise from a group of residence
"""

api.group_detach_exercise(group_id, exercise_id)


@cli.command()
@click.argument("group_id")
@click.option("--json/--yaml", "useJson", default=None)
@pass_api_client
@ cli.command()
@ click.argument("group_id")
@ click.option("--json/--yaml", "useJson", default=None)
@ pass_api_client
def students(api: ApiClient, group_id, useJson):
"""
List all students of a group.
Expand All @@ -118,10 +148,10 @@ def students(api: ApiClient, group_id, useJson):
click.echo("{} {}".format(student["id"], student["fullName"]))


@cli.command()
@click.argument("group_id")
@click.option("--json/--yaml", "useJson", default=None)
@pass_api_client
@ cli.command()
@ click.argument("group_id")
@ click.option("--json/--yaml", "useJson", default=None)
@ pass_api_client
def assignments(api: ApiClient, group_id, useJson):
"""
List all (regular) assignments of a group.
Expand All @@ -136,4 +166,39 @@ def assignments(api: ApiClient, group_id, useJson):
else:
for assignment in assignments:
click.echo("{} {}".format(
assignment["id"], get_localized_name(assignment["localizedTexts"])))
assignment["id"], get_localized_name(
assignment["localizedTexts"])))


@ cli.command()
@ click.argument("group_id")
@ click.option("--json/--yaml", "useJson", default=None)
@ pass_api_client
def shadow_assignments(api: ApiClient, group_id, useJson):
"""
List all shadow assignments of a group.
"""

assignments = api.get_group_shadow_assignments(group_id)
if useJson is True:
json.dump(assignments, sys.stdout, sort_keys=True, indent=4)
elif useJson is False:
yaml = YAML(typ="safe")
yaml.dump(assignments, sys.stdout)
else:
for assignment in assignments:
click.echo("{} {}".format(
assignment["id"], get_localized_name(
assignment["localizedTexts"])))


@cli.command()
@click.argument("group_id")
@click.option("--unset", is_flag=True)
@pass_api_client
def enable(api: ApiClient, group_id, unset):
"""
Set (or unset) exam flag of a group.
"""

api.set_group_exam_flag(group_id, not unset)
Loading

0 comments on commit 0fa6d15

Please sign in to comment.