Skip to content

Commit 0fa6d15

Browse files
committed
Adding new features required to facilitate tests (group creation and shadow assignment management).
1 parent 6b31456 commit 0fa6d15

File tree

4 files changed

+190
-57
lines changed

4 files changed

+190
-57
lines changed

recodex/api.py

+55-21
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,15 @@ def fork_exercise(self, exercise_id, group_id):
9191
})
9292

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

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

99100
def add_exercise_files(self, exercise_id, file_ids):
100-
self.post("/exercises/{}/supplementary-files".format(exercise_id), data={"files": file_ids})
101+
self.post("/exercises/{}/supplementary-files".format(exercise_id),
102+
data={"files": file_ids})
101103

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

111113
def set_exercise_archived(self, exercise_id, archived):
112-
self.post('/exercises/{}/archived'.format(exercise_id), data={"archived": archived})
114+
self.post('/exercises/{}/archived'.format(exercise_id),
115+
data={"archived": archived})
113116

114117
def set_exercise_author(self, exercise_id, author):
115-
self.post('/exercises/{}/author'.format(exercise_id), data={"author": author})
118+
self.post('/exercises/{}/author'.format(exercise_id),
119+
data={"author": author})
116120

117121
def set_exercise_admins(self, exercise_id, admins_ids):
118-
self.post('/exercises/{}/admins'.format(exercise_id), data={"admins": admins_ids})
122+
self.post('/exercises/{}/admins'.format(exercise_id),
123+
data={"admins": admins_ids})
119124

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

148153
def update_exercise_config(self, exercise_id, config):
149-
self.post("/exercises/{}/config".format(exercise_id), data={"config": config})
154+
self.post("/exercises/{}/config".format(exercise_id),
155+
data={"config": config})
150156

151157
def set_exercise_tests(self, exercise_id, tests):
152-
self.post("/exercises/{}/tests".format(exercise_id), data={"tests": tests})
158+
self.post("/exercises/{}/tests".format(exercise_id),
159+
data={"tests": tests})
153160

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

161168
def evaluate_reference_solutions(self, exercise_id):
162-
self.post("/reference-solutions/exercise/{}/evaluate".format(exercise_id), data={})
169+
self.post(
170+
"/reference-solutions/exercise/{}/evaluate".format(exercise_id), data={})
163171

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

254+
def create_group(self, data):
255+
return self.post("/groups/", data=data)
256+
246257
def get_group(self, group_id):
247258
return self.get("/groups/{}".format(group_id))
248259

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

263+
def get_group_shadow_assignments(self, group_id):
264+
return self.get("/groups/{}/shadow-assignments".format(group_id))
265+
252266
def group_add_student(self, group_id, user_id):
253267
return self.post("/groups/{}/students/{}".format(group_id, user_id))
254268

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

258272
def group_attach_exercise(self, group_id, exercise_id):
259-
return self.post("/exercises/{}/groups/{}".format(exercise_id, group_id))
273+
return self.post("/exercises/{}/groups/{}"
274+
.format(exercise_id, group_id))
260275

261276
def group_detach_exercise(self, group_id, exercise_id):
262-
return self.delete("/exercises/{}/groups/{}".format(exercise_id, group_id))
277+
return self.delete("/exercises/{}/groups/{}"
278+
.format(exercise_id, group_id))
279+
280+
def set_group_exam_flag(self, group_id, is_exam=True):
281+
return self.post("/groups/{}/exam".format(group_id),
282+
data={"value": is_exam})
263283

264284
# Assignments and related stuff...
265285

@@ -304,18 +324,30 @@ def delete_solution(self, solution_id):
304324

305325
# Shadow Assignments
306326

327+
def create_shadow_assignment(self, group_id):
328+
return self.post("/shadow-assignments/", data={
329+
'groupId': group_id,
330+
})
331+
307332
def get_shadow_assignment(self, assignment_id):
308333
return self.get("/shadow-assignments/{}".format(assignment_id))
309334

