def comparison(request, course_key=None, exercise_key=None, ak=None, bk=None, ck=None, course=None, exercise=None): comparison = get_object_or_404(Comparison, submission_a__exercise=exercise, pk=ck, submission_a__student__key=ak, submission_b__student__key=bk) if request.method == "POST": result = "review" in request.POST and comparison.update_review(request.POST["review"]) if request.is_ajax(): return JsonResponse({ "success": result }) reverse_flag = False a = comparison.submission_a b = comparison.submission_b if "reverse" in request.GET: reverse_flag = True a = comparison.submission_b b = comparison.submission_a p_config = provider_config(course.provider) get_submission_text = configured_function(p_config, "get_submission_text") return render(request, "review/comparison.html", { "hierarchy": ((settings.APP_NAME, reverse("index")), (course.name, reverse("course", kwargs={ "course_key": course.key })), (exercise.name, reverse("exercise", kwargs={ "course_key": course.key, "exercise_key": exercise.key })), ("%s vs %s" % (a.student.key, b.student.key), None)), "course": course, "exercise": exercise, "comparisons": exercise.comparisons_for_student(a.student), "comparison": comparison, "reverse": reverse_flag, "a": a, "b": b, "source_a": get_submission_text(a, p_config), "source_b": get_submission_text(b, p_config) })
def build_graph(request, course, course_key): if request.method != "POST" or not request.is_ajax(): return HttpResponseBadRequest() task_state = json.loads(request.body.decode("utf-8")) if task_state["task_id"]: # Task is pending, check state and return result if ready async_result = AsyncResult(task_state["task_id"]) if async_result.ready(): task_state["ready"] = True task_state["task_id"] = None if async_result.state == "SUCCESS": task_state["graph_data"] = async_result.get() async_result.forget() else: task_state["graph_data"] = {} elif not task_state["ready"]: graph_data = json.loads(course.similarity_graph_json or '{}') min_similarity, min_matches = task_state["min_similarity"], task_state["min_matches"] if graph_data and graph_data["min_similarity"] == min_similarity and graph_data["min_matches"] == min_matches: # Graph was already cached task_state["graph_data"] = graph_data task_state["ready"] = True else: # No graph cached, build p_config = provider_config(course.provider) if not p_config.get("async_graph", True): task_state["graph_data"] = graph.generate_match_graph(course.key, float(min_similarity), int(min_matches)) task_state["ready"] = True else: async_task = graph.generate_match_graph.delay(course.key, float(min_similarity), int(min_matches)) task_state["task_id"] = async_task.id return JsonResponse(task_state)
def exercise_settings(request, course_key=None, exercise_key=None, course=None, exercise=None): p_config = provider_config(course.provider) context = { "hierarchy": ( (settings.APP_NAME, reverse("index")), (course.name, reverse("course", kwargs={ "course_key": course.key })), ("%s settings" % (exercise.name), None) ), "course": course, "exercise": exercise, "provider_reload": "full_reload" in p_config, "change_success": set(), "change_failure": {}, } if request.method == "POST": if "save" in request.POST: form = ExerciseForm(request.POST) if form.is_valid(): form.save(exercise) context["change_success"].add("save") elif "override_template" in request.POST: form_template = ExerciseTemplateForm(request.POST) if form_template.is_valid(): form_template.save(exercise) context["change_success"].add("override_template") elif "clear_and_recompare" in request.POST: configured_function(p_config, "recompare")(exercise, p_config) context["change_success"].add("clear_and_recompare") elif "provider_reload" in request.POST: configured_function(p_config, "full_reload")(exercise, p_config) context["change_success"].add("provider_reload") elif "delete_exercise" in request.POST: form = DeleteExerciseFrom(request.POST) if form.is_valid() and form.cleaned_data["name"] == exercise.name: exercise.delete() return redirect("course", course_key=course.key) else: context["change_failure"]["delete_exercise"] = form.cleaned_data["name"] template_source = configured_function(p_config, 'get_exercise_template')(exercise, p_config) if exercise.template_tokens and not template_source: context["template_source_error"] = True context["template_tokens"] = exercise.template_tokens context["template_source"] = '' else: context["template_source"] = template_source context["form"] = ExerciseForm({ "name": exercise.name, "paused": exercise.paused, "tokenizer": exercise.tokenizer, "minimum_match_tokens": exercise.minimum_match_tokens, }) context["form_template"] = ExerciseTemplateForm({ "template": template_source, }) context["form_delete_exercise"] = DeleteExerciseFrom({ "name": '' }) return render(request, "review/exercise_settings.html", context)
def course(request, course_key=None, course=None): context = { "hierarchy": ((settings.APP_NAME, reverse("index")), (course.name, None)), "course": course, "exercises": course.exercises.all(), } if request.method == "POST": # The user can click "Match all unmatched" for a shortcut to match all unmatched submissions for every exercise p_config = provider_config(course.provider) if "match-all-unmatched-for-exercises" in request.POST: configured_function(p_config, 'recompare_unmatched')(course) return redirect("course", course_key=course.key) return render(request, "review/course.html", context)
def handle(self, *args, **options): lock = acquire_lock() if lock is None: logger.info("Cannot get manage lock, another process running.") return start = time.time() for course in Course.objects.filter(archived=False): # Run provider tasks. p_config = provider_config(course.provider) f = configured_function(p_config, "cron") f(course, p_config) invalid_submissions = [] # Tokenize and match new and valid submissions. for submission in Submission.objects.filter( exercise__course=course, exercise__paused=False, tokens__isnull=True): if not tokenize_submission(submission, p_config): invalid_submissions.append(submission) continue if (not match(submission) or (settings.CRON_STOP_SECONDS is not None and time.time() - start > settings.CRON_STOP_SECONDS)): return # Delete all new submissions that could not be tokenized. for submission in invalid_submissions: submission.delete() # Check again for yet unmatched submissions. for submission in Submission.objects.filter( exercise__course=course, exercise__paused=False, max_similarity__isnull=True): if (not match(submission) or (settings.CRON_STOP_SECONDS is not None and time.time() - start > settings.CRON_STOP_SECONDS)): return
def hook_submission(request, course_key=None): """ Receives the hook call for new submission and passes it to the course provider. """ course = get_object_or_404(Course, key=course_key) if course.archived: logger.error("Submission hook failed, archived course %s", course) raise Http404() if request.method == "GET": return HttpResponse("Received hook submission request for course {}, but doing nothing since GET requests are ignored.".format(course)) config = provider_config(course.provider) try: f = configured_function(config, "hook") f(request, course, config) except Exception: logger.exception("Submission hook failed") return HttpResponse("Working on it sire!")
def prepare_submission(submission, matching_start_time=''): if matching_start_time: # The exercise of this submission has been marked for matching, this submission is going directly to matching. submission.matching_start_time = matching_start_time provider_config = config_loaders.provider_config( submission.exercise.course.provider) get_submission_text = config_loaders.configured_function( provider_config, "get_submission_text") submission_text = get_submission_text(submission, provider_config) if submission_text is None: submission.invalid = True submission.save() raise InsertError("Failed to get submission text for submission %s" % submission) tokens, json_indexes = tokenizer.tokenize_submission( submission, submission_text, provider_config) if not tokens: submission.invalid = True submission.save() raise InsertError( "Tokenizer returned an empty token string for submission %s, will not save submission" % submission) submission.tokens = tokens submission.indexes_json = json_indexes # Compute checksum of submitted source code for finding exact character matches quickly # This line will not be reached if submission_text contains data not encodable in utf-8, since it is checked in tokenizer.tokenize_submission submission_hash = hashlib.md5(submission_text.encode("utf-8")) submission.source_checksum = submission_hash.hexdigest() submission.save() # Compute similarity of submitted tokens to exercise template tokens template_comparison = matcher.match_against_template(submission) template_comparison.save()
def recompare_all_unmatched(course_id): course = Course.objects.get(pk=course_id) p_config = config_loaders.provider_config(course.provider) recompare = config_loaders.configured_function(p_config, "recompare") for exercise in course.exercises_with_unmatched_submissions: recompare(exercise, p_config)
def configure_course(request, course_key=None, course=None): context = { "hierarchy": ((settings.APP_NAME, reverse("index")), (course.name, reverse("course", kwargs={"course_key": course.key})), ("Configure", None)), "course": course, "provider_data": [ { "description": "{:s}, all submission data are retrieved from here".format( course.provider_name), "path": settings.PROVIDERS[course.provider].get("host", "UNKNOWN"), }, { "description": "Data providers should POST the IDs of new submissions to this path in order to have them automatically downloaded by Radar", "path": request.build_absolute_uri( reverse("hook_submission", kwargs={"course_key": course.key})), }, { "description": "Login requests using the LTI-protocol should be made to this path", "path": request.build_absolute_uri(reverse("lti_login")), }, ], "errors": [], } # The state of the API read task is contained in this dict pending_api_read = { "task_id": None, "poll_URL": reverse("configure_course", kwargs={"course_key": course.key}), "ready": False, "poll_interval_seconds": 5, "config_type": "automatic" } if request.method == "GET": if "true" in request.GET.get("success", ''): # All done, show success message context["change_success"] = True pending_api_read["json"] = json.dumps(pending_api_read) context["pending_api_read"] = pending_api_read return render(request, "review/configure.html", context) if request.method != "POST": return HttpResponseBadRequest() p_config = provider_config(course.provider) if "create-exercises" in request.POST or "overwrite-exercises" in request.POST: # API data has been fetched in a previous step, now the user wants to add exercises that were shown in the table if "create-exercises" in request.POST: # Pre-configured, read-only table exercises = json.loads(request.POST["exercises-json"]) for exercise_data in exercises: key_str = str(exercise_data["exercise_key"]) exercise = course.get_exercise(key_str) exercise.set_from_config(exercise_data) exercise.save() # Queue fetch and match for all submissions for this exercise full_reload = configured_function(p_config, "full_reload") full_reload(exercise, p_config) elif "overwrite-exercises" in request.POST: # Manual configuration, editable table, overwrite existing checked_rows = (key.split("-", 1)[0] for key in request.POST if key.endswith("enabled")) exercises = ({ "exercise_key": exercise_key, "name": request.POST[exercise_key + "-name"], "template_source": request.POST.get(exercise_key + "-template-source", ''), "tokenizer": request.POST[exercise_key + "-tokenizer"], "minimum_match_tokens": request.POST[exercise_key + "-min-match-tokens"] } for exercise_key in checked_rows) for exercise_data in exercises: key = str(exercise_data["exercise_key"]) course.exercises.filter(key=key).delete() exercise = course.get_exercise(key) exercise.set_from_config(exercise_data) exercise.save() full_reload = configured_function(p_config, "full_reload") full_reload(exercise, p_config) return redirect( reverse("configure_course", kwargs={"course_key": course.key}) + "?success=true") if not request.is_ajax(): return HttpResponseBadRequest("Unknown POST request") pending_api_read = json.loads(request.body.decode("utf-8")) if pending_api_read["task_id"]: # Task is pending, check state and return result if ready async_result = AsyncResult(pending_api_read["task_id"]) if async_result.ready(): pending_api_read["ready"] = True pending_api_read["task_id"] = None if async_result.state == "SUCCESS": exercise_data = async_result.get() async_result.forget() config_table = template_loader.get_template( "review/configure_table.html") exercise_data["config_type"] = pending_api_read["config_type"] pending_api_read["resultHTML"] = config_table.render( exercise_data, request) else: pending_api_read["resultHTML"] = '' return JsonResponse(pending_api_read) if pending_api_read["ready"]: # The client might be polling a few times even after it has received the results return JsonResponse(pending_api_read) # Put full read of provider API on task queue and store the task id for tracking has_radar_config = pending_api_read["config_type"] == "automatic" async_api_read = configured_function(p_config, "async_api_read") pending_api_read["task_id"] = async_api_read(request, course, has_radar_config) return JsonResponse(pending_api_read)
def pair_view_summary(request, course=None, course_key=None, a=None, a_key=None, b=None, b_key=None): authors = {a_key, b_key} a = Student.objects.get(key=a_key, course=course) b = Student.objects.get(key=b_key, course=course) # Get comparisons of authors marked as plagiarized comparisons = (Comparison.objects .filter(submission_a__exercise__course=course) .filter(similarity__gt=0) .select_related("submission_a", "submission_b","submission_a__exercise", "submission_b__exercise", "submission_a__student", "submission_b__student") .filter(Q(submission_a__student__key__in=authors) & Q(submission_b__student__key__in=authors)) .filter(review=settings.REVIEW_CHOICES[4][0])) p_config = provider_config(course.provider) get_submission_text = configured_function(p_config, "get_submission_text") sources = [] # Loop through comparisons and add to sources for n in comparisons: reverse_flag = False student_a = n.submission_a.student.key student_b = n.submission_b.student.key text_a = n.submission_a text_b = n.submission_b submission_text_a = get_submission_text(text_a, p_config) submission_text_b = get_submission_text(text_b, p_config) matches = n.matches_json template_comparisons_a = text_a.template_comparison.matches_json template_comparisons_b = text_b.template_comparison.matches_json indexes_a = text_a.indexes_json indexes_b = text_b.indexes_json exercise = n.submission_a.exercise.name if "reverse" in request.GET: reverse_flag = True text_a = n.submission_b text_b = n.submission_a sources.append({"text_a" : submission_text_a, "text_b" : submission_text_b, "matches" : matches, "templates_a" : template_comparisons_a, "templates_b" : template_comparisons_b, "indexes_a" : indexes_a, "indexes_b" : indexes_b, "reverse_flag" : reverse_flag, "student_a" : student_a, "student_b" : student_b, "exercise" : exercise}) context = { "hierarchy": ( (settings.APP_NAME, reverse("index")), (course.name, reverse("course", kwargs={ "course_key": course.key })), ("%s vs %s" % (a_key, b_key), reverse("pair_view", kwargs={ "course_key": course_key, "a_key": a_key, "b_key": b_key })), ("Summary", None) ), "course": course, "a" : a_key, "b" : b_key, "a_object" : a, "b_object" : b, "sources": sources, "time": now, } return render(request, "review/pair_view_summary.html", context)
def create_submission(task, submission_key, course_key, submission_api_url, matching_start_time=''): """ Fetch submission data for a new submission with provider key submission_key from a given API url, create new submission, and tokenize submission content. If matching_start_time timestamp is given, it will be written into the submission object before writing. """ course = Course.objects.get(key=course_key) if Submission.objects.filter(key=submission_key).exists(): # raise ProviderTaskError("Submission with key %s already exists, will not create a duplicate." % submission_key) write_error( "Submission with key %s already exists, will not create a duplicate." % submission_key, "create_submission") return # We need someone with a token to the A+ API. api_client = aplus.get_api_client(course) # Request data from provider API try: data = api_client.load_data(submission_api_url) except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout) as err: logger.exception("Unable to read data from the API.") data = None del api_client if not data: logger.error( "API returned nothing for submission %s, skipping submission", submission_key) return exercise_data = data["exercise"] # Check if exercise is configured for Radar # If not, and there is no manually configured exercise in the database, skip radar_config = aplus.get_radar_config(exercise_data) if radar_config is None and not course.has_exercise( str(exercise_data["id"])): return # Get or create exercise configuration exercise = course.get_exercise(str(exercise_data["id"])) if exercise.name == "unknown": # Get template source try: radar_config["template_source"] = radar_config[ "get_template_source"]() except Exception as e: write_error( "Error while attempting to get template source for submission %s\n%s" % (submission_key, str(e)), "create_submission") radar_config["template_source"] = '' exercise.set_from_config(radar_config) exercise.save() del radar_config # A+ allows more than one submitter for a single submission # TODO: if there are more than one unique submitters, # set as approved plagiate and show this in the UI ## for submitter_id in _decode_students(data["submitters"]): submitter_id = "_".join(aplus._decode_students(data["submitters"])) student = course.get_student(str(submitter_id)) submission = Submission.objects.create( key=submission_key, exercise=exercise, student=student, provider_url=data["html_url"], provider_submission_time=data["submission_time"], grade=data["grade"], ) if matching_start_time: # The exercise of this submission has been marked for matching, this submission is going directly to matching. submission.matching_start_time = matching_start_time provider_config = config_loaders.provider_config(course.provider) get_submission_text = config_loaders.configured_function( provider_config, "get_submission_text") submission_text = get_submission_text(submission, provider_config) if submission_text is None: # raise ProviderTaskError("Failed to get submission text for submission %s" % submission) submission.invalid = True submission.save() write_error( "Failed to get submission text for submission %s" % submission, "create_submission") return tokens, json_indexes = tokenizer.tokenize_submission( submission, submission_text, provider_config) if not tokens: # raise ProviderTaskError("Tokenizer returned an empty token string for submission %s, will not save submission" % submission_key) submission.invalid = True submission.save() write_error( "Tokenizer returned an empty token string for submission %s, will not save submission" % submission_key, "create_submission") return submission.tokens = tokens submission.indexes_json = json_indexes # Compute checksum of submitted source code for finding exact character matches quickly # This line will not be reached if submission_text contains data not encodable in utf-8, since it is checked in tokenizer.tokenize_submission submission_hash = hashlib.md5(submission_text.encode("utf-8")) submission.source_checksum = submission_hash.hexdigest() submission.save() # Compute similarity of submitted tokens to exercise template tokens template_comparison = matcher.match_against_template(submission) template_comparison.save()
def get_full_course_config(api_user_id, course_id, has_radar_config=True): """ Perform full traversal of the exercises list of a course in the A+ API. The API access token of a RadarUser with the given id will be used for access. If has_radar_config is given and False, all submittable exercises will be retireved. Else, only exercises defined with Radar configuration data will be retrieved. """ result = {} course = Course.objects.get(pk=course_id) api_user = RadarUser.objects.get(pk=api_user_id) p_config = config_loaders.provider_config(course.provider) client = api_user.get_api_client(course.namespace) try: if client is None: raise APIAuthException response = client.load_data(course.url) if response is None: raise APIAuthException exercises = response.get("exercises", []) except APIAuthException: exercises = [] result.setdefault("errors", []).append( "This user does not have correct credentials to use the API of %s" % repr(course)) if not exercises: result.setdefault("errors", []).append("No exercises found for %s" % repr(course)) if has_radar_config: # Exercise API data is expected to contain Radar configurations # Partition all radar configs into unseen and existing exercises new_exercises, old_exercises = [], [] for radar_config in aplus.leafs_with_radar_config(exercises): radar_config["template_source"] = radar_config[ "get_template_source"]() # We got the template and lambdas are not serializable so we delete the getter del radar_config["get_template_source"] if course.has_exercise(radar_config["exercise_key"]): old_exercises.append(radar_config) else: new_exercises.append(radar_config) result["exercises"] = { "old": old_exercises, "new": new_exercises, "new_json": json.dumps(new_exercises), } else: # Exercise API data is not expected to contain Radar data, choose all submittable exercises and patch them with a default Radar config new_exercises = [] # Note that the type of 'exercise' is AplusApiDict for exercise in aplus.submittable_exercises(exercises): # Avoid overwriting exercise_info if it is defined patched_exercise_info = dict(exercise["exercise_info"] or {}, radar={ "tokenizer": "skip", "minimum_match_tokens": 15 }) exercise.add_data({"exercise_info": patched_exercise_info}) radar_config = aplus.get_radar_config(exercise) if radar_config: radar_config["template_source"] = radar_config[ "get_template_source"]() del radar_config["get_template_source"] new_exercises.append(radar_config) result["exercises"] = { "new": new_exercises, "tokenizer_choices": settings.TOKENIZER_CHOICES } return result