예제 #1
0
def index(request):
    try:
        exams = all_exams()

        if not is_teacher(request.user):
            exams = [
                exam for exam in exams
                if request.user.username in exam.students
            ]

        return render(request, 'examinator/index.html', {'exams': exams})
    except ExamException as e:
        if is_teacher(request.user):
            return HttpResponse(e)
        return HttpResponse("Error in exam configuration")
예제 #2
0
def pipeline_status(request, submit_id):
    submit = get_object_or_404(Submit, id=submit_id)

    if not is_teacher(request.user) and request.user != submit.student:
        raise PermissionDenied()

    s = get_submit_job_status(submit.jobid)
    return JsonResponse({
        'status':
        s.status,
        'finished':
        s.finished,
        'message':
        s.message if is_teacher(request.user) else '',
    })
예제 #3
0
def submit_source(request, submit_id, path):
    submit = get_object_or_404(Submit, id=submit_id)
    if not is_teacher(
            request.user) and request.user.username != submit.student.username:
        raise PermissionDenied()

    for s in submit.all_sources():
        if s.virt == path:
            path = s.phys
            mime = mimedetector.from_file(s.phys)
            if request.GET.get('convert', False):
                key = hashlib.sha1(
                    f"{submit_id}{path}".encode('utf-8')).hexdigest()
                path = os.path.join(BASE_DIR, "cache", "media", key[0], key[1],
                                    key)
                os.makedirs(os.path.dirname(path), exist_ok=True)
                if not os.path.exists(path):
                    if mime.startswith("image/"):
                        subprocess.check_call(
                            ["/usr/bin/convert", s.phys, f"WEBP:{path}"])
                    else:
                        raise Exception(f"Unsuppored mime {mime} for convert")
                mime = mimedetector.from_file(path)

            with open(path, 'rb') as f:
                res = HttpResponse(f)
                if mime:
                    res['Content-type'] = mime
                res['Accept-Ranges'] = 'bytes'
                return res
    raise Http404()
예제 #4
0
def submit_download(request, assignment_id, login, submit_num):
    submit = get_object_or_404(Submit,
                               assignment_id=assignment_id,
                               student__username=login,
                               submit_num=submit_num)

    if 'token' in request.GET:
        token = signing.loads(request.GET['token'], max_age=3600)
        if token.get('submit_id') != submit.id:
            raise PermissionDenied()
    elif not request.user.is_authenticated:
        return redirect(f'{settings.LOGIN_URL}?next={request.path}')
    elif not is_teacher(
            request.user) and request.user.username != submit.student.username:
        raise PermissionDenied()

    with io.BytesIO() as f:
        with tarfile.open(fileobj=f, mode="w:gz") as tar:
            for source in submit.all_sources():
                info = tarfile.TarInfo(source.virt)
                info.size = os.path.getsize(source.phys)
                with open(source.phys, "rb") as fr:
                    tar.addfile(info, fileobj=fr)

        f.seek(0)
        return file_response(f, f"{login}_{submit_num}.tar.gz",
                             "application/tar")
예제 #5
0
def raw_result_content(request, submit_id, test_name, result_type, file):
    submit = get_object_or_404(Submit, pk=submit_id)

    if submit.student_id != request.user.id and not is_teacher(request.user):
        raise PermissionDenied()

    for pipe in get(submit)['results']:
        for test in pipe.tests:
            if test.name == test_name:
                if file in test.files:
                    if result_type in test.files[file]:
                        if result_type == "html":
                            return HttpResponse(
                                test.files[file][result_type].read(),
                                content_type='text/html')
                        else:
                            file_content = test.files[file][result_type].open(
                                'rb').read()
                            file_name = f"{result_type}-{file}"
                            extension = os.path.splitext(file)[1]
                            file_mime = mimedetector.from_buffer(file_content)

                            if extension in DIRECT_SHOW_EXTENSIONS and file_mime:
                                return HttpResponse(file_content,
                                                    content_type=file_mime)
                            return file_response(file_content, file_name,
                                                 "text/plain")
    raise Http404()
예제 #6
0
def evaluate_submit(request, submit, meta=None):
    submit_url = request.build_absolute_uri(
        reverse('task_detail',
                kwargs={
                    'login': submit.student.username,
                    'assignment_id': submit.assignment_id,
                    'submit_num': submit.submit_num,
                }))
    task_url = request.build_absolute_uri(
        reverse('teacher_task_tar',
                kwargs={
                    'task_id': submit.assignment.task_id,
                }))
    token = signing.dumps({
        'submit_id': submit.id,
        'task_id': submit.assignment.task_id,
    })

    meta = {
        **get_meta(submit.student.username), 'before_announce':
        not is_teacher(submit.student)
        and submit.assignment.assigned > timezone.now(),
        **(meta if meta else {})
    }

    task_dir = os.path.join(BASE_DIR, "tasks", submit.assignment.task.code)
    task = TestSet(task_dir, meta)

    return django_rq.get_queue(task.queue).enqueue(evaluate_job,
                                                   submit_url,
                                                   task_url,
                                                   token,
                                                   meta,
                                                   job_timeout=task.timeout)
예제 #7
0
파일: views.py 프로젝트: mrlvsb/kelvin
def show(request, survey_file):
    try:
        conf = survey_read(survey_file, request.user)

        editable = 'editable' in conf and conf['editable']
        answered = Answer.objects.filter(student=request.user, survey_name=survey_file)

        if answered and not editable:
            return render(request, 'survey.html', {'survey': conf})

        defaults = None
        if answered:
            defaults = json.loads(answered[0].answers)

        form = create_survey_form(request, conf, defaults)
        if request.method == 'POST' and form.is_valid():
            Answer.objects.update_or_create(
                    student_id=request.user.id,
                    survey_name=conf['name'],
                    defaults={
                        "answers": json.dumps(form.cleaned_data)
                    }
            )
            return redirect(request.path_info)

        return render(request, "survey.html", {
            "form": form,
            "survey": conf,
        })
    except FileNotFoundError as e:
        raise Http404()
    except SurveyError as e:
        if not is_teacher(request.user):
            raise e
        return HttpResponse(e)
예제 #8
0
def check_is_task_accessible(request, task):
    if not is_teacher(request.user):
        assigned_tasks = AssignedTask.objects.filter(
            task_id=task.id,
            clazz__students__id=request.user.id,
            assigned__lte=datetime.now())
        if not assigned_tasks:
            raise PermissionDenied()
예제 #9
0
파일: common.py 프로젝트: mrlvsb/kelvin
def template_context(request):
    return {
        'is_teacher':
        is_teacher(request.user),
        'vapid_public_key':
        getattr(settings, 'WEBPUSH_SETTINGS', {}).get('VAPID_PUBLIC_KEY', ''),
        'webpush_save_url':
        reverse('save_webpush_info'),
    }
예제 #10
0
def show_upload(request, exam_id, student, filename):
    if not is_teacher(request.user) and student != request.user.username:
        return HttpResponseForbidden()

    if '..' in exam_id or '..' in filename:
        return HttpResponseForbidden()

    with open(os.path.join("exams", exam_id, student, "uploads", filename),
              "rb") as f:
        resp = HttpResponse(f)
        resp['Content-Type'] = 'application/octet-stream'
        return resp
예제 #11
0
파일: views.py 프로젝트: mrlvsb/kelvin
def survey_read(path, user):
    try:
        with open(os.path.join(base, f"{path}.yaml")) as f:
            conf = yaml.load(f.read(), Loader=yaml.SafeLoader)
            conf['name'] = path
            if is_teacher(user) or ('active' in conf and conf['active']):
                if 'questions' not in conf or not isinstance(conf['questions'], list):
                    raise SurveyError(f"Survey '{path}' does not contain list of questions in yaml")
                return conf
    except FileNotFoundError as e:
        raise e
    except Exception as e:
        raise SurveyError(e)
예제 #12
0
def raw_test_content(request, task_name, test_name, file):
    task = get_object_or_404(Task, code=task_name)

    username = request.user.username
    if is_teacher(request.user) and 'student' in request.GET:
        username = request.GET['student']

    tests = create_taskset(task, username)

    for test in tests:
        if test.name == test_name:
            if file in test.files:
                return file_response(test.files[file].open('rb'),
                                     f"{test_name}.{file}", "text/plain")
    raise Http404()
예제 #13
0
def teacher_task_tar(request, task_id):
    task = get_object_or_404(Task, id=task_id)

    if 'token' in request.GET:
        token = signing.loads(request.GET['token'], max_age=3600)
        if token.get('task_id') != task_id:
            raise PermissionDenied()
    elif not is_teacher(request.user):
        raise PermissionDenied()

    f = tempfile.TemporaryFile()
    with tarfile.open(fileobj=f, mode='w') as tar:
        tar.add(task.dir(), '')
    f.seek(0, io.SEEK_SET)

    res = FileResponse(f)
    res['Content-Type'] = 'application/x-tar'
    return res
예제 #14
0
파일: views.py 프로젝트: mrlvsb/kelvin
def info(request):
    res = {}
    res['user'] = {
        'id': request.user.id,
        'username': request.user.username,
        'name': request.user.get_full_name(),
        'teacher': is_teacher(request.user),
        'is_superuser': request.user.is_superuser,
    }

    semester = current_semester()
    res['semester'] = {
        'begin': semester.begin,
        'year': semester.year,
        'winter': semester.winter,
        'abbr': str(semester),
    }

    return JsonResponse(res)
예제 #15
0
def tar_test_data(request, task_name):
    task = get_object_or_404(Task, code=task_name)
    check_is_task_accessible(request, task)

    username = request.user.username
    if is_teacher(request.user) and 'student' in request.GET:
        username = request.GET['student']

    tests = create_taskset(task, username)

    with io.BytesIO() as f:
        with tarfile.open(fileobj=f, mode="w:gz") as tar:
            for test in tests:
                for file_path in test.files:
                    test_file = test.files[file_path]
                    info = tarfile.TarInfo(os.path.join(test.name, file_path))
                    info.size = test_file.size()
                    tar.addfile(info, fileobj=test_file.open('rb'))

        f.seek(0)
        return file_response(f, f"{task_name}.tar.gz", "application/tar")
예제 #16
0
파일: common.py 프로젝트: mrlvsb/kelvin
def index(request):
    if is_teacher(request.user):
        return ui(request)
    return student_index(request)
예제 #17
0
def submit_diff(request, login, assignment_id, submit_a, submit_b):
    submit = get_object_or_404(Submit,
                               assignment_id=assignment_id,
                               student__username=login,
                               submit_num=submit_a)

    if not is_teacher(
            request.user) and request.user.username != submit.student.username:
        raise PermissionDenied()

    base_dir = os.path.dirname(submit.dir())
    dir_a = os.path.join(base_dir, str(submit_a))
    dir_b = os.path.join(base_dir, str(submit_b))

    files_a = os.listdir(dir_a)
    files_b = os.listdir(dir_b)

    excludes = []
    for root, subdirs, files in [*os.walk(dir_a), *os.walk(dir_b)]:
        for f in files:
            path = os.path.join(root, f)
            mime = mimedetector.from_file(path)
            if mime and not mime.startswith('image/') and not is_file_small(
                    path):
                excludes.append('--exclude')

                p = os.path.relpath(path, base_dir).split('/')[1:]
                excludes.append(os.path.join(*p))

    def get_patch(p1, p2):
        # python3.7 does not support errors on TemporaryFile
        with tempfile.NamedTemporaryFile('r') as diff:
            subprocess.Popen(["diff", "-ruiwN", *excludes] + [p1, p2],
                             cwd=base_dir,
                             stdout=diff).wait()
            with open(diff.name, errors='ignore') as out:
                return out.read()

    # TODO: find better diffing tool that handles file renames
    if len(files_a) == 1 and os.path.isfile(
            files_a[0]) and len(files_b) == 1 and os.path.isfile(files_b[0]):
        with tempfile.TemporaryDirectory() as p1, tempfile.TemporaryDirectory(
        ) as p2:
            with open(os.path.join(p1, "main.c"), 'w') as out:
                with open(os.path.join(dir_a, files_a[0]),
                          errors='ignore') as inp:
                    out.write(inp.read())
            with open(os.path.join(p2, "main.c"), 'w') as out:
                with open(os.path.join(dir_b, files_b[0]),
                          errors='ignore') as inp:
                    out.write(inp.read())

            out = get_patch(p1, p2)
            out = re.sub(r'^(---|\+\+\+) /tmp/[^/]+/', '\\1 ', out, flags=re.M)
    else:
        out = get_patch(str(submit_a), str(submit_b))
        out = re.sub(r'^(---|\+\+\+) [0-9]+/', '\\1 ', out, flags=re.M)

    out = "\n".join([
        line for line in out.split("\n") if not line.startswith('Binary file')
    ])
    resp = HttpResponse(out)
    resp['Content-Type'] = 'text/x-diff'
    return resp
예제 #18
0
def task_detail(request, assignment_id, submit_num=None, login=None):
    submits = Submit.objects.filter(
        assignment__pk=assignment_id, ).order_by('-id')

    if is_teacher(request.user):
        submits = submits.filter(student__username=login)
    else:
        submits = submits.filter(student__pk=request.user.id)
        if login != request.user.username:
            raise PermissionDenied()

    assignment = get_object_or_404(AssignedTask, id=assignment_id)
    testset = create_taskset(assignment.task,
                             login if login else request.user.username)
    is_announce = False
    if (assignment.assigned > datetime.now()
            or not assignment.clazz.students.filter(
                username=request.user.username)) and not is_teacher(
                    request.user):
        is_announce = True
        if not assignment.task.announce:
            raise Http404()

    data = {
        # TODO: task and deadline can be combined into assignment ad deal with it in template
        'task':
        assignment.task,
        'assigned':
        assignment.assigned if is_announce else None,
        'deadline':
        assignment.deadline,
        'submits':
        submits,
        'text':
        testset.load_readme().announce
        if is_announce else testset.load_readme(),
        'inputs':
        None if is_announce else testset,
        'max_inline_content_bytes':
        MAX_INLINE_CONTENT_BYTES,
        'has_pipeline':
        bool(testset.pipeline),
        'upload':
        not is_teacher(request.user) or request.user.username == login,
    }

    current_submit = None
    if submit_num:
        try:
            current_submit = submits.get(submit_num=submit_num)
        except Submit.DoesNotExist:
            raise Http404()
    elif submits:
        current_submit = submits[0]
        return redirect(
            reverse('task_detail',
                    kwargs={
                        'assignment_id': current_submit.assignment_id,
                        'submit_num': current_submit.submit_num,
                        'login': current_submit.student.username,
                    }))

    if current_submit:
        data = {**data, **get(current_submit)}
        data['comment_count'] = current_submit.comment_set.count()

        moss_res = moss_result(current_submit.assignment.task.id)
        if moss_res and (is_teacher(request.user)
                         or moss_res.opts.get('show_to_students', False)):
            svg = moss_res.to_svg(login=current_submit.student.username,
                                  anonymize=not is_teacher(request.user))
            if svg:
                data['has_pipeline'] = True

                res = PipeResult("plagiarism")
                res.title = "Plagiarism checker"

                prepend = ""
                if is_teacher(request.user):
                    if not moss_res.opts.get('show_to_students', False):
                        prepend = "<div class='text-muted'>Not shown to students</div>"
                    prepend += f'<a href="/teacher/task/{current_submit.assignment.task_id}/moss">Change thresholds</a>'

                res.html = f"""
                    {prepend}
                    <style>
                    #plagiarism svg {{
                        width: 100%;
                        height: 300px;
                        border: 1px solid rgba(0,0,0,.125);
                    }}
                    </style>
                    <div id="plagiarism">{svg}</div>
                    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/svg-pan-zoom.min.js"></script>
                    <script>
                        document.addEventListener('DOMContentLoaded', () => {{
                            const observer = new MutationObserver((changes) => {{
                                if(changes[0].target.classList.contains('active')) {{
                                    svgPanZoom('#plagiarism svg')
                                }}
                            }});
                            observer.observe(document.querySelector('#plagiarism svg').closest('.tab-pane'), {{
                                attributeFilter: ['class']
                            }});
                        }});
                    </script>
                """
                data['results'].pipelines = [res] + data['results'].pipelines

        submit_nums = sorted(submits.values_list('submit_num', flat=True))
        current_idx = submit_nums.index(current_submit.submit_num)
        if current_idx - 1 >= 0:
            data['prev_submit'] = submit_nums[current_idx - 1]
        if current_idx + 1 < len(submit_nums):
            data['next_submit'] = submit_nums[current_idx + 1]

        data['total_submits'] = submits.count()
        data['late_submit'] = assignment.deadline and submits.order_by(
            'id').reverse()[0].created_at > assignment.deadline
        data['diff_versions'] = [(s.submit_num, s.created_at)
                                 for s in submits.order_by('id')]
        data['job_status'] = not get_submit_job_status(
            current_submit.jobid).finished

    if request.method == 'POST':
        s = Submit()
        s.student = request.user
        s.assignment = assignment
        s.submit_num = Submit.objects.filter(
            assignment__id=s.assignment.id,
            student__id=request.user.id).count() + 1

        solutions = request.FILES.getlist('solution')
        tmp = request.POST.get('paths', None)
        if tmp:
            paths = [f.rstrip('\r') for f in tmp.split('\n') if f.rstrip('\r')]
        else:
            paths = [f.name for f in solutions]

        upload_submit_files(s, paths, solutions)

        # we need submit_id before putting the job to the queue
        s.save()
        s.jobid = evaluate_submit(request, s).id
        s.save()

        # delete previous notifications
        Notification.objects.filter(
            action_object_object_id__in=[str(s.id) for s in submits],
            action_object_content_type=ContentType.objects.get_for_model(
                Submit),
            verb='submitted',
        ).delete()

        if not is_teacher(request.user):
            notify.send(
                sender=request.user,
                recipient=[assignment.clazz.teacher],
                verb='submitted',
                action_object=s,
                important=any([s.assigned_points is not None
                               for s in submits]),
            )

        return redirect(
            reverse('task_detail',
                    kwargs={
                        'login': s.student.username,
                        'assignment_id': s.assignment.id,
                        'submit_num': s.submit_num
                    }) + '#result')
    return render(request, 'web/task_detail.html', data)
예제 #19
0
def task_asset(request, task_name, path):
    task = get_object_or_404(Task, code=task_name)
    try:
        check_is_task_accessible(request, task)
    except PermissionDenied:
        if not path.split('/')[-1].startswith('announce.'):
            raise PermissionDenied()

    deny_files = ['config.yml', 'script.py', 'solution.c', 'solution.cpp']
    if '..' in path or (path in deny_files and not is_teacher(request.user)):
        raise PermissionDenied()

    system_path = os.path.join("tasks", task_name, path)
    if request.method not in ['HEAD', 'GET']:
        if not is_teacher(request.user):
            raise PermissionDenied()

        if request.method == 'PUT':
            os.makedirs(os.path.dirname(system_path), exist_ok=True)
            with open(system_path, 'wb') as f:
                f.write(request.body)

            if path == 'readme.md':
                readme = load_readme(task.code)
                task.name = readme.name
                if not task.name:
                    task.name = task.code
                task.announce = True if readme.announce else False
                task.save()
            return HttpResponse(status=204)
        elif request.method == 'DELETE':
            os.unlink(system_path)
            return HttpResponse(status=204)
        elif request.method == 'MOVE':
            dst = request.headers['Destination']
            if '..' in dst:
                raise PermissionDenied()
            system_dst = os.path.join("tasks", task_name, dst.lstrip('/'))
            os.makedirs(os.path.dirname(system_dst), exist_ok=True)
            shutil.move(system_path, system_dst)
            return HttpResponse(status=204)
        else:
            return HttpResponseBadRequest()

    try:
        with open(system_path, 'rb') as f:
            resp = HttpResponse(f)
            mime = mimedetector.from_file(system_path)
            if system_path.endswith('.js'):
                mime = 'text/javascript'
            elif system_path.endswith('.wasm'):
                mime = 'application/wasm'
            if mime:
                resp['Content-Type'] = f"{mime};charset=utf-8"
            return resp
    except FileNotFoundError as e:
        archive_ext = '.tar.gz'
        if system_path.endswith(archive_ext):
            directory = system_path[:-len(archive_ext)]
            if os.path.isdir(directory):
                with io.BytesIO() as f:
                    with tarfile.open(fileobj=f, mode="w:gz") as tar:
                        tar.add(directory, recursive=True, arcname='')
                    f.seek(0)
                    return file_response(f, os.path.basename(system_path),
                                         "application/tar")
        raise Http404()
예제 #20
0
def submit_comments(request, assignment_id, login, submit_num):
    submit = get_object_or_404(Submit,
                               assignment_id=assignment_id,
                               student__username=login,
                               submit_num=submit_num)

    if not is_teacher(
            request.user) and request.user.username != submit.student.username:
        raise PermissionDenied()

    submits = []
    for s in Submit.objects.filter(
            assignment_id=assignment_id,
            student__username=login).order_by('submit_num'):
        submits.append({
            'num': s.submit_num,
            'submitted': s.created_at,
            'points': s.assigned_points,
        })

    def get_comment_type(comment):
        if comment.author == comment.submit.student:
            return 'student'
        return 'teacher'

    notifications = {
        c.action_object.id: c
        for c in Notification.objects.filter(
            target_object_id=submit.id,
            target_content_type=ContentType.objects.get_for_model(Submit))
    }

    def dump_comment(comment):
        notification = notifications.get(comment.id, None)
        unread = False
        notification_id = None
        if notification:
            unread = notification.unread
            notification_id = notification.id

        return {
            'id': comment.id,
            'author': comment.author.get_full_name(),
            'author_id': comment.author.id,
            'text': comment.text,
            'can_edit': comment.author == request.user,
            'type': get_comment_type(comment),
            'unread': unread,
            'notification_id': notification_id,
        }

    if request.method == 'POST':
        data = json.loads(request.body)
        comment = Comment()
        comment.submit = submit
        comment.author = request.user
        comment.text = data['text']
        comment.source = data.get('source', None)
        comment.line = data.get('line', None)
        comment.save()

        notify.send(
            sender=request.user,
            recipient=comment_recipients(submit, request.user),
            verb='added new',
            action_object=comment,
            target=submit,
            public=False,
            important=True,
        )
        return JsonResponse({**dump_comment(comment), 'unread': True})
    elif request.method == 'PATCH':
        data = json.loads(request.body)
        comment = get_object_or_404(Comment, id=data['id'])

        if comment.author != request.user:
            raise PermissionDenied()

        Notification.objects.filter(
            action_object_object_id=comment.id,
            action_object_content_type=ContentType.objects.get_for_model(
                Comment)).delete()

        if not data['text']:
            comment.delete()
            return HttpResponse('{}')
        else:
            if comment.text != data['text']:
                comment.text = data['text']
                comment.save()

                notify.send(
                    sender=request.user,
                    recipient=comment_recipients(submit, request.user),
                    verb='updated',
                    action_object=comment,
                    target=submit,
                    public=False,
                    important=True,
                )
            return JsonResponse(dump_comment(comment))

    result = {}
    for source in submit.all_sources():
        mime = mimedetector.from_file(source.phys)
        if mime and mime.startswith('image/'):
            SUPPORTED_IMAGES = [
                'image/png',
                'image/jpeg',
                'image/gif',
                'image/webp',
                'image/svg+xml',
            ]

            result[source.virt] = {
                'type':
                'img',
                'path':
                source.virt,
                'src':
                reverse('submit_source', args=[submit.id, source.virt]) +
                ('?convert=1' if mime not in SUPPORTED_IMAGES else ''),
            }
        elif mime and mime.startswith("video/"):
            name = ('.'.join(source.virt.split('.')[:-1]))
            if name not in result:
                result[name] = {
                    'type': 'video',
                    'path': name,
                    'sources': [],
                }
            result[name]['sources'].append(
                reverse('submit_source', args=[submit.id, source.virt]))
        else:
            content = ''
            content_url = None
            error = None

            try:
                if is_file_small(source.phys):
                    with open(source.phys) as f:
                        content = f.read()
                else:
                    content_url = reverse("submit_source",
                                          kwargs=dict(submit_id=submit.id,
                                                      path=source.virt))
            except UnicodeDecodeError:
                error = "The file contains binary data or is not encoded in UTF-8"
            except FileNotFoundError:
                error = "source code not found"

            result[source.virt] = {
                'type': 'source',
                'path': source.virt,
                'content': content,
                'content_url': content_url,
                'error': error,
                'comments': {},
            }

    # add comments from pipeline
    resultset = get(submit)
    for pipe in resultset['results']:
        for source, comments in pipe.comments.items():
            for comment in comments:
                try:
                    line = min(result[source]['content'].count('\n'),
                               comment['line']) - 1
                    if not any(
                            filter(
                                lambda c: c['text'] == comment['text'],
                                result[source]['comments'].setdefault(
                                    line, []))):
                        result[source]['comments'].setdefault(line, []).append(
                            {
                                'id': -1,
                                'author': 'Kelvin',
                                'text': comment['text'],
                                'can_edit': False,
                                'type': 'automated',
                                'url': comment.get('url', None),
                            })
                except KeyError as e:
                    logging.exception(e)

    summary_comments = []
    for comment in Comment.objects.filter(submit_id=submit.id).order_by('id'):
        try:
            if not comment.source:
                summary_comments.append(dump_comment(comment))
            else:
                max_lines = result[comment.source]['content'].count('\n')
                line = 0 if comment.line > max_lines else comment.line
                result[comment.source]['comments'].setdefault(
                    comment.line - 1, []).append(dump_comment(comment))
        except KeyError as e:
            logging.exception(e)

    priorities = {
        'video': 0,
        'img': 1,
        'source': 2,
    }
    return JsonResponse({
        'sources':
        sorted(result.values(),
               key=lambda f: (priorities[f['type']], f['path'])),
        'summary_comments':
        summary_comments,
        'submits':
        submits,
        'current_submit':
        submit.submit_num,
        'deadline':
        submit.assignment.deadline,
    })