Beispiel #1
0
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)
    })
Beispiel #2
0
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)
Beispiel #3
0
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)
Beispiel #4
0
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)
Beispiel #5
0
    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
Beispiel #6
0
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!")
Beispiel #7
0
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()
Beispiel #8
0
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)
Beispiel #9
0
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)
Beispiel #10
0
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)
Beispiel #11
0
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()
Beispiel #12
0
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