Skip to content

Commit

Permalink
Adding exercise cloning command, improving user searching.
Browse files Browse the repository at this point in the history
  • Loading branch information
krulis-martin committed Oct 8, 2023
1 parent 21971d5 commit c30e63a
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 28 deletions.
21 changes: 19 additions & 2 deletions recodex/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def get_reference_solutions(self, exercise_id):
def get_reference_solution_evaluations(self, solution_id):
return self.get("/reference-solutions/{}/submissions".format(solution_id))

def get_reference_solution_files(self, solution_id):
return self.get("/reference-solutions/{}/files".format(solution_id))

def upload_file(self, filename, stream):
return self.post("/uploaded-files", files={"file": (filename, stream)})

Expand All @@ -79,6 +82,11 @@ def create_exercise(self, group_id):
"groupId": group_id
})

def fork_exercise(self, exercise_id, group_id):
return self.post("/exercises/{}/fork".format(exercise_id), data={
"groupId": group_id
})

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

Expand Down Expand Up @@ -199,8 +207,14 @@ def get_user(self, user_id):
def update_user(self, user_id, user_data):
return self.post("/users/{}".format(user_id), data=user_data)

def search_users(self, instance_id, search_string):
return self.get("/users/?filters[instanceId]={}&filters[search]={}".format(instance_id, search_string))["items"]
def search_users(self, instance_id, search_string=None, roles=None):
query = "/users/?filters[instanceId]={}".format(instance_id)
if search_string is not None:
query += "&filters[search]={}".format(search_string)
if roles is not None:
for role in roles:
query += "&filters[roles][]={}".format(role)
return self.get(query)["items"]

def register_user(self, instance_id, email, first_name, last_name, password):
return self.post("/users", data={
Expand Down Expand Up @@ -330,6 +344,9 @@ def get_group_students(self, group_id):
def get_user_solutions(self, assignment_id, user_id):
return self.get("/exercise-assignments/{}/users/{}/solutions".format(assignment_id, user_id))

def download_file(self, file_id, file_name):
return self.download("/uploaded-files/{}/download".format(file_id), file_name)

def download_solution(self, solution_id, file_name):
return self.download("/assignment-solutions/{}/download-solution".format(solution_id), file_name)

Expand Down
81 changes: 69 additions & 12 deletions recodex/plugins/exercises/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import io
import os
from datetime import datetime, timedelta
from tempfile import mkdtemp
import shutil

import click

Expand Down Expand Up @@ -164,17 +166,7 @@ def add_localization(api: ApiClient, locale, exercise_id, include_name):
api.update_exercise(exercise_id, exercise)


@cli.command()
@click.argument("files", nargs=-1)
@click.option("exercise_id", "-e")
@click.option("note", "-n", default="")
@click.option("runtime_environment", "-r", default=None)
@pass_api_client
def add_reference_solution(api: ApiClient, exercise_id, note, runtime_environment, files):
if len(files) == 0:
print('No files given.', file=sys.stderr)
return

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

preflight = api.presubmit_check(exercise_id, uploaded_files)
Expand All @@ -200,7 +192,20 @@ def add_reference_solution(api: ApiClient, exercise_id, note, runtime_environmen
if entry_point is not None:
submit_data["solutionParams"] = {"variables": [{"name": "entry-point", "value": os.path.basename(files[0])}]}

result = api.create_reference_solution(exercise_id, submit_data)
return api.create_reference_solution(exercise_id, submit_data)


@cli.command()
@click.argument("files", nargs=-1)
@click.option("exercise_id", "-e", help="Exercise ID")
@click.option("note", "-n", default="", help="Note associated with the solution")
@click.option("runtime_environment", "-r", default=None, help="Runtime environment ID")
@pass_api_client
def add_reference_solution(api: ApiClient, exercise_id, note, runtime_environment, files):
if len(files) == 0:
print('No files given.', file=sys.stderr)
return
result = _add_reference_solution(api, exercise_id, note, runtime_environment, files)
click.echo(result["referenceSolution"]["id"])


Expand Down Expand Up @@ -401,3 +406,55 @@ def set_ref_solution_visibility(api: ApiClient, ref_solution_id, visibility):
Change visibility of a reference solution.
"""
api.update_reference_solution_visibility(ref_solution_id, int(visibility))


@cli.command()
@click.argument("exercise_id")
@click.argument("group_id")
@click.option('--complete', is_flag=True)
@pass_api_client
def fork(api: ApiClient, exercise_id, group_id, complete):
"""
Copy (fork) given exercise and make the copy resident in specified group.
If complete flag is present, the (public) ref. solutions is copied as well.
"""
res = api.fork_exercise(exercise_id, group_id)
new_id = res["id"]

if complete:
# reference solutions must be copied one by one
solutions = api.get_reference_solutions(exercise_id)
for solution in solutions:
if solution["visibility"] <= 0:
continue # only public solutions are copied

# we need to list and download files first
files = api.get_reference_solution_files(solution["id"])
file_names = []
entry_point = None
tmpdir = mkdtemp() # into a temporary directory
if not tmpdir:
raise Exception("Unable to create a temporary directory.")

for file in files:
if not file["name"]:
continue
path = tmpdir + '/' + file["name"]
if file.get("isEntryPoint", False):
entry_point = path
else:
file_names.append(path)
api.download_file(file["id"], path)

if entry_point is not None:
file_names.insert(0, entry_point) # make sure entry point is the first file on the list

if len(file_names) > 0:
ref_res = _add_reference_solution(api, new_id, solution["description"],
solution["runtimeEnvironmentId"], file_names)
api.update_reference_solution_visibility(
ref_res["referenceSolution"]["id"], solution["visibility"])

shutil.rmtree(tmpdir)

click.echo(new_id)
37 changes: 24 additions & 13 deletions recodex/plugins/users/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ def cli():
def format_user_csv(user):
return {
'id': user['id'],
'title_before': user['name']['degreesBeforeName'],
'title_before': user['name']['titlesBeforeName'],
'first_name': user['name']['firstName'],
'last_name': user['name']['lastName'],
'title_after': user['name']['degreesAfterName'],
'title_after': user['name']['titlesAfterName'],
'avatar_url': user['avatarUrl'],
}

Expand All @@ -43,27 +43,38 @@ def get(api: ApiClient, user_id, useJson):


@cli.command()
@click.argument("search_string")
@click.option('--csv', 'as_csv', is_flag=True, help='Return full records formated into CSV.')
@click.option("--json/--yaml", "useJson", default=None, help='Default is CSV.')
@click.option('--only-active', 'onlyActive', is_flag=True, help='Return full records formated into CSV.')
@click.option("search", "-s", default=None, help="Roles split by comma")
@click.option("roles", "-r", default=None, help="Roles split by comma")
@pass_user_context
@pass_api_client
def search(api: ApiClient, context: UserContext, search_string, as_csv):
def search(api: ApiClient, context: UserContext, search, roles, useJson, onlyActive):
"""
Search for a user
"""
if roles is not None:
roles = roles.split(',')

if as_csv:
users = []
instances_ids = api.get_user(context.user_id)["privateData"]["instancesIds"]
for instance_id in instances_ids:
for user in api.search_users(instance_id, search, roles):
if not onlyActive or user.get("privateData", {}).get("isAllowed", False):
users.append(user)

if useJson is True:
json.dump(users, sys.stdout, sort_keys=True, indent=4)
elif useJson is False:
yaml.dump(users, sys.stdout)
else:
# print CSV header
fieldnames = ['id', 'title_before', 'first_name', 'last_name', 'title_after', 'avatar_url']
csv_writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames)
csv_writer.writeheader()

instances_ids = api.get_user(context.user_id)["privateData"]["instancesIds"]
for instance_id in instances_ids:
for user in api.search_users(instance_id, search_string):
if as_csv:
csv_writer.writerow(format_user_csv(user))
else:
click.echo("{} {}".format(user["fullName"], user["id"]))
for user in users:
csv_writer.writerow(format_user_csv(user))


@cli.command()
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='recodex-cli',
version='0.0.24',
version='0.0.25',
description='ReCodEx CLI',
long_description='A command line frontend to the ReCodEx programmer evaluation system',
classifiers=[
Expand Down

0 comments on commit c30e63a

Please sign in to comment.