310-
def create_shadow_assignment_points(self, assignment_id, user_id, points, note, awarded_at=None):
311-
return self.post("/shadow-assignments/{}/create-points/".format(assignment_id), data={
312-
'userId': user_id,
313-
'points': points,
314-
'note': note,
315-
'awardedAt': awarded_at,
316-
})
317-
318-
def update_shadow_assignment_points(self, points_id, points, note, awarded_at=None):
335+
def update_shadow_assignment(self, assignment_id, data):
336+
return self.post("/shadow-assignments/{}".format(assignment_id),
337+
data=data)
338+
339+
def create_shadow_assignment_points(self, assignment_id, user_id, points,
340+
note, awarded_at=None):
341+
return self.post("/shadow-assignments/{}/create-points/"
342+
.format(assignment_id), data={
343+
'userId': user_id,
344+
'points': points,
345+
'note': note,
346+
'awardedAt': awarded_at,
347+
})
348+
349+
def update_shadow_assignment_points(self, points_id, points, note,
350+
awarded_at=None):
319351
return self.post("/shadow-assignments/points/{}".format(points_id), data={
320352
'points': points,
321353
'note': note,
@@ -358,11 +390,13 @@ def extract_payload(response):
358390
try:
359391
json = response.json()
360392
except JSONDecodeError:
361-
logging.error("Loading JSON response failed, see full response below:")
393+
logging.error(
394+
"Loading JSON response failed, see full response below:")
362395
logging.error(response.text)
363396
raise RuntimeError("Loading JSON response failed")
364397

365398
if not json["success"]:
366-
raise RuntimeError("Received error from API: " + json["error"]["message"])
399+
raise RuntimeError("Received error from API: " +
400+
json["error"]["message"])
367401

368402
return json["payload"]

recodex/plugins/exercises/cli.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def add_localization(api: ApiClient, locale, exercise_id, include_name):
193193

194194

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

199199
preflight = api.presubmit_check(exercise_id, uploaded_files)
@@ -329,7 +329,7 @@ def set_config(api: ApiClient, exercise_id, file_name, useJson):
329329
"""
330330
Load a JSON or YAML from a file and set it as configuration.
331331
"""
332-
with open(file_name, 'r') as stream:
332+
with open(file_name, 'r', encoding="utf-8") as stream:
333333
if useJson:
334334
config = json.load(stream)
335335
else:

recodex/plugins/groups/cli.py

+99-34
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,41 @@ def all(api: ApiClient, useJson, archived):
3434

3535

3636
@cli.command()
37-
@click.argument("group_id")
38-
@click.option("--json/--yaml", "useJson", default=False)
37+
@click.argument("parent_id")
3938
@pass_api_client
39+
def create(api: ApiClient, parent_id):
40+
"""
41+
Create a new group.
42+
"""
43+
parent = api.get_group(parent_id)
44+
data = {
45+
"isPublic": False,
46+
"publicStats": False,
47+
"isOrganizational": False,
48+
"detaining": False,
49+
"externalId": "",
50+
"hasThreshold": False,
51+
"localizedTexts": [],
52+
"noAdmin": False,
53+
}
54+
55+
data_input = sys.stdin.read().strip()
56+
data_json = json.loads(data_input)
57+
for [key, value] in data_json.items():
58+
if key in data:
59+
data[key] = value
60+
61+
data["instanceId"] = parent["privateData"]["instanceId"]
62+
data["parentGroupId"] = parent_id
63+
64+
group = api.create_group(data)
65+
click.echo(group['id'])
66+
67+
68+
@ cli.command()
69+
@ click.argument("group_id")
70+
@ click.option("--json/--yaml", "useJson", default=False)
71+
@ pass_api_client
4072
def detail(api: ApiClient, group_id, useJson):
4173
"""
4274
Read detailed data about given group
@@ -47,61 +79,59 @@ def detail(api: ApiClient, group_id, useJson):
4779
json.dump(group, sys.stdout, sort_keys=True, indent=4)
4880
elif useJson is False:
4981
yaml = YAML(typ="safe")
50-
yaml.dump(group, sys.stdout)
82+
yaml.dump(group, sys.stdout)
5183

5284

53-
@cli.command()
54-
@click.argument("group_id")
55-
@click.argument("user_id")
56-
@pass_api_client
85+
@ cli.command()
86+
@ click.argument("group_id")
87+
@ click.argument("user_id")
88+
@ pass_api_client
5789
def join(api: ApiClient, group_id, user_id):
5890
"""
5991
Add user as a member (student) of a group
6092
"""
6193

6294
api.group_add_student(group_id, user_id)
6395

64-
65-
@cli.command()
66-
@click.argument("group_id")
67-
@click.argument("user_id")
68-
@pass_api_client
69-
def leave(api: ApiClient, group_id, user_id):
70-
"""
96+
@ cli.command()
97+
@ click.argument("group_id")
98+
@ click.argument("user_id")
99+
@ pass_api_client
100+
def leave(api: ApiClient, group_id, user_id):
101+
"""
71102
Remove user (student) from a group
72103
"""
73104

74105
api.group_remove_student(group_id, user_id)
75106

76107

77-
@cli.command()
78-
@click.argument("group_id")
79-
@click.argument("exercise_id")
80-
@pass_api_client
108+
@ cli.command()
109+
@ click.argument("group_id")
110+
@ click.argument("exercise_id")
111+
@ pass_api_client
81112
def attach(api: ApiClient, group_id, exercise_id):
82113
"""
83114
Attach exercise to a group of residence
84115
"""
85116

86117
api.group_attach_exercise(group_id, exercise_id)
87118

88-
89-
@cli.command()
90-
@click.argument("group_id")
91-
@click.argument("exercise_id")
92-
@pass_api_client
93-
def detach(api: ApiClient, group_id, exercise_id):
94-
"""
119+
@ cli.command()
120+
@ click.argument("group_id")
121+
@ click.argument("exercise_id")
122+
@ pass_api_client
123+
def detach(api: ApiClient, group_id, exercise_id):
124+
"""
95125
Detach exercise from a group of residence
96126
"""
97127

98128
api.group_detach_exercise(group_id, exercise_id)
99129

100130

101-
@cli.command()
102-
@click.argument("group_id")
103-
@click.option("--json/--yaml", "useJson", default=None)
104-
@pass_api_client
131+
@ cli.command()
132+
@ click.argument("group_id")
133+
@ click.option("--json/--yaml", "useJson", default=None)
134+
@ pass_api_client
105135
def students(api: ApiClient, group_id, useJson):
106136
"""
107137
List all students of a group.
@@ -118,10 +148,10 @@ def students(api: ApiClient, group_id, useJson):
118148
click.echo("{} {}".format(student["id"], student["fullName"]))
119149

120150

121-
@cli.command()
122-
@click.argument("group_id")
123-
@click.option("--json/--yaml", "useJson", default=None)
124-
@pass_api_client
151+
@ cli.command()
152+
@ click.argument("group_id")
153+
@ click.option("--json/--yaml", "useJson", default=None)
154+
@ pass_api_client
125155
def assignments(api: ApiClient, group_id, useJson):
126156
"""
127157
List all (regular) assignments of a group.
@@ -136,4 +166,39 @@ def assignments(api: ApiClient, group_id, useJson):
136166
else:
137167
for assignment in assignments:
138168
click.echo("{} {}".format(
139-
assignment["id"], get_localized_name(assignment["localizedTexts"])))
169+
assignment["id"], get_localized_name(
170+
assignment["localizedTexts"])))
171+
172+
173+
@ cli.command()
174+
@ click.argument("group_id")
175+
@ click.option("--json/--yaml", "useJson", default=None)
176+
@ pass_api_client
177+
def shadow_assignments(api: ApiClient, group_id, useJson):
178+
"""
179+
List all shadow assignments of a group.
180+
"""
181+
182+
assignments = api.get_group_shadow_assignments(group_id)
183+
if useJson is True:
184+
json.dump(assignments, sys.stdout, sort_keys=True, indent=4)
185+
elif useJson is False:
186+
yaml = YAML(typ="safe")
187+
yaml.dump(assignments, sys.stdout)
188+
else:
189+
for assignment in assignments:
190+
click.echo("{} {}".format(
191+
assignment["id"], get_localized_name(
192+
assignment["localizedTexts"])))
193+
194+
195+
@cli.command()
196+
@click.argument("group_id")
197+
@click.option("--unset", is_flag=True)
198+
@pass_api_client
199+
def enable(api: ApiClient, group_id, unset):
200+
"""
201+
Set (or unset) exam flag of a group.
202+
"""
203+
204+
api.set_group_exam_flag(group_id, not unset)

0 commit comments

Comments
 (0)