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")
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 '', })
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()
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")
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()
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)
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)
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()
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'), }
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
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)
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()
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
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)
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")
def index(request): if is_teacher(request.user): return ui(request) return student_index(request)
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
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)
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()
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, })