commit 9c0123cbf2a0fda0873ce50129233934c29feb2b Author: Egor Matveev Date: Sun Jul 11 10:28:12 2021 +0300 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6cafb14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ +# Django # +*.log +*.pot +*.pyc +__pycache__ +db.sqlite3 +media +data + +# Backup files # +*.bak + +# If you are using PyCharm # +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/gradle.xml +.idea/**/libraries +*.iws /out/ + +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Sublime Text # +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..238a492 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/../../../../../../:\Users\79175\Desktop\Sprint_server\.idea/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/Sprint_server.iml b/.idea/Sprint_server.iml new file mode 100644 index 0000000..955dc7e --- /dev/null +++ b/.idea/Sprint_server.iml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3cdc6ae --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..86ee15e --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a2e120d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9d6d6ae --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..42c9b4c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Django", + "type": "python", + "request": "launch", + "program": ".\\manage.py", + "args": [ + "runserver" + ], + "django": true + }, + { + "name": "Python: Текущий файл", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": [ + "runserver" + ] + } + ] +} \ No newline at end of file diff --git a/Main/Tester.py b/Main/Tester.py new file mode 100644 index 0000000..73febae --- /dev/null +++ b/Main/Tester.py @@ -0,0 +1,243 @@ +from os import listdir, mkdir +from os.path import basename, isdir +from shutil import rmtree, copyfile +from threading import Thread +from xml.dom.minidom import parse + +from Main.models import * +from .main import solution_path + + +def start_new(host): + in_queue = list(Solution.objects.filter(result='IN QUEUE')) + if in_queue: + sol = in_queue[0] + for s in in_queue: + dif = s.task.block.priority * 10 + s.task.priority - sol.task.block.priority * 10 - sol.task.priority + if dif > 0: + sol = s + elif dif == 0 and s.id < sol.id: + sol = s + Tester(sol, host).test() + + +def is_project(path): + return any([x.endswith('.csproj') for x in listdir(path)]) + + +def get_node_value(element): + return element[0].firstChild.nodeValue + + +def nunit_path(working_dir): + return '..{}'.format(sep) * len(working_dir.split(sep)) + 'nunit_console{}nunit3-console.exe'.format(sep) + + +class Tester: + + def __init__(self, solution, host): + self.solution = solution + self.host = host + self.working_dir = '' + self.files = [] + + # функция компиляции + def build(self, path): + # решение для UNIX + # shell('msbuild ' + path + ' /p:Configuration=Debug') + + # решение для Windows + cmd = 'dotnet build {} -o {}\\bin\\Debug'.format(path, path) + with self.solution.log_fs as fs: + shell(cmd, fs) + + def build_and_copy(self, path, working_dir): + if exists(join(path, 'bin', 'Debug')): + rmtree(join(path, 'bin', 'Debug')) + self.build(path) + name = basename(path) + if not exists(join(path, 'bin', 'Debug')) or not any( + x.endswith('.exe') for x in listdir(join(path, 'bin', 'Debug'))): + return False + self.files.append(basename(path)) + for file in listdir(join(path, 'bin', 'Debug')): + if exists(join(path, 'bin', 'Debug', file)): + new_file = join(working_dir, basename(file)) + try: + copyfile(join(path, 'bin', 'Debug', file), new_file) + except: + pass + else: + return False + return True + + def push(self): + solution = self.solution + if solution.result == 'SOLUTION ERROR': + return + solution.result = 'IN QUEUE' + solution.save() + from Main.models import System + if len(Solution.objects.filter(result='TESTING')) < int(System.objects.get(key='queue_size').value): + self.test() + + def delete_everything(self): + ssp = solution_path(self.solution.path()) + sln_path = join(ssp, '.idea') + if exists(sln_path): + rmtree(sln_path) + sln_path = join(ssp, '.vs') + if exists(sln_path): + rmtree(sln_path) + sln_path = ssp + for p in listdir(sln_path): + if isdir(join(sln_path, p)): + if exists(join(sln_path, p, 'bin')): + rmtree(join(sln_path, p, 'bin')) + if exists(join(sln_path, p, 'obj')): + rmtree(join(sln_path, p, 'obj')) + if exists(self.working_dir): + rmtree(self.working_dir) + if exists(join(self.solution.path(), 'solution.zip')): + remove(join(self.solution.path(), 'solution.zip')) + if exists(join(self.solution.path(), '__MACOSX')): + rmtree(join(self.solution.path(), '__MACOSX')) + if exists(join(sln_path, '.DS_Store')): + remove(join(sln_path, '.DS_Store')) + if exists(join(sln_path, 'test_folder')): + rmtree(join(sln_path, 'test_folder')) + + def nunit_testing(self): + solution = self.solution + with self.solution.log_fs as fs: + fs.write(b'Building image\n') + shell('docker build -t solution_{} {}'.format(self.solution.id, self.working_dir)) + with self.solution.log_fs as fs: + fs.write(b'Image built successfully\n') + + def execute(): + with self.solution.log_fs as fs: + shell('docker run --name solution_container_{} solution_{}'.format(self.solution.id, self.solution.id), + output=fs) + + solution.write_log('Running container') + t = Thread(target=execute) + t.start() + t.join(self.solution.task.time_limit / 1000) + solution.write_log('Running finished') + with self.solution.log_fs as fs: + shell('docker cp solution_container_{}:/app/TestResults.xml {}'.format(self.solution.id, self.working_dir), + fs) + with self.solution.log_fs as fs: + shell('docker rm --force solution_container_{}'.format(self.solution.id), fs) + with self.solution.log_fs as fs: + shell('docker image rm solution_{}'.format(self.solution.id), fs) + if not exists(join(self.working_dir, 'TestResults.xml')): + self.solution.set_result('Time limit') + solution.write_log('Result file not found in container') + return + solution.write_log('Result file found in container') + try: + doc = parse(join(self.working_dir, 'TestResults.xml')) + res = get_node_value(doc.getElementsByTagName('Passed')) + '/' + get_node_value( + doc.getElementsByTagName('Total')) + self.solution.details = '' + for el in doc.getElementsByTagName('Result'): + self.solution.details += '
' + get_node_value(el.getElementsByTagName('MethodName')) + '
' + r = get_node_value(el.getElementsByTagName('Successful')) + if r == 'true': + self.solution.details += '
Passed
' + else: + self.solution.details += '
Failed
' + mes = get_node_value(el.getElementsByTagName('Message')) + self.solution.details += '
{}
'.format(mes) + except: + solution.write_log('Unknown error') + res = 'TEST ERROR' + self.solution.set_result(res) + + def test(self): + solution = self.solution + solution.result = 'TESTING' + solution.save() + try: + if not exists(self.solution.task.tests_path()): + with self.solution.log_fs as fs: + fs.write(b'No test file found\n') + solution.set_result('TEST ERROR') + solution.save() + self.delete_everything() + start_new(self.host) + return + sln_path = solution_path(join(MEDIA_ROOT, 'solutions', str(solution.id))) + if sln_path == '': + solution.set_result('TEST ERROR') + solution.save() + self.delete_everything() + start_new(self.host) + return + working_dir = join(sln_path, 'test_folder') + if exists(working_dir): + try: + rmtree(working_dir) + except: + remove(working_dir) + mkdir(working_dir) + with self.solution.log_fs as fs: + fs.write(b'Testing directory created\n') + for project in listdir(sln_path): + solution.write_log('Checking if {} is project'.format(project)) + prj = project + project = join(sln_path, project) + if isdir(project) and is_project(project) and basename(project) != 'TestsProject': + if not self.build_and_copy(project, working_dir): + solution.set_result('Compilation error') + solution.write_log('Failed to compile project {}'.format(prj)) + solution.save() + self.delete_everything() + start_new(self.host) + return + dll_path = solution.task.tests_path() + solution.write_log('Copying test file to working directory') + copyfile(dll_path, join(working_dir, str(solution.task.id) + '.cs')) + solution.write_log('Test file copied') + for file in listdir('SprintTest'): + try: + copyfile(join('SprintTest', file), join(working_dir, file)) + except: + pass + self.working_dir = working_dir + build_tests_cmd = 'csc -out:{} -t:library /r:{} /r:{} /r:{} '.format(join(self.working_dir, 'tests.dll'), + join(self.working_dir, + 'SprintTest.dll'), + join(working_dir, + 'System.Runtime.dll'), + join(working_dir, + 'System.Reflection.dll')) + for file in self.files: + build_tests_cmd += '/r:{}.dll '.format(join(self.working_dir, file)) + build_tests_cmd += self.solution.task.tests_path() + if exists(join(self.working_dir, 'tests.dll')): + remove(join(self.working_dir, 'tests.dll')) + solution.write_log('Building tests file started') + with self.solution.log_fs as fs: + shell(build_tests_cmd, fs) + with self.solution.log_fs as fs: + fs.write(b'Building tests file finished\n') + if exists(join(self.working_dir, 'tests.dll')): + with self.solution.log_fs as fs: + fs.write(b'Got .dll tests file\n') + for file in ExtraFile.objects.filter(task=self.solution.task): + copyfile(file.path, join(working_dir, file.filename)) + self.nunit_testing() + else: + solution.set_result('TEST ERROR') + solution.write_log('Failed to compile tests') + except: + solution.set_result('TEST ERROR') + raise + with self.solution.log_fs as fs: + fs.write(b'Unknown error\n') + solution.save() + self.delete_everything() + start_new(self.host) diff --git a/Main/Timer.py b/Main/Timer.py new file mode 100644 index 0000000..8b15ec3 --- /dev/null +++ b/Main/Timer.py @@ -0,0 +1,29 @@ +class Method: + + def __init__(self, meth, name): + self.meth = meth + self.name = name + + def __eq__(self, other): + return self.name == other.name + + def execute(self): + self.meth() + + +class Timer: + methods = [] + + def push(self, meth): + methods.append(math) + + def polling(self): + for i in range(len(self.methods)): + methods[i].execute() + + def remove(method_name): + for method in self.methods: + if method.name == method_name: + self.methods.remove(method) + return + raise IndexError("No method in list") diff --git a/Main/__init__.py b/Main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Main/admin.py b/Main/admin.py new file mode 100644 index 0000000..6181281 --- /dev/null +++ b/Main/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from Main.models import * + +# Register your models here. + +admin.site.register(Course) +admin.site.register(Block) +admin.site.register(Task) +admin.site.register(Subscribe) +admin.site.register(Restore) +admin.site.register(UserInfo) +admin.site.register(Solution) +admin.site.register(System) +admin.site.register(ExtraFile) \ No newline at end of file diff --git a/Main/apps.py b/Main/apps.py new file mode 100644 index 0000000..2d54e36 --- /dev/null +++ b/Main/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MainConfig(AppConfig): + name = 'Main' diff --git a/Main/commands.py b/Main/commands.py new file mode 100644 index 0000000..b181c53 --- /dev/null +++ b/Main/commands.py @@ -0,0 +1,8 @@ +from subprocess import Popen +from sys import stdout + + +def shell(cmd, output=stdout): + p = Popen(cmd, shell=True, stdout=output) + p.wait() + p.kill() diff --git a/Main/context_processors.py b/Main/context_processors.py new file mode 100644 index 0000000..0f71e69 --- /dev/null +++ b/Main/context_processors.py @@ -0,0 +1,8 @@ +from .main import check_admin + + +def attributes(request): + return { + 'current_page': 'settings' if '/settings' == request.path else 'admin' if '/admin/' in request.path else 'main', + 'is_admin': check_admin(request.user) + } \ No newline at end of file diff --git a/Main/forms.py b/Main/forms.py new file mode 100644 index 0000000..4a7be6e --- /dev/null +++ b/Main/forms.py @@ -0,0 +1,32 @@ +from django import forms + + +class PasswordField(forms.CharField): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.widget = forms.PasswordInput() + + +class LoginForm(forms.Form): + email = forms.EmailField(widget=forms.TextInput(attrs={"class": "input_simple"})) + password = forms.CharField(widget=forms.PasswordInput(attrs={"class": "input_simple"})) + + +class FileForm(forms.Form): + file = forms.FileField(widget=forms.FileInput(attrs={'class': 'input_simple'}), required=False) + + +class TestsForm(forms.Form): + tests = forms.FileField(widget=forms.FileInput(), required=False) + + +class ChangePasswordForm(forms.Form): + old = PasswordField(label='Старый пароль') + new = PasswordField(label='Новый пароль') + again = PasswordField(label='Еще раз') + + +class ResetPasswordForm(forms.Form): + new = PasswordField(label='Новый пароль') + again = PasswordField(label='Еще раз') diff --git a/Main/main.py b/Main/main.py new file mode 100644 index 0000000..94e2598 --- /dev/null +++ b/Main/main.py @@ -0,0 +1,339 @@ +import smtplib +from contextlib import contextmanager +from json import dumps +from os import listdir, mkdir +from os.path import isdir, basename, dirname, join, exists +from random import choice +from shutil import copyfile, rmtree +from string import ascii_letters +from threading import Thread +from time import sleep + +import copydetect +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from django.db.transaction import atomic +from django.utils import timezone +from django.utils.datastructures import MultiValueDictKeyError + +from Main.models import Course, Block, Solution, ThreadSafe, Restore, System, Subscribe, UserInfo +from Sprint.settings import MEDIA_ROOT + +base_dir = 'data' + + +@contextmanager +def lock(key): + pk = ThreadSafe.objects.get_or_create(key=key)[0].pk + try: + objs = ThreadSafe.objects.filter(pk=pk).select_for_update() + with atomic(): + list(objs) + yield None + finally: + pass + + +def get_in_html_tag(full, tag_name): + try: + return full.split('
'.format(tag_name))[1].split('
')[0] + except IndexError: + return '' + + +def random_string(): + letters = ascii_letters + return ''.join(choice(letters) for _ in range(20)) + + +def get_restore_hash(): + available = [r.code for r in Restore.objects.all()] + while True: + s = random_string() + if s not in available: + break + return s + + +def send(subject, to_addr, body_text): + from_addr = System.objects.get(key='email_address').value + body = "\r\n".join(( + "From: %s" % from_addr, + "To: %s" % to_addr, + "Subject: %s" % subject, + "", + body_text + )) + + server = smtplib.SMTP('SMTP.Office365.com', 587) + server.starttls() + server.login(System.objects.get(key='email_address').value, System.objects.get(key='email_password').value) + server.sendmail(from_addr, [to_addr], body) + server.quit() + + +def send_email(subject, to_addr, body_text): + Thread(target=lambda: send(subject, to_addr, body_text)).start() + + +def check_login(user): + return user.is_authenticated + + +def check_admin(user): + if check_teacher(user): + return True + if not check_login(user): + return False + return len(Subscribe.objects.filter(user=user, is_assistant=True)) > 0 + + +def check_teacher(user): + return user.is_staff and check_login(user) + + +def check_god(user): + return user.is_superuser and check_login(user) + + +def courses_available(user): + if user.is_superuser: + return Course.objects.all() + else: + return [s.course for s in Subscribe.objects.filter(user=user)] + + +def blocks_available(user): + courses = courses_available(user) + blocks = {} + is_admin = check_admin(user) + for course in courses: + if is_admin: + blocks[course] = Block.objects.filter( + course=course + ) + else: + blocks[course] = Block.objects.filter( + opened=True, + time_start__lte=timezone.now(), + course=course + ) + return blocks + + +def can_send_solution(user, task): + if user.is_superuser: + return True + try: + s = Subscribe.objects.get(course=task.block.course, user=user) + except ObjectDoesNotExist: + return False + if s.is_assistant: + return True + return task.block.time_start <= timezone.now() <= task.block.time_end and task.max_solutions_count > len(Solution.objects.filter(user=user, task=task)) and task.block.opened + + +def check_permission_block(user, block): + blocks = blocks_available(user) + for course in blocks.keys(): + if block in blocks[course]: + return True + return False + + +def is_integer(x): + try: + int(x) + return True + except ValueError: + return False + + +def check_admin_on_course(user, course): + if user.is_superuser: + return True + try: + s = Subscribe.objects.get(user=user, course=course) + except ObjectDoesNotExist: + return False + return s.is_assistant or user.is_staff + + +def comparer(value1, value2): + if value1 < value2: + return 1 + elif value1 == value2: + return 0 + else: + return -1 + + +def result_comparer(result1, result2): + verdicts = ['IN QUEUE', 'TESTING', 'TEST ERROR', 'SOLUTION ERROR', 'Compilation error', 'Time limit'] + if result1 in verdicts and result2 in verdicts: + return comparer(verdicts.index(result1), verdicts.index(result2)) + if result1 in verdicts and result2 not in verdicts: + return 1 + if result1 not in verdicts and result2 in verdicts: + return -1 + return comparer(int(result1.split('/')[0]), int(result2.split('/')[0])) + + +def solutions_filter(request): + try: + solutions = list(reversed(Solution.objects.filter(task__block_id=request['block_id']))) + except MultiValueDictKeyError as e: + return [Solution.objects.get(id=request['id'])] + if 'solution_id' in request.keys(): + solutions = [solution for solution in solutions if any([solution.id == int(i) for i in request['solution_id'].strip().split()])] + if 'task_name' in request.keys(): + solutions = [solution for solution in solutions if solution.task.name == request['task_name']] + if 'user' in request.keys(): + solutions = [solution for solution in solutions if str(solution.userinfo) == request['user']] + if 'group' in request.keys(): + solutions = [solution for solution in solutions if solution.userinfo.group == request['group']] + if 'best_result' in request.keys(): + sols = {} + for solution in solutions: + if (solution.user.username, solution.task.id) in sols.keys(): + comp = result_comparer(sols[(solution.user.username, solution.task.id)][0].result, solution.result) + if comp == 1: + sols[(solution.user.username, solution.task.id)] = [solution] + elif comp == 0: + sols[(solution.user.username, solution.task.id)].append(solution) + else: + sols[(solution.user.username, solution.task.id)] = [solution] + solutions = [] + for sol in sols.values(): + for val in sol: + solutions.append(val) + solutions = list(sorted(solutions, key=lambda s: s.id, reverse=True)) + if 'last_solution' in request.keys(): + visited = [] + new_solutions = [] + for solution in solutions: + if (solution.user.username, solution.task.id) not in visited: + visited.append((solution.user.username, solution.task.id)) + new_solutions.append(solution) + solutions = new_solutions + if 'only_students' in request.keys(): + solutions = [solution for solution in solutions if not check_admin_on_course(solution.user, solution.task.block.course)] + if 'not_seen' in request.keys(): + solutions = [solution for solution in solutions if solution.mark == None] + return sorted(solutions, key=lambda s: s.id, reverse=True) + + +def re_test(solutions_request, request): + from .Tester import Tester + for sol in solutions_request: + sol.details = '' + with open(sol.log_file, 'wb') as fs: + fs.write(b'') + sol.save() + Thread(target=lambda: Tester(sol, request.META['HTTP_HOST']).push()).start() + sleep(.1) + + +def block_solutions_info(block): + all_solutions = Solution.objects.filter(task__block=block) + all_users = [solution.userinfo for solution in all_solutions] + return { + 'tasks': sorted(list(set([solution.task for solution in all_solutions])), key=lambda x: x.name), + 'users': sorted(list(set(all_users)), key=lambda x: str(x)), + 'groups': sorted(list(set([userinfo.group for userinfo in all_users])), key=lambda x: str(x)) + } + + +def delete_folder(path): + flag = True + while flag: + try: + rmtree(dirname(path)) + flag = False + except: + pass + + +def solution_path(path): + files = [x for x in listdir(path) if x.endswith('.sln') and not x.startswith('.')] + if files: + return path + return ''.join([solution_path(join(path, file)) for file in listdir(path) if isdir(join(path, file))]) + + +def register_user(u): + password = random_string() + user = User.objects.create_user(username=u['email'], email=u['email'], password=password) + UserInfo.objects.create( + surname=u['surname'], + name=u['name'], + middle_name=u['middle_name'], + group=u['group'], + user=user + ) + send_email('You have been registered in Sprint!', u['email'], + 'Your password is: {}\nPlease change it after login in settings!\nhttps://sprint.cshse.ru/'.format(password)) + return user + + +def check_cheating(solutions, block, cheating_percent): + block.cheating_checking = True + block.save() + try: + cheating_data = {} + cheating_path = join(MEDIA_ROOT, 'cheating', str(block.id)) + if exists(cheating_path): + rmtree(cheating_path) + mkdir(cheating_path) + for solution in solutions: + for file in solution.user_files.keys(): + user_file = join(MEDIA_ROOT, 'solutions', str(solution.id), file) + dest_file = join(cheating_path, '_'.join([str(solution.id), basename(file)])) + copyfile(user_file, dest_file) + files_len = len(solutions) + files = listdir(cheating_path) + for i in range(len(files) - 1): + for j in range(i + 1, len(files)): + file1 = files[i] + file2 = files[j] + s1 = file1.split('_') + s2 = file2.split('_') + sol1 = Solution.objects.get(id=int(s1[0])) + sol2 = Solution.objects.get(id=int(s2[0])) + filename1 = '_'.join(s1[1:]) + filename2 = '_'.join(s2[1:]) + if sol1.user == sol2.user or sol1.task != sol2.task or filename1 != filename2: + continue + fp1 = copydetect.CodeFingerprint(join(cheating_path, file1), 25, 1) + fp2 = copydetect.CodeFingerprint(join(cheating_path, file2), 25, 1) + token_overlap, similarities, slices = copydetect.compare_files(fp1, fp2) + similarity = (similarities[0] + similarities[1]) / 2 + if similarity >= cheating_percent / 100: + if sol1.user.id not in cheating_data.keys(): + cheating_data[sol1.user.id] = [] + if sol2.user.id not in cheating_data.keys(): + cheating_data[sol2.user.id] = [] + cheating_data[sol1.user.id].append({ + 'source': True, + 'solution': sol1.id, + 'file': filename1, + 'similar': sol2.id, + 'similarity': round(similarity * 100, 2) + }) + cheating_data[sol2.user.id].append({ + 'source': False, + 'solution': sol2.id, + 'file': filename2, + 'similar': sol1.id, + 'similarity': round(similarity * 100, 2) + }) + finally: + if exists(cheating_path): + rmtree(cheating_path) + with open(block.cheating_results_path, 'w') as fs: + fs.write(dumps(cheating_data)) + block = Block.objects.get(id=block.id) + block.cheating_checking = False + block.save() + print('finished') + diff --git a/Main/migrations/0001_initial.py b/Main/migrations/0001_initial.py new file mode 100644 index 0000000..e530768 --- /dev/null +++ b/Main/migrations/0001_initial.py @@ -0,0 +1,98 @@ +# Generated by Django 3.0.2 on 2020-06-25 20:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Block', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField()), + ('time_start', models.DateTimeField()), + ('time_end', models.DateTimeField()), + ('opened', models.IntegerField()), + ], + ), + migrations.CreateModel( + name='Course', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField()), + ], + ), + migrations.CreateModel( + name='UserInfo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('surname', models.TextField()), + ('name', models.TextField()), + ('middle_name', models.TextField()), + ('group_name', models.TextField()), + ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField()), + ('legend', models.TextField()), + ('input', models.TextField()), + ('output', models.TextField()), + ('specifications', models.TextField()), + ('time_limit', models.IntegerField()), + ('block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Block')), + ], + ), + migrations.CreateModel( + name='Subscribe', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_assistant', models.IntegerField()), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Course')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Solution', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('result', models.TextField()), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Task')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Restore', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.TextField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Mark', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('mark', models.IntegerField()), + ('block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Block')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='block', + name='course', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Course'), + ), + ] diff --git a/Main/migrations/0002_auto_20200626_0946.py b/Main/migrations/0002_auto_20200626_0946.py new file mode 100644 index 0000000..a25c745 --- /dev/null +++ b/Main/migrations/0002_auto_20200626_0946.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.2 on 2020-06-26 09:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='userinfo', + old_name='group_name', + new_name='group', + ), + ] diff --git a/Main/migrations/0003_auto_20200627_1959.py b/Main/migrations/0003_auto_20200627_1959.py new file mode 100644 index 0000000..228e39c --- /dev/null +++ b/Main/migrations/0003_auto_20200627_1959.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.2 on 2020-06-27 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0002_auto_20200626_0946'), + ] + + operations = [ + migrations.AlterField( + model_name='subscribe', + name='is_assistant', + field=models.IntegerField(default=0), + ), + ] diff --git a/Main/migrations/0004_auto_20200628_0917.py b/Main/migrations/0004_auto_20200628_0917.py new file mode 100644 index 0000000..7696150 --- /dev/null +++ b/Main/migrations/0004_auto_20200628_0917.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.2 on 2020-06-28 09:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0003_auto_20200627_1959'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='input', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='task', + name='legend', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='task', + name='output', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='task', + name='specifications', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='task', + name='time_limit', + field=models.IntegerField(default=10000), + ), + ] diff --git a/Main/migrations/0005_solution_time_sent.py b/Main/migrations/0005_solution_time_sent.py new file mode 100644 index 0000000..39dfb60 --- /dev/null +++ b/Main/migrations/0005_solution_time_sent.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.2 on 2020-06-28 10:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0004_auto_20200628_0917'), + ] + + operations = [ + migrations.AddField( + model_name='solution', + name='time_sent', + field=models.DateTimeField(null=True), + ), + ] diff --git a/Main/migrations/0006_auto_20200628_1315.py b/Main/migrations/0006_auto_20200628_1315.py new file mode 100644 index 0000000..d761af7 --- /dev/null +++ b/Main/migrations/0006_auto_20200628_1315.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.2 on 2020-06-28 13:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0005_solution_time_sent'), + ] + + operations = [ + migrations.AlterField( + model_name='solution', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.UserInfo'), + ), + ] diff --git a/Main/migrations/0007_auto_20200629_0833.py b/Main/migrations/0007_auto_20200629_0833.py new file mode 100644 index 0000000..77a213f --- /dev/null +++ b/Main/migrations/0007_auto_20200629_0833.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.2 on 2020-06-29 08:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('Main', '0006_auto_20200628_1315'), + ] + + operations = [ + migrations.AlterField( + model_name='solution', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/Main/migrations/0008_auto_20200702_2140.py b/Main/migrations/0008_auto_20200702_2140.py new file mode 100644 index 0000000..9207044 --- /dev/null +++ b/Main/migrations/0008_auto_20200702_2140.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.2 on 2020-07-02 18:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0007_auto_20200629_0833'), + ] + + operations = [ + migrations.AddField( + model_name='userinfo', + name='mark_notification', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='userinfo', + name='new_block_notification', + field=models.IntegerField(default=0), + ), + ] diff --git a/Main/migrations/0009_auto_20200704_1703.py b/Main/migrations/0009_auto_20200704_1703.py new file mode 100644 index 0000000..7c8eeb6 --- /dev/null +++ b/Main/migrations/0009_auto_20200704_1703.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.2 on 2020-07-04 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0008_auto_20200702_2140'), + ] + + operations = [ + migrations.AddField( + model_name='solution', + name='comment', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='solution', + name='mark', + field=models.IntegerField(null=True), + ), + ] diff --git a/Main/migrations/0010_system.py b/Main/migrations/0010_system.py new file mode 100644 index 0000000..b388d98 --- /dev/null +++ b/Main/migrations/0010_system.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.2 on 2020-07-24 10:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0009_auto_20200704_1703'), + ] + + operations = [ + migrations.CreateModel( + name='System', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.TextField()), + ('value', models.TextField()), + ], + ), + ] diff --git a/Main/migrations/0011_auto_20200814_2035.py b/Main/migrations/0011_auto_20200814_2035.py new file mode 100644 index 0000000..b2b0209 --- /dev/null +++ b/Main/migrations/0011_auto_20200814_2035.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.2 on 2020-08-14 17:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0010_system'), + ] + + operations = [ + migrations.CreateModel( + name='ThreadSafe', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=80, unique=True)), + ], + ), + migrations.DeleteModel( + name='Mark', + ), + ] diff --git a/Main/migrations/0012_auto_20200901_1154.py b/Main/migrations/0012_auto_20200901_1154.py new file mode 100644 index 0000000..7da64e8 --- /dev/null +++ b/Main/migrations/0012_auto_20200901_1154.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1 on 2020-09-01 08:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0011_auto_20200814_2035'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='weight', + field=models.FloatField(default=1.0), + ), + migrations.CreateModel( + name='ExtraFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.task')), + ], + ), + ] diff --git a/Main/migrations/0013_delete_extrafile.py b/Main/migrations/0013_delete_extrafile.py new file mode 100644 index 0000000..c303052 --- /dev/null +++ b/Main/migrations/0013_delete_extrafile.py @@ -0,0 +1,16 @@ +# Generated by Django 3.1 on 2020-09-01 09:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0012_auto_20200901_1154'), + ] + + operations = [ + migrations.DeleteModel( + name='ExtraFile', + ), + ] diff --git a/Main/migrations/0014_extrafile.py b/Main/migrations/0014_extrafile.py new file mode 100644 index 0000000..cdea665 --- /dev/null +++ b/Main/migrations/0014_extrafile.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1 on 2020-09-01 09:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0013_delete_extrafile'), + ] + + operations = [ + migrations.CreateModel( + name='ExtraFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='')), + ('filename', models.TextField()), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.task')), + ], + ), + ] diff --git a/Main/migrations/0015_auto_20200902_1555.py b/Main/migrations/0015_auto_20200902_1555.py new file mode 100644 index 0000000..d6da9ba --- /dev/null +++ b/Main/migrations/0015_auto_20200902_1555.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1 on 2020-09-02 12:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0014_extrafile'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='max_mark', + field=models.IntegerField(default=10), + ), + migrations.AlterField( + model_name='extrafile', + name='file', + field=models.FileField(upload_to='data\\extra_files'), + ), + ] diff --git a/Main/migrations/0016_task_max_solutions_count.py b/Main/migrations/0016_task_max_solutions_count.py new file mode 100644 index 0000000..2f772e4 --- /dev/null +++ b/Main/migrations/0016_task_max_solutions_count.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-09-02 13:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0015_auto_20200902_1555'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='max_solutions_count', + field=models.IntegerField(default=10), + ), + ] diff --git a/Main/migrations/0017_solution_details.py b/Main/migrations/0017_solution_details.py new file mode 100644 index 0000000..2b5d6f2 --- /dev/null +++ b/Main/migrations/0017_solution_details.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-09-05 12:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0016_task_max_solutions_count'), + ] + + operations = [ + migrations.AddField( + model_name='solution', + name='details', + field=models.TextField(default=''), + ), + ] diff --git a/Main/migrations/0018_remove_extrafile_file.py b/Main/migrations/0018_remove_extrafile_file.py new file mode 100644 index 0000000..74afce9 --- /dev/null +++ b/Main/migrations/0018_remove_extrafile_file.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-09-17 08:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0017_solution_details'), + ] + + operations = [ + migrations.RemoveField( + model_name='extrafile', + name='file', + ), + ] diff --git a/Main/migrations/0019_task_show_details.py b/Main/migrations/0019_task_show_details.py new file mode 100644 index 0000000..4093064 --- /dev/null +++ b/Main/migrations/0019_task_show_details.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-09-17 09:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0018_remove_extrafile_file'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='show_details', + field=models.IntegerField(default=1), + ), + ] diff --git a/Main/migrations/0020_task_solution_type.py b/Main/migrations/0020_task_solution_type.py new file mode 100644 index 0000000..8f0fdb9 --- /dev/null +++ b/Main/migrations/0020_task_solution_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-10-08 08:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0019_task_show_details'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='solution_type', + field=models.TextField(default='Решение'), + ), + ] diff --git a/Main/migrations/0021_remove_task_solution_type.py b/Main/migrations/0021_remove_task_solution_type.py new file mode 100644 index 0000000..0736cf7 --- /dev/null +++ b/Main/migrations/0021_remove_task_solution_type.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-10-08 10:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0020_task_solution_type'), + ] + + operations = [ + migrations.RemoveField( + model_name='task', + name='solution_type', + ), + ] diff --git a/Main/migrations/0022_task_full_solution.py b/Main/migrations/0022_task_full_solution.py new file mode 100644 index 0000000..24eec3a --- /dev/null +++ b/Main/migrations/0022_task_full_solution.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-10-22 14:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0021_remove_task_solution_type'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='full_solution', + field=models.IntegerField(default=0), + ), + ] diff --git a/Main/migrations/0023_extrafile_for_compilation.py b/Main/migrations/0023_extrafile_for_compilation.py new file mode 100644 index 0000000..9a360a4 --- /dev/null +++ b/Main/migrations/0023_extrafile_for_compilation.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-11-01 19:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0022_task_full_solution'), + ] + + operations = [ + migrations.AddField( + model_name='extrafile', + name='for_compilation', + field=models.IntegerField(default=0), + ), + ] diff --git a/Main/migrations/0024_extrafile_sample.py b/Main/migrations/0024_extrafile_sample.py new file mode 100644 index 0000000..31a9d3e --- /dev/null +++ b/Main/migrations/0024_extrafile_sample.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-11-06 08:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0023_extrafile_for_compilation'), + ] + + operations = [ + migrations.AddField( + model_name='extrafile', + name='sample', + field=models.IntegerField(default=0), + ), + ] diff --git a/Main/migrations/0025_auto_20201106_1848.py b/Main/migrations/0025_auto_20201106_1848.py new file mode 100644 index 0000000..64f7d6e --- /dev/null +++ b/Main/migrations/0025_auto_20201106_1848.py @@ -0,0 +1,51 @@ +# Generated by Django 3.1 on 2020-11-06 15:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0024_extrafile_sample'), + ] + + operations = [ + migrations.RemoveField( + model_name='userinfo', + name='mark_notification', + ), + migrations.RemoveField( + model_name='userinfo', + name='new_block_notification', + ), + migrations.AlterField( + model_name='block', + name='opened', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='extrafile', + name='for_compilation', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='extrafile', + name='sample', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='subscribe', + name='is_assistant', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='task', + name='full_solution', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='task', + name='show_details', + field=models.BooleanField(default=False), + ), + ] diff --git a/Main/migrations/0026_block_show_rating.py b/Main/migrations/0026_block_show_rating.py new file mode 100644 index 0000000..45f4c80 --- /dev/null +++ b/Main/migrations/0026_block_show_rating.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2020-12-01 08:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0025_auto_20201106_1848'), + ] + + operations = [ + migrations.AddField( + model_name='block', + name='show_rating', + field=models.BooleanField(default=True), + ), + ] diff --git a/Main/migrations/0027_task_mark_formula.py b/Main/migrations/0027_task_mark_formula.py new file mode 100644 index 0000000..6fb2096 --- /dev/null +++ b/Main/migrations/0027_task_mark_formula.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2020-12-26 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0026_block_show_rating'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='mark_formula', + field=models.TextField(default='None'), + ), + ] diff --git a/Main/migrations/0028_task_show_result.py b/Main/migrations/0028_task_show_result.py new file mode 100644 index 0000000..23fec4d --- /dev/null +++ b/Main/migrations/0028_task_show_result.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2021-01-01 09:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0027_task_mark_formula'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='show_result', + field=models.BooleanField(default=True), + ), + ] diff --git a/Main/migrations/0029_auto_20210130_1950.py b/Main/migrations/0029_auto_20210130_1950.py new file mode 100644 index 0000000..7b0458b --- /dev/null +++ b/Main/migrations/0029_auto_20210130_1950.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.3 on 2021-01-30 16:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0028_task_show_result'), + ] + + operations = [ + migrations.AddField( + model_name='block', + name='priority', + field=models.IntegerField(default=5), + ), + migrations.AddField( + model_name='task', + name='priority', + field=models.IntegerField(default=5), + ), + ] diff --git a/Main/migrations/0030_message.py b/Main/migrations/0030_message.py new file mode 100644 index 0000000..815e0dd --- /dev/null +++ b/Main/migrations/0030_message.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.3 on 2021-02-06 21:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('Main', '0029_auto_20210130_1950'), + ] + + operations = [ + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('for_all', models.BooleanField()), + ('text', models.TextField()), + ('reply_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='Main.message')), + ('sender', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.task')), + ], + ), + ] diff --git a/Main/migrations/0031_block_cheating_checking.py b/Main/migrations/0031_block_cheating_checking.py new file mode 100644 index 0000000..57f91e6 --- /dev/null +++ b/Main/migrations/0031_block_cheating_checking.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2021-03-13 08:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0030_message'), + ] + + operations = [ + migrations.AddField( + model_name='block', + name='cheating_checking', + field=models.BooleanField(default=False), + ), + ] diff --git a/Main/migrations/0032_block_cheating_data.py b/Main/migrations/0032_block_cheating_data.py new file mode 100644 index 0000000..3bbbe8d --- /dev/null +++ b/Main/migrations/0032_block_cheating_data.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2021-03-14 12:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0031_block_cheating_checking'), + ] + + operations = [ + migrations.AddField( + model_name='block', + name='cheating_data', + field=models.TextField(default='[]'), + ), + ] diff --git a/Main/migrations/0033_remove_block_cheating_data.py b/Main/migrations/0033_remove_block_cheating_data.py new file mode 100644 index 0000000..d27a788 --- /dev/null +++ b/Main/migrations/0033_remove_block_cheating_data.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.3 on 2021-03-14 13:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0032_block_cheating_data'), + ] + + operations = [ + migrations.RemoveField( + model_name='block', + name='cheating_data', + ), + ] diff --git a/Main/migrations/__init__.py b/Main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Main/models.py b/Main/models.py new file mode 100644 index 0000000..6784de2 --- /dev/null +++ b/Main/models.py @@ -0,0 +1,471 @@ +from django.contrib.auth.models import User +from django.db import models +from django.dispatch import receiver +from django.db.models.signals import post_delete +from os.path import sep, join, exists +from os import remove + +from Main.commands import shell +from Sprint.settings import MEDIA_ROOT +from django.core.exceptions import ObjectDoesNotExist +from json import loads + + +base_dir = 'data' + + +class ThreadSafe(models.Model): + key = models.CharField(max_length=80, unique=True) + + +class Course(models.Model): + name = models.TextField() + + @property + def teachers(self): + return [UserInfo.objects.get(user=s.user) for s in Subscribe.objects.filter(user__is_staff=1, course=self)] + + @property + def subscribes(self): + return sorted(Subscribe.objects.filter(course=self), key=lambda s: s.user.email) + + + @property + def students(self): + userinfo = lambda sub: sub.user.userinfo + return sorted(Subscribe.objects.filter(course=self, is_assistant=False, user__is_staff=False), key=lambda s: userinfo(s).surname + userinfo(s).name + userinfo(s).middle_name) + + def __str__(self): + return self.name + + +class Block(models.Model): + name = models.TextField() + course = models.ForeignKey(Course, on_delete=models.CASCADE) + time_start = models.DateTimeField() + time_end = models.DateTimeField() + opened = models.BooleanField(default=False) + show_rating = models.BooleanField(default=True) + priority = models.IntegerField(default=5) + cheating_checking = models.BooleanField(default=False) + + @property + def messages(self): + return Message.objects.filter(task__block=self) + + def __str__(self): + return self.name + + @property + def tasks(self): + return Task.objects.filter(block=self) + + @property + def time_start_chrome(self): + return self.time_start.strftime("%Y-%m-%dT%H:%M") + + @property + def time_end_chrome(self): + return self.time_end.strftime("%Y-%m-%dT%H:%M") + + @property + def is_opened(self): + return 'checked' if self.opened else '' + + @property + def solutions(self): + return reversed(Solution.objects.filter(task__block=self)) + + @property + def subscribed_users(self): + return [UserInfo.objects.get(user=s.user) for s in Subscribe.objects.filter(course=self.course)] + + @property + def cheating_results_path(self): + return join(MEDIA_ROOT, 'cheating_results', str(self.id)) + + @property + def cheating_checked(self): + return self.cheating_results != {} + + @property + def cheating_results(self): + return loads(open(self.cheating_results_path, 'r').read()) if exists(self.cheating_results_path) else {} + + @property + def cheating_status(self): + if self.cheating_checking: + return 'Идет проверка' + if not exists(self.cheating_results_path): + return 'Еще не проверено' + return 'Проверка завершена' + + +class Restore(models.Model): + code = models.TextField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + + def __str__(self): + return self.user.username + + +class Subscribe(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + course = models.ForeignKey(Course, on_delete=models.CASCADE) + is_assistant = models.BooleanField(default=False) + + def __str__(self): + return self.user.username + '|' + self.course.name + + @property + def userinfo(self): + return UserInfo.objects.get(user=self.user) + + @property + def role(self): + if self.user.is_superuser: + return 'Администратор' + if self.user.is_staff: + return 'Преподаватель' + return 'Ассистент' if self.is_assistant else 'Студент' + + +class Task(models.Model): + name = models.TextField() + block = models.ForeignKey(Block, on_delete=models.CASCADE) + legend = models.TextField(default='') + input = models.TextField(default='') + output = models.TextField(default='') + specifications = models.TextField(default='') + time_limit = models.IntegerField(default=10000) + weight = models.FloatField(default=1.0) + max_mark = models.IntegerField(default=10) + max_solutions_count = models.IntegerField(default=10) + show_result = models.BooleanField(default=True) + show_details = models.BooleanField(default=False) + full_solution = models.BooleanField(default=False) + mark_formula = models.TextField(default='None') + priority = models.IntegerField(default=5) + + @property + def students_solutions(self): + students = [sub.user for sub in Subscribe.objects.filter(course=self.block.course)] + solutions = Solution.objects.filter(task=self) + return [sol for sol in solutions if sol.user in students] + + @property + def correct_count(self): + solutions = self.students_solutions + count = 0 + for sol in solutions: + res = sol.result.split('/') + if len(res) == 2 and res[0] == res[1]: + count += 1 + return count + + @property + def solutions_count(self): + return len(self.students_solutions) + + @property + def partially_passed(self): + solutions = self.students_solutions + count = 0 + for sol in solutions: + res = sol.result.split('/') + if len(res) == 2 and res[0] != res[1]: + count += 1 + return count + + @property + def solutions_with_error(self): + return self.solutions_count - self.correct_count - self.partially_passed + + @property + def samples(self): + return [{ + 'input': file, + 'output': file.answer + } for file in ExtraFile.objects.filter(task=self, sample=True).order_by('filename')] + + def __hash__(self): + return self.id + + @property + def showable(self): + return 'checked' if self.show_details else '' + + + def __str__(self): + return self.name + + def tests_path(self): + return join(base_dir, 'tests', str(self.id) + '.cs') + + @property + def tests_text(self): + try: + return open(self.tests_path(), 'r').read() + except FileNotFoundError: + with open(self.tests_path(), 'w') as fs: + pass + return '' + + @property + def tests_uploaded(self): + from os.path import exists + return exists(self.tests_path()) + + @property + def files(self): + return ExtraFile.objects.filter(task=self).order_by('filename') + + @property + def files_for_compilation(self): + return ExtraFile.objects.filter(task=self, for_compilation=True) + + + @property + def is_full_solution(self): + return 'checked' if self.full_solution else '' + + +class UserInfo(models.Model): + surname = models.TextField() + name = models.TextField() + middle_name = models.TextField() + group = models.TextField() + user = models.OneToOneField(User, on_delete=models.CASCADE, null=True) + + def __eq__(self, obj): + return str(self) == str(obj) + + def __hash__(self): + return self.id + + def __str__(self): + return "{} {} {}".format(self.surname, self.name, self.middle_name) + + +class Solution(models.Model): + task = models.ForeignKey(Task, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + result = models.TextField() + details = models.TextField(default='') + time_sent = models.DateTimeField(null=True) + mark = models.IntegerField(null=True) + comment = models.TextField(default='') + + def set_result(self, result): + self.result = result + if len(result.split('/')) != 1: + result = int(result.split('/')[0]) + try: + self.mark = eval(self.task.mark_formula) + except: + self.mark = None + self.save() + + def __str__(self): + return str(self.id) + + def path(self): + return join(base_dir, 'solutions', str(self.id)) + + def write_log(self, text): + with self.log_fs as fs: + fs.write(bytes(text + '\n', 'cp866')) + + @property + def log_file(self): + return join(MEDIA_ROOT, 'logs', str(self.id) + '.log') + + @property + def log_text(self): + try: + return open(self.log_file, 'rb').read().decode('cp866') + except FileNotFoundError: + return '' + + @property + def log_fs(self): + return open(self.log_file, 'ab') + + @property + def userinfo(self): + return UserInfo.objects.get(user=self.user) + + @property + def mark_property(self): + return str(self.mark) if self.mark is not None else 'нет оценки' + + @property + def mark_select(self): + line = '' + if self.mark: + line += '' + else: + line += '' + for mark in range(self.task.max_mark + 1): + if mark == self.mark: + line += ''.format(mark, mark) + else: + line += ''.format(mark, mark) + return line + + @property + def comment_property(self): + return self.comment if self.comment else 'нет комментария' + + @staticmethod + def get_files(path): + from os import listdir + from os.path import isfile, join + files_dict = {} + for file in listdir(path): + if file == '__MACOSX' or file == 'test_folder' or file == 'bin' or file == 'obj' or file == '.vs': + continue + current_file = join(path, file) + if isfile(current_file): + if not current_file.endswith('.csproj') and not current_file.endswith('.sln'): + try: + files_dict[sep.join(current_file.split('solutions' + sep)[1].split(sep)[1:])] \ + = open(current_file, 'rb').read().decode('UTF-8') + except UnicodeDecodeError: + pass + else: + files_dict = {**files_dict, **Solution.get_files(current_file)} + return files_dict + + @property + def files(self): + return Solution.get_files(self.path()) + + @property + def user_files(self): + f = {} + comp_files = [ef.filename for ef in ExtraFile.objects.filter(task=self.task, for_compilation=True)] + for fi in self.files.keys(): + if not fi in comp_files: + f[fi] = self.files[fi] + return f + + @property + def passed_all_tests(self): + spl = self.result.split('/') + return len(spl) == 2 and spl[0] == spl[1] + +class System(models.Model): + key = models.TextField() + value = models.TextField() + + def __str__(self): + return self.key + + +class ExtraFile(models.Model): + task = models.ForeignKey(Task, on_delete=models.CASCADE) + filename = models.TextField() + for_compilation = models.BooleanField(default=False) + sample = models.BooleanField(default=False) + + @property + def answer(self): + try: + return ExtraFile.objects.get(task=self.task, filename=self.filename + '.a') + except ObjectDoesNotExist: + return None + + @property + def num(self): + try: + return int(self.filename.split('.')[0]) + except ValueError: + return '' + + @property + def is_for_compilation(self): + return 'checked' if self.for_compilation else '' + + @property + def is_sample(self): + return 'checked' if self.sample else '' + + @property + def can_be_sample(self): + try: + int(self.filename) + except: + return False + try: + ans = ExtraFile.objects.get(task=self.task, filename=self.filename + '.a') + except ObjectDoesNotExist: + return False + return self.readable and ans.readable + + + @property + def path(self): + return join(MEDIA_ROOT, 'extra_files', str(self.id)) + + + @property + def readable(self): + try: + open(self.path, 'rb').read().decode('UTF-8') + return True + except UnicodeDecodeError: + return False + + @property + def text(self): + return open(self.path, 'rb').read().decode('UTF-8') + + + def __str__(self): + return self.filename + + + def write(self, data): + with open(self.path, 'wb') as fs: + fs.write(data) + + +class Message(models.Model): + task = models.ForeignKey(Task, on_delete=models.CASCADE) + sender = models.ForeignKey(User, on_delete=models.CASCADE, null=True) + reply_to = models.ForeignKey('Message', on_delete=models.CASCADE, null=True) + for_all = models.BooleanField() + text = models.TextField() + + +@receiver(post_delete, sender=Task) +def delete_task_hook(sender, instance, using, **kwargs): + if exists(instance.tests_path()): + from os import remove + remove(instance.tests_path()) + + +@receiver(post_delete, sender=Solution) +def delete_solution_hook(sender, instance, using, **kwargs): + if exists(instance.path()): + from shutil import rmtree + rmtree(instance.path()) + shell('docker rm --force solution_container_{}'.format(instance.id)) + shell('docker image rm solution_{}'.format(instance.id)) + + +@receiver(post_delete, sender=ExtraFile) +def delete_file_hook(sender, instance, using, **kwargs): + try: + if exists(instance.path): + remove(instance.path) + except ValueError: + pass + if instance.filename.endswith('.a'): + try: + t = ExtraFile.objects.get(task=instance.task, filename=instance.filename[:-2]) + except ObjectDoesNotExist: + return + t.sample = False + t.save() diff --git a/Main/static/css/styles.css b/Main/static/css/styles.css new file mode 100644 index 0000000..dc564c7 --- /dev/null +++ b/Main/static/css/styles.css @@ -0,0 +1,11 @@ +.main_div { + margin-left: 5%; + margin-right: 5%; +} +.top-buffer { + margin-bottom:15px; +} +.top-button { + min-width: 100px; + display: inline-block; +} \ No newline at end of file diff --git a/Main/static/files/Darwin.dll b/Main/static/files/Darwin.dll new file mode 100644 index 0000000..7fadbf5 Binary files /dev/null and b/Main/static/files/Darwin.dll differ diff --git a/Main/static/img/admin_block_page.png b/Main/static/img/admin_block_page.png new file mode 100644 index 0000000..73824cb Binary files /dev/null and b/Main/static/img/admin_block_page.png differ diff --git a/Main/static/img/admin_page.png b/Main/static/img/admin_page.png new file mode 100644 index 0000000..059ce85 Binary files /dev/null and b/Main/static/img/admin_page.png differ diff --git a/Main/static/img/block_page.png b/Main/static/img/block_page.png new file mode 100644 index 0000000..13b3ac2 Binary files /dev/null and b/Main/static/img/block_page.png differ diff --git a/Main/static/img/icon.svg b/Main/static/img/icon.svg new file mode 100644 index 0000000..92d38f1 --- /dev/null +++ b/Main/static/img/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Main/static/img/main_page.png b/Main/static/img/main_page.png new file mode 100644 index 0000000..38ae2c9 Binary files /dev/null and b/Main/static/img/main_page.png differ diff --git a/Main/static/img/solution_page.png b/Main/static/img/solution_page.png new file mode 100644 index 0000000..db21d87 Binary files /dev/null and b/Main/static/img/solution_page.png differ diff --git a/Main/static/img/solutions_page.png b/Main/static/img/solutions_page.png new file mode 100644 index 0000000..04aecc2 Binary files /dev/null and b/Main/static/img/solutions_page.png differ diff --git a/Main/static/img/task_page.png b/Main/static/img/task_page.png new file mode 100644 index 0000000..8c7e4e6 Binary files /dev/null and b/Main/static/img/task_page.png differ diff --git a/Main/static/img/task_settings_page_1.png b/Main/static/img/task_settings_page_1.png new file mode 100644 index 0000000..1c79ea0 Binary files /dev/null and b/Main/static/img/task_settings_page_1.png differ diff --git a/Main/static/img/task_settings_page_2.png b/Main/static/img/task_settings_page_2.png new file mode 100644 index 0000000..4dd65a5 Binary files /dev/null and b/Main/static/img/task_settings_page_2.png differ diff --git a/Main/static/img/test_datails.png b/Main/static/img/test_datails.png new file mode 100644 index 0000000..1520ec8 Binary files /dev/null and b/Main/static/img/test_datails.png differ diff --git a/Main/static/img/upload_rules.png b/Main/static/img/upload_rules.png new file mode 100644 index 0000000..0dca4b2 Binary files /dev/null and b/Main/static/img/upload_rules.png differ diff --git a/Main/static/img/users_settings_page.png b/Main/static/img/users_settings_page.png new file mode 100644 index 0000000..2f1ae74 Binary files /dev/null and b/Main/static/img/users_settings_page.png differ diff --git a/Main/static/js/scripts.js b/Main/static/js/scripts.js new file mode 100644 index 0000000..8cdd22e --- /dev/null +++ b/Main/static/js/scripts.js @@ -0,0 +1,12 @@ +function main() { + window.location.href = "/main" +} +function logout() { + window.location.href = "/exit" +} +function settings() { + window.location.href = "/settings" +} +function admin() { + window.location.href = '../..' +} \ No newline at end of file diff --git a/Main/templatetags/__init__.py b/Main/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Main/templatetags/filters.py b/Main/templatetags/filters.py new file mode 100644 index 0000000..8b55ade --- /dev/null +++ b/Main/templatetags/filters.py @@ -0,0 +1,112 @@ +from django import template +from Main.models import Solution, Task, UserInfo +from django.contrib.auth.models import User + + +register = template.Library() + + +@register.filter('mark_for_task') +def mark_for_task(task, user): + try: + return round(list(Solution.objects.filter(task=task, user=user, mark__isnull=False))[-1].mark * 10 / task.max_mark) + except IndexError: + return 0 + + +@register.filter('mark_for_block') +def mark_for_block(block, user): + tasks = Task.objects.filter(block=block) + mark = 0 + for task in tasks: + mft = mark_for_task(task, user) + mark += mft * task.weight + return round(mark) + + +@register.filter('marked') +def marked(mark): + return mark != -1 + + +@register.filter('mark_color') +def mark_color(mark): + mark = round(mark) + if mark > 7: + return '#00FF00' + elif mark > 5: + return '#FFFF00' + elif mark > 3: + return '#FAD7A0' + elif mark > 0: + return '#F1948A' + else: + return '#FFFFFF' + + +@register.filter('in_dict') +def in_dict(value, dict): + return value in dict.keys() + + +@register.filter('last_attempts') +def last_attempts(user, task): + return task.max_solutions_count - len(Solution.objects.filter(task=task, user=user)) + + +@register.filter('userinfo_by_user') +def userinfo_by_user(user): + return UserInfo.objects.get(user=user) + + +@register.filter('mark_status') +def mark_status(user, task): + sols = Solution.objects.filter(user=user, task=task) + if len(sols) == 0: + return '-' + return sols.last().result + + +@register.filter('fully_marked') +def fully_marked(user, task): + return len(Solution.objects.filter(user=user, task=task, mark=None)) == 0 + + +@register.filter('is_code') +def is_code(path): + return path.endswith('.cs') + + +@register.filter('num_range') +def num_range(n): + return range(1, n + 1) + + +@register.filter('length') +def length(collection): + return len(collection) + + +@register.filter('user_by_id') +def user_by_id(user_id): + return User.objects.get(id=user_id) + + +@register.filter('dict_key') +def dict_key(d, k): + return d[k] + + +@register.filter('solution_by_id') +def solution_by_id(solution_id): + return Solution.objects.get(id=solution_id) + + +@register.filter('solution_file_text') +def solution_file_text(solution, filename): + files = solution.user_files + for key in files.keys(): + value = files[key] + if key.endswith(filename): + return value + raise Exception(f'No such file for solution {solution.id} and filename {filename}') diff --git a/Main/tests.py b/Main/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Main/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Main/views.py b/Main/views.py new file mode 100644 index 0000000..eb26aa3 --- /dev/null +++ b/Main/views.py @@ -0,0 +1,787 @@ +from django.core.exceptions import ObjectDoesNotExist +from json import load, dumps, loads +from os import remove, mkdir, listdir, rename +from os.path import sep, join, exists, isfile, dirname +from shutil import rmtree, copytree, make_archive, copyfile +from threading import Thread +from zipfile import ZipFile, BadZipFile +from datetime import datetime, timedelta + +from django.contrib.auth import login, authenticate, logout +from django.core.exceptions import ObjectDoesNotExist +from django.http import HttpResponseRedirect, HttpResponse +from django.shortcuts import render +from django.utils import timezone +from django.utils.timezone import make_aware + +from Main.templatetags.filters import * +from Sprint.settings import MEDIA_ROOT +from .Tester import Tester +from .forms import * +from .main import solutions_filter, check_admin_on_course, re_test, check_admin, check_teacher, random_string, \ + send_email, check_permission_block, is_integer, check_god, blocks_available, check_login, \ + get_restore_hash, block_solutions_info, solution_path, can_send_solution, get_in_html_tag, register_user, \ + check_cheating, result_comparer +from .models import Block, Subscribe, Course, Restore, ExtraFile, Message + + +def download_rating(request): + block = Block.objects.get(id=request.GET['block_id']) + if not check_admin_on_course(request.user, block.course): + return HttpResponseRedirect('/main') + s = 'ФИО,' + tasks = block.tasks + users = [UserInfo.objects.get(user=user.user) for user in Subscribe.objects.filter(course=block.course, is_assistant=False, user__is_staff=False)] + for task in tasks: + try: + s += task.name.split('.')[0] + ',' + except IndexError: + s += task.name + ',' + s += 'Score,Summ\n' + for user in users: + s += str(user) + ',' + for task in tasks: + s += str(mark_for_task(task, user.user)) + ',' + s += '0,' + str(mark_for_block(block, user.user)) + '\n' + response = HttpResponse(bytes(s, encoding='utf-8'), content_type='application/force-download') + response['Content-Disposition'] = 'inline; filename=rating.csv' + return response + + +def download(request): + if not check_admin(request.user) or not check_admin_on_course(request.user, Block.objects.get(id=request.GET['block_id']).course): + return HttpResponseRedirect('/main') + sols = solutions_filter(request.GET) + if len(sols) == 0: + return HttpResponseRedirect('/admin/solutions?block_id=' + request.GET['block_id']) + new_folder = join(MEDIA_ROOT, request.user.username) + if exists(new_folder): + try: + rmtree(new_folder) + except: + remove(new_folder) + mkdir(new_folder) + cur_folder = join(new_folder, 'solutions') + mkdir(cur_folder) + for sol in sols: + uinfo = UserInfo.objects.get(user=sol.user) + folder = join(cur_folder, str(uinfo) + '-' + str(uinfo.user.id)) + if not exists(folder): + mkdir(folder) + files = sol.files + files_for_compilation = [f.filename for f in sol.task.files_for_compilation] + for f in files.copy().keys(): + if f.split(sep)[-1] in files_for_compilation: + del files[f] + if len(files.keys()) == 1: + dest = join(folder, "-".join([sol.task.name.split('.')[0], str(sol.id), 'dotnet', sol.result.replace('/', 'from')])) + '.cs' + source = join(sol.path(), list(files.keys())[0]) + with open(dest, 'wb') as fs: + fs.write(open(source, 'rb').read()) + else: + newname = join(folder, '-'.join([sol.task.name.split('.')[0], str(sol.id), 'dotnet', sol.result])).replace('/', 'from') + mkdir(newname) + for f in files.keys(): + with open(join(newname, f.split(sep)[-1]), 'wb') as fs: + fs.write(open(join(sol.path(), f), 'rb').read()) + make_archive(newname, 'zip', newname) + rmtree(newname) + zip_folder = join(dirname(cur_folder), 'solutions') + make_archive(zip_folder, 'zip', cur_folder) + response = HttpResponse(open(zip_folder + '.zip', 'rb').read(), content_type='application/force-download') + response['Content-Disposition'] = 'inline; filename=solutions.zip' + rmtree(dirname(cur_folder)) + return response + + +def queue(request): + block = Block.objects.get(id=request.GET['block_id']) + if not check_admin_on_course(request.user, block.course): + return HttpResponseRedirect('/main') + return render(request, 'queue.html', {'Block': block}) + + +def docs(request): + if not check_admin(request.user): + return HttpResponseRedirect('/main') + return render(request, "docs.html", context={'is_teacher': check_teacher(request.user)}) + + +def retest(request): + solutions_request = solutions_filter(request.GET) + if not check_admin_on_course(request.user, Block.objects.get(id=request.GET['block_id']).course): + return HttpResponseRedirect('/main') + req = '?block_id=' + str(request.GET['block_id']) + for key in request.GET.keys(): + if key != 'block_id': + req += '&{}={}'.format(key, request.GET[key]) + Thread(target=lambda: re_test(solutions_request, request)).start() + if 'next' in request.GET.keys(): + return HttpResponseRedirect(request.GET['next']) + return HttpResponseRedirect('/admin/solutions%s' % req) + + +def solution(request): + current_solution = Solution.objects.get(id=request.GET['id']) + if not request.user.is_authenticated: + return HttpResponseRedirect('/main') + if current_solution.user != request.user: + try: + Subscribe.objects.get(user=request.user, is_assistant=True, course=current_solution.task.block.course) + except: + if not request.user.is_superuser: + return HttpResponseRedirect('/main') + can_edit = check_admin_on_course(request.user, current_solution.task.block.course) + if not can_edit: + # тут по хорошему надо использовать регулярки, но я что-то не разобрался + while True: + i = current_solution.details.find('
')
+            if i == -1:
+                break
+            j = current_solution.details.find('
') + 6 + current_solution.details = current_solution.details.replace(current_solution.details[i:j], '') + if current_solution.user != request.user: + return HttpResponseRedirect('/main') + solutions_request = solutions_filter(request.GET) + if request.path == '/admin/solution': + from_admin = True + else: + from_admin = False + if request.method == 'POST' and can_edit: + if request.POST['action'] == 'Зачесть': + current_solution.mark = None if request.POST['mark'] == 'нет оценки' else int(request.POST['mark']) + elif request.POST['action'] == 'Незачесть': + current_solution.mark = 0 + else: + current_solution.mark = current_solution.task.max_mark + current_solution.comment = request.POST['comment'] + current_solution.save() + return render(request, 'solution.html', context={'solution': current_solution, + 'from_admin': from_admin, + 'can_edit': can_edit, + 'path': request.path}) + + +def solutions(request): + current_block = Block.objects.get(id=request.GET['block_id']) + try: + if not request.user.is_superuser: + s = Subscribe.objects.get(user=request.user, course=current_block.course) + if not s.is_assistant and not s.user.is_staff: + return HttpResponseRedirect('/main') + except ObjectDoesNotExist: + return HttpResponseRedirect('/main') + req = '' + sols = solutions_filter(request.GET) + for key in request.GET.keys(): + req += '&{}={}'.format(key, request.GET[key]) + if request.method == 'POST': + Solution.objects.get(id=request.POST['DELETE_SOLUTION']).delete() + return HttpResponseRedirect('/admin/solutions?block_id={}{}'.format(current_block.id, req)) + return render(request, 'solutions.html', context={'Block': current_block, + 'filter': ' '.join([str(sol.id) for sol in sols]), + 'solutions': sols, + 'req': req, + 'options': {key: request.GET[key] for key in request.GET.keys()}, + 'solutions_info': block_solutions_info(current_block)}) + + +def messages(request): + return HttpResponseRedirect('/main') + current_block = Block.objects.get(id=request.GET['block_id']) + return render(request, 'messages.html', context={'Block': current_block}) + + +def users_settings(request): + current_course = Course.objects.get(id=request.GET['course_id']) + if not check_admin_on_course(request.user, current_course): + if not request.user.is_superuser: + return HttpResponseRedirect('/main') + if request.method == 'POST': + if 'input' in request.POST.keys(): + line = request.POST['input'] + if '@' in line: + users = UserInfo.objects.filter(user__email=line) + elif any(c.isdigit() for c in line): + users = UserInfo.objects.filter(group=line) + else: + try: + s, n, m = line.split(' ') + except ValueError: + s, n, m = '', '', '' + users = list(UserInfo.objects.filter(surname=s, name=n, middle_name=m)) + list(UserInfo.objects.filter(group=line)) + for user in users: + try: + Subscribe.objects.get(user=user.user, course=current_course) + except ObjectDoesNotExist: + Subscribe.objects.create(user=user.user, course=current_course) + elif 'user_delete' in request.POST.keys(): + username = request.POST['user_delete'] + course_id = request.GET['course_id'] + Subscribe.objects.get(user__email=username, course_id=course_id).delete() + elif 'file' in request.FILES.keys(): + users = load(request.FILES['file']) + for u in users: + password = random_string() + flag = False + try: + user = User.objects.get(email=u['email']) + except ObjectDoesNotExist: + flag = True + if flag: + user = register_user(u) + try: + Subscribe.objects.get(user=user, course=current_course) + except ObjectDoesNotExist: + Subscribe.objects.create(user=user, course=current_course, is_assistant=False) + else: + key = list(request.POST.keys())[1] + role = request.POST[key] + username = '_'.join(key.split('_')[1:]) + s = Subscribe.objects.get(user__email=username, course=current_course) + s.is_assistant = role == 'Ассистент' + s.save() + return HttpResponseRedirect('/admin/users_settings?course_id=' + str(current_course.id)) + return render(request, 'users_settings.html', context={'course': current_course}) + + +def task(request): + current_task = Task.objects.get(id=request.GET['id']) + user = request.user + if not check_permission_block(user, current_task.block): + return HttpResponseRedirect('/main') + can_send = can_send_solution(user, current_task) + if request.method == 'POST': + if 'message' in request.POST.keys(): + Message.objects.create( + task=current_task, + sender=request.user, + reply_to=None, + for_all=False, + text=request.POST['message'] + ) + return HttpResponseRedirect('/task?id=' + str(current_task.id)) + if 'file' in request.FILES.keys() and can_send: + current_solution = Solution.objects.create( + task=current_task, + user=request.user, + result='IN QUEUE', + time_sent=timezone.now() + ) + log_file_path = current_solution.log_file + with open(log_file_path, 'wb') as fs: + pass + solution_dir = current_solution.path() + sep + if exists(solution_dir): + rmtree(solution_dir) + mkdir(solution_dir) + with open(solution_dir + 'solution.zip', 'wb') as fs: + for chunk in request.FILES['file'].chunks(): + fs.write(chunk) + flag = False + solution_created = False + try: + with ZipFile(solution_dir + 'solution.zip') as obj: + obj.extractall(solution_dir) + except BadZipFile: + rename(solution_dir + 'solution.zip', solution_dir + request.FILES['file'].name) + sln_path = solution_path(solution_dir) + if current_task.full_solution != bool(sln_path): + current_solution.result = 'TEST ERROR' + with open(log_file_path, 'ab') as fs: + fs.write(b'Can\'t find sln file in solution' if current_task.full_solution else b'Sln file in path') + current_solution.save() + return HttpResponseRedirect('/task?id=' + str(current_task.id)) + if not bool(sln_path): + copytree('SampleSolution', join(solution_dir, 'Solution')) + for file in listdir(solution_dir): + if file == 'solution.zip' or file == 'Solution': + continue + cur_file = join(solution_dir, file) + if isfile(cur_file): + copyfile(cur_file, join(solution_dir, 'Solution', 'SampleProject', file)) + remove(cur_file) + else: + rmtree(cur_file) + if not current_task.full_solution: + for file in current_task.files_for_compilation: + copyfile(file.path, join(solution_dir, 'Solution', 'SampleProject', file.filename)) + #Tester(current_solution, request.META['HTTP_HOST']).push() + Thread(target=lambda: Tester(current_solution, request.META['HTTP_HOST']).push()).start() + return HttpResponseRedirect('/task?id=' + str(current_task.id)) + return render(request, 'task.html', context={'task': current_task, + 'solutions': reversed(Solution.objects.filter(task=current_task, user=user)), + 'can_send': can_send, + 'can_edit': check_admin_on_course(request.user, current_task.block.course)}) + + +from django.views.decorators.csrf import csrf_exempt + + +@csrf_exempt +def task_test(request): + current_task = Task.objects.get(id=request.GET['id']) + user = request.user + if not check_permission_block(user, current_task.block): + return HttpResponseRedirect('/main') + can_send = can_send_solution(user, current_task) + if request.method == 'POST': + if 'file' in request.FILES.keys() and can_send: + current_solution = Solution.objects.create( + task=current_task, + user=request.user, + result='IN QUEUE', + time_sent=timezone.now() + ) + log_file_path = current_solution.log_file + with open(log_file_path, 'wb') as fs: + pass + solution_dir = current_solution.path() + sep + if exists(solution_dir): + rmtree(solution_dir) + mkdir(solution_dir) + with open(solution_dir + 'solution.zip', 'wb') as fs: + for chunk in request.FILES['file'].chunks(): + fs.write(chunk) + flag = False + solution_created = False + try: + with ZipFile(solution_dir + 'solution.zip') as obj: + obj.extractall(solution_dir) + except BadZipFile: + rename(solution_dir + 'solution.zip', solution_dir + request.FILES['file'].name) + sln_path = solution_path(solution_dir) + if current_task.full_solution != bool(sln_path): + current_solution.result = 'TEST ERROR' + with open(log_file_path, 'ab') as fs: + fs.write(b'Can\'t find sln file in solution' if current_task.full_solution else b'Sln file in path') + current_solution.save() + return HttpResponseRedirect('/task?id=' + str(current_task.id)) + if not bool(sln_path): + copytree('SampleSolution', join(solution_dir, 'Solution')) + for file in listdir(solution_dir): + if file == 'solution.zip' or file == 'Solution': + continue + cur_file = join(solution_dir, file) + if isfile(cur_file): + copyfile(cur_file, join(solution_dir, 'Solution', 'SampleProject', file)) + remove(cur_file) + else: + rmtree(cur_file) + if not current_task.full_solution: + for file in current_task.files_for_compilation: + copyfile(file.path, join(solution_dir, 'Solution', 'SampleProject', file.filename)) + #Tester(current_solution, request.META['HTTP_HOST']).push() + Thread(target=lambda: Tester(current_solution, request.META['HTTP_HOST']).push()).start() + return HttpResponseRedirect('/task?id=' + str(current_task.id)) + + +def block(request): + current_block = Block.objects.get(id=request.GET['id']) + if not check_permission_block(request.user, current_block): + return HttpResponseRedirect('/main') + return render(request, 'block.html', context={'Block': current_block, + 'is_admin': check_admin(request.user), + 'can_edit': check_admin_on_course(request.user, current_block.course), + 'user': request.user}) + + +def task_settings(request): + if not check_admin(request.user): + return HttpResponseRedirect('/main') + current_task = Task.objects.get(id=request.GET['id']) + if request.method == 'POST': + action = request.POST['ACTION'] + if action == 'DELETE': + t = Task.objects.get(id=request.GET['id']) + block_id = t.block.id + t.delete() + return HttpResponseRedirect('/admin/block?id=' + str(block_id)) + elif action.startswith('SAVE_EXTRA_FILE_'): + i = action.split('_')[-1] + ef = ExtraFile.objects.get(id=int(i)) + with open(ef.path, 'wb') as fs: + file_text = request.POST['extra_file_text_' + i] + fs.write(bytes(file_text, encoding='utf-8')) + ef.for_compilation = '{}_for_compilation'.format(ef.id) in request.POST.keys() + ef.save() + elif action == 'SAVE': + current_task.legend, current_task.input, current_task.output, current_task.specifications = \ + request.POST['legend'], request.POST['input'], request.POST['output'], request.POST['specifications'] + current_task.time_limit = int(request.POST['time_limit']) if is_integer(request.POST['time_limit']) else 10000 + current_task.show_details = 'show_details' in request.POST.keys() + current_task.full_solution = 'full_solution' in request.POST.keys() + current_task.mark_formula = request.POST['mark_formula'] + current_task.show_result = 'show_result' in request.POST.keys() + current_task.priority = request.POST['priority'] + for ef in ExtraFile.objects.filter(task=current_task): + ef.sample = 'sample_' + str(ef.id) in request.POST.keys() + ef.save() + try: + current_task.weight = float(request.POST['weight'].replace(',', '.')) + except ValueError: + current_task.weight = 1.0 + try: + current_task.max_mark = int(request.POST['max_mark']) + if current_task.max_mark == 0: + raise ValueError + except ValueError: + current_task.max_mark = 10 + try: + current_task.max_solutions_count = int(request.POST['max_solutions_count']) + except ValueError: + current_task.max_solutions_count = 10 + + elif action == 'UPLOAD_EXTRA_FILE': + if request.FILES['file'].name.endswith('.zip'): + try: + wdir = join(MEDIA_ROOT, 'extra_files', 'files' + str(current_task.id)) + if exists(wdir): + rmtree(wdir) + mkdir(wdir) + with open(join(wdir, 'file.zip'), 'wb') as fs: + for chunk in request.FILES['file'].chunks(): + fs.write(chunk) + with ZipFile(join(wdir, 'file.zip')) as obj: + obj.extractall(wdir) + remove(join(wdir, 'file.zip')) + for file in listdir(wdir): + if isfile(join(wdir, file)): + try: + ef = ExtraFile.objects.get(filename=file, task=current_task) + except ObjectDoesNotExist: + ef = ExtraFile.objects.create(filename=file, task=current_task) + ef.write(open(join(wdir, file), 'rb').read()) + rmtree(wdir) + except BadZipFile: + pass + else: + try: + ef = ExtraFile.objects.get(filename=request.FILES['file'].name, task=current_task) + except ObjectDoesNotExist: + ef = ExtraFile.objects.create(filename=request.FILES['file'].name, task=current_task) + with open(ef.path, 'wb') as fs: + for chunk in request.FILES['file'].chunks(): + fs.write(chunk) + elif action == 'CREATE_EXTRA_FILE': + try: + ExtraFile.objects.get(task=current_task, filename=request.POST['newfile_name']) + except ObjectDoesNotExist: + ef = ExtraFile.objects.create(task=current_task, filename=request.POST['newfile_name']) + f = open(join(MEDIA_ROOT, 'extra_files', str(ef.id)), 'w') + f.close() + elif action.startswith('DELETE_FILE_'): + ExtraFile.objects.get(id=int(action.split('_')[-1])).delete() + elif action == 'SAVE_TESTS': + tt = request.POST['tests_text'] + cs_file = current_task.tests_path() + with open(cs_file, 'wb') as fs: + fs.write(bytes(tt, encoding='utf-8')) + else: + raise NotImplementedError() + current_task.save() + return HttpResponseRedirect('/admin/task?id=' + str(current_task.id)) + return render(request, 'task_settings.html', context={'task': current_task, + 'tests': TestsForm(), + 'is_superuser': check_teacher(request.user)}) + + +def block_settings(request): + if not check_admin(request.user): + return HttpResponseRedirect('/main') + current_block = Block.objects.get(id=request.GET['id']) + if not check_permission_block(request.user, current_block): + return HttpResponseRedirect('/main') + if request.method == 'POST': + if 'name' in request.POST.keys(): + current_block = Block.objects.get(id=request.POST['block_id']) + if not check_teacher(request.user) or not check_permission_block(request.user, current_block): + return HttpResponseRedirect('/main') + task_name = request.POST['name'] + current_task = Task.objects.create( + name=task_name, + block=current_block + ) + with open(current_task.tests_path(), 'w') as fs: + pass + return HttpResponseRedirect('/admin/task?id=' + str(current_task.id)) + if 'file' in request.FILES.keys(): + if exists(request.user.username): + rmtree(request.user.username) + mkdir(request.user.username) + with open(join(request.user.username, 'task.zip'), 'wb') as fs: + for chunk in request.FILES['file'].chunks(): + fs.write(chunk) + try: + with ZipFile(join(request.user.username, 'task.zip')) as obj: + obj.extractall(request.user.username) + except BadZipFile: + rmtree(request.user.username) + return HttpResponseRedirect('/admin/block?id={}'.format(current_block.id)) + task = Task.objects.create(name='Новый таск', block=current_block) + root = request.user.username + if exists(join(root, 'meta.json')): + data = loads(open(join(root, 'meta.json'), 'r', encoding='utf-8').read()) + task.name = data['localizedNames']['ru'] + task.time_limit = data['invocationLimits']['idlenessLimitMillis'] + for f in sorted(listdir(join(root, 'tests'))): + e = ExtraFile.objects.create( + task=task, + filename=f, + for_compilation=False + ) + try: + e.sample=data['testSets'][str(int(f.split('.')[0]))]['example'] and not f.endswith('.a') + except KeyError: + e.sample = False + e.save() + copyfile(join(root, 'tests', f), join(MEDIA_ROOT, 'extra_files', str(e.id))) + statements = open(join(root, 'statements', 'ru', 'html', 'statement.html'), 'r', encoding='utf-8').read() + task.legend = get_in_html_tag(statements, 'legend') + task.input = get_in_html_tag(statements, 'input-specification') + task.output = get_in_html_tag(statements, 'output-specification') + task.specifications = get_in_html_tag(statements, 'notes') + task.save() + with open(join(MEDIA_ROOT, 'tests', str(task.id) + '.cs'), 'w') as fs: + pass + rmtree(root) + return HttpResponseRedirect('/admin/task?id={}'.format(task.id)) + if 'block_delete' in request.POST.keys(): + Block.objects.get(id=request.POST['block_delete']).delete() + return HttpResponseRedirect('/admin/main') + else: + time_start = make_aware(datetime.strptime(request.POST['time_start'], "%Y-%m-%dT%H:%M") + timedelta(hours=3)) + time_end = make_aware(datetime.strptime(request.POST['time_end'], "%Y-%m-%dT%H:%M") + timedelta(hours=3)) + current_block.opened = 'opened' in request.POST.keys() + current_block.time_start = time_start + current_block.time_end = time_end + current_block.show_rating = "rating" in request.POST.keys() + current_block.priority = request.POST['priority'] + current_block.save() + return HttpResponseRedirect('/admin/block?id={}'.format(current_block.id)) + return render(request, 'block_settings.html', context={'is_superuser': check_teacher(request.user), + 'Block': current_block}) + + +def solutions_table(request): + current_task = Task.objects.get(id=request.GET['id']) + user = request.user + if not check_permission_block(user, current_task.block): + return HttpResponse("done") + sols = Solution.objects.filter(task=current_task, user=user) + can_edit = check_admin_on_course(request.user, current_task.block.course) + # тут по хорошему надо использовать регулярки, но я что-то не разобрался + if not can_edit: + for sol in sols: + while True: + i = sol.details.find('
')
+                if i == -1:
+                    break
+                j = sol.details.find('
') + 6 + sol.details = sol.details.replace(sol.details[i:j], '') + if any(sol.result == 'TESTING' or sol.result == 'IN QUEUE' for sol in sols) or 'render' in request.GET.keys(): + return render(request, 'solutions_table.html', context={ + 'solutions': reversed(sols), + 'can_edit': can_edit, + 'task': current_task}) + return HttpResponse('done') + + +def queue_table(request): + block = Block.objects.get(id=request.GET['block_id']) + if not check_admin_on_course(request.user, block): + return HttpResponse('get away from here') + sols = list(Solution.objects.filter(task__block=block, result='TESTING')) + list(Solution.objects.filter(task__block=block, result='IN QUEUE')) + return render(request, 'queue_table.html', {'solutions': sorted(sols, key=lambda x: -x.task.block.priority * 10 - x.task.priority)}) + + +def get_result_data(request): + solution = Solution.objects.get(id=request.GET['id']) + if not check_admin_on_course(request.user, solution.task.block.course): + return HttpResponse(dumps({'success': False})) + return HttpResponse(dumps({ + 'success': True, + 'results_text': solution.details, + 'tests_text': solution.task.tests_text, + 'log_text': solution.log_text + })) + + +def get_comment_data(request): + solution = Solution.objects.get(id=request.GET['id']) + if not check_admin_on_course(request.user, solution.task.block.course): + return HttpResponse(dumps({'success': False})) + return HttpResponse(dumps({ + 'success': True, + 'comment_text': solution.comment + })) + + +def rating(request): + current_block = Block.objects.get(id=request.GET['block_id']) + if not check_admin_on_course(request.user, current_block.course) and not current_block.show_rating: + return HttpResponseRedirect('/main') + return render(request, 'rating.html', context={'Block': Block.objects.get(id=request.GET['block_id']), 'admin_course': check_admin_on_course(request.user, current_block.course)}) + + +def cheating(request): + current_block = Block.objects.get(id=request.GET['block_id']) + if not check_admin_on_course(request.user, current_block.course): + return HttpResponseRedirect('/main') + if request.method == 'POST': + if not current_block.cheating_checking: + req = ['_'.join(elem.split('_')[1:]) for elem in request.POST.keys() if elem.startswith('check_')] + tasks = Task.objects.filter(id__in=[int(elem.split('_')[1]) for elem in req if elem.startswith('task')]) + users = User.objects.filter(id__in=[int(elem.split('_')[1]) for elem in req if elem.startswith('user')]) + solutions = Solution.objects.filter(user__in=users).filter(task__in=tasks) + if 'all_tests' in request.POST.keys(): + solutions = [sol for sol in solutions if sol.passed_all_tests] + if 'best_result' in request.POST.keys(): + sols = {} + for solution in solutions: + if (solution.user.username, solution.task.id) in sols.keys(): + comp = result_comparer(sols[(solution.user.username, solution.task.id)][0].result, solution.result) + if comp == 1: + sols[(solution.user.username, solution.task.id)] = [solution] + elif comp == 0: + sols[(solution.user.username, solution.task.id)].append(solution) + else: + sols[(solution.user.username, solution.task.id)] = [solution] + solutions = [val for sol in sols.values() for val in sol] + solutions = list(sorted(solutions, key=lambda s: s.id)) + if 'last_solution' in request.POST.keys(): + sols = {} + for sol in solutions: + pair = sol.user, sol.task + if pair not in sols.keys(): + sols[pair] = [] + sols[pair].append(sol) + solutions = [sols[key][len(sols[key]) - 1] for key in sols.keys()] + Thread(target=check_cheating, args=(solutions, current_block, int(request.POST['cheating_percent']))).start() + return HttpResponseRedirect('/admin/cheating?block_id=' + str(current_block.id)) + return render(request, 'cheating.html', {'Block': current_block}) + + +def admin(request): + if not check_admin(request.user): + return HttpResponseRedirect('/main') + if request.method == 'POST': + if 'invite' in request.POST.keys(): + register_user(request.POST) + return HttpResponseRedirect('/admin/main') + name = request.POST['name'] + course = Course.objects.get(id=request.POST['course_id']) + current_block = Block.objects.create(name=name, + course=course, + opened=False, + time_start=timezone.now(), + time_end=timezone.now()) + if not check_teacher(request.user): + return HttpResponseRedirect('/main') + try: + Subscribe.objects.get(user=request.user, course=course) + except ObjectDoesNotExist: + if not request.user.is_superuser: + return HttpResponseRedirect('/main') + return HttpResponseRedirect('/admin/block?id=' + str(current_block.id)) + return render(request, "admin.html", context={"blocks": blocks_available(request.user), + 'is_superuser': check_god(request.user), + 'is_teacher': check_teacher(request.user)}) + + +def reset_password(request): + code = request.GET['code'] + try: + res = Restore.objects.get(code=code) + except ObjectDoesNotExist: + return HttpResponseRedirect('/enter') + context = {'form': ResetPasswordForm()} + if request.method == 'GET': + return render(request, 'reset_password.html', context=context) + else: + if request.POST['new'] != request.POST['again']: + context['error'] = 'Пароли не совпадают' + return render(request, 'reset_password.html', context=context) + else: + res.user.set_password(request.POST['new']) + res.user.save() + res.delete() + return HttpResponseRedirect('/enter') + + +def settings(request): + if not check_login(request.user): + return HttpResponseRedirect('/enter') + context = {'is_admin': check_admin(request.user), 'form': ChangePasswordForm()} + if request.method == 'POST': + old = request.POST['old'] + new = request.POST['new'].strip() + again = request.POST['again'].strip() + username = request.user.username + user = request.user + if user is None: + context['error'] = 'Неверный пароль' + if len(new) < 8 or not any([a.isdigit() for a in new]) or new.lower() == new: + context['error'] = 'Пароль слишком слабый' + elif new != again: + context['error'] = 'Пароли не совпадают' + elif new == '' or new.replace(' ', '') == '': + context['error'] = 'Некорректный пароль' + else: + user.set_password(new) + user.save() + context['error'] = 'Пароль успешно изменен' + login(request, user) + return render(request, 'settings.html', context=context) + + +def exit(request): + logout(request) + return HttpResponseRedirect('/enter') + + +def redirect(request): + return HttpResponseRedirect('/main') + + +def main(request): + if not check_login(request.user): + return HttpResponseRedirect('/enter') + return render(request, 'main.html', context={'blocks': blocks_available(request.user)}) + + +def restore(request): + if check_login(request.user): + return HttpResponseRedirect('/main') + elif request.method == 'GET': + return render(request, 'restore.html') + else: + email = request.POST['email'] + try: + user = User.objects.get(email=email) + except ObjectDoesNotExist: + return HttpResponseRedirect('/enter') + h = get_restore_hash() + try: + r = Restore.objects.get(user__email=email) + r.code = h + r.save() + except ObjectDoesNotExist: + Restore.objects.create(user=user, code=h) + send_email('Reset password', + email, + 'Restore your password using this link:\nhttp://{}/reset_password?code={}' + .format(request.META['HTTP_HOST'], h)) + return HttpResponseRedirect('/enter') + + +def enter(request): + if check_login(request.user): + return HttpResponseRedirect('/main') + if request.method == 'POST': + user = authenticate(username=request.POST['email'].strip(), password=request.POST['password'].strip()) + if user is not None: + login(request, user) + return HttpResponseRedirect('/enter') + else: + return render(request, "enter.html", context={"form": LoginForm()}) diff --git a/Sprint/__init__.py b/Sprint/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Sprint/asgi.py b/Sprint/asgi.py new file mode 100644 index 0000000..4078f7f --- /dev/null +++ b/Sprint/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for Sprint project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Sprint.settings') + +application = get_asgi_application() diff --git a/Sprint/settings.py b/Sprint/settings.py new file mode 100644 index 0000000..34167a5 --- /dev/null +++ b/Sprint/settings.py @@ -0,0 +1,153 @@ +""" +Django settings for Sprint project. + +Generated by 'django-admin startproject' using Django 3.0.7. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.0/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '-w#*mn6*fa8a=(-c0@klx&$vl%hpiy&l(u*3%0a#2)wdt##(z2' + +DEPLOY = False + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = not DEPLOY + +SECURE_SSL_REDIRECT = DEPLOY + + +ALLOWED_HOSTS = [ + '*' +] + + +# Application definition + +INSTALLED_APPS = [ + 'grappelli', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'Main.apps.MainConfig' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.locale.LocaleMiddleware', +] + +ROOT_URLCONF = 'Sprint.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')] + , + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'Main.context_processors.attributes' + ], + }, + }, +] + +WSGI_APPLICATION = 'Sprint.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ + +_ = lambda s: s + +LANGUAGES = ( + ('en', _('English')), + ('ru', _('Russian')) +) + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'Europe/Moscow' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + +STATIC_URL = '/static/' + +STATIC_ROOT = os.path.join(BASE_DIR, "static_root") + +MEDIA_ROOT = os.path.join(BASE_DIR, 'data') + +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "Main/static"), +] + + +# Authentication backends +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + ) diff --git a/Sprint/urls.py b/Sprint/urls.py new file mode 100644 index 0000000..ffa122f --- /dev/null +++ b/Sprint/urls.py @@ -0,0 +1,38 @@ +from django.contrib import admin +from django.urls import path, re_path, include +from Main import views + +urlpatterns = [ + path('grappelli/', include('grappelli.urls')), # grappelli URLS + path('main', views.main), + path('settings', views.settings), + path('enter', views.enter, name='enter'), + path('restore', views.restore, name='restore'), + path('reset_password', views.reset_password), + path('exit', views.exit), + path('block', views.block), + path('task', views.task), + path('solution', views.solution), + path('rating', views.rating), + path('messages', views.messages), + path('admin/rating', views.rating), + path('admin/download_rating', views.download_rating), + path('admin/solution', views.solution), + path('admin/retest', views.retest), + path('admin/docs', views.docs), + path('admin/block', views.block_settings), + path('admin/task', views.task_settings), + path('admin/main', views.admin), + path('admin/solutions', views.solutions), + path('admin/users_settings', views.users_settings), + path('admin/download', views.download), + path('admin/queue', views.queue), + path('admin/cheating', views.cheating), + path('queue_table', views.queue_table), + path('task_test', views.task_test), + path('solutions_table', views.solutions_table), + path('get_result_data', views.get_result_data), + path('get_comment_data', views.get_comment_data), + path('admin/', admin.site.urls), + re_path('^', views.redirect) +] diff --git a/Sprint/wsgi.py b/Sprint/wsgi.py new file mode 100644 index 0000000..37c78bf --- /dev/null +++ b/Sprint/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for Sprint project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Sprint.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..4cc5db2 --- /dev/null +++ b/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Sprint.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..2c6af90 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,90 @@ +{% extends 'base.html' %} + +{% block title %}Админка{% endblock %} + +{% block styles %} + .form { + margin-bottom: 15px; + } +{% endblock %} + +{% block scripts %} + function change() { + let surname = document.getElementById('surname').value; + let name = document.getElementById('name').value; + let middle_name = document.getElementById('middle_name').value; + let group = document.getElementById('group').value; + let email = document.getElementById('email').value; + let invite = document.getElementById('inviteButton'); + console.log(surname + ' ' + name + ' ' + middle_name + ' ' + group + ' ' + email); + let dis = surname == '' || name == '' || middle_name == '' || group == '' || email == '' || email.indexOf('@') == -1 || email.indexOf('.') == -1 || email.indexOf('.') < email.indexOf('@'); + console.log(dis); + invite.disabled = dis; + } +{% endblock %} + +{% block content %} +

Доступные курсы

+ {% for key, value in blocks.items %} +
{{ key.name }}
+ {% for block in value %} + {{ block.name }}
+ {% endfor %} + {% if is_teacher %} + + + + + {% endif %} + {% endfor %} + {% if is_superuser %} +

Пригласить пользователя

+
+ {% csrf_token %} +
+
+
+
+
+ +
+
+
+ +
+
+ {% endif %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..535b5f1 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,62 @@ + +{% load static %} + + + + + Sprint | {% block title %}{% endblock %} + + {% block links %} + {% endblock %} + + + + + + + + + + + + + + + + + + +
+ + {% block content %}{% endblock %} +
+ + \ No newline at end of file diff --git a/templates/block.html b/templates/block.html new file mode 100644 index 0000000..a4faf45 --- /dev/null +++ b/templates/block.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}{{ Block.name }}{% endblock %} + +{% block content %} +

{{ Block.name }} {% if can_edit %} {% endif %}

+
Таски
+ + {% for task in Block.tasks %} + + + + + {% endfor %} +
+ {{ task.name }} + + {% with mark=task|mark_for_task:user %} + {% if mark|marked %} +
+ {{ mark }} +
+ {% endif %} + {% endwith %} +
+ {% if Block.show_rating %} + Рейтинг + {% endif %} + Сообщения +
+ {% if Block.opened %} + Открыто для просмотра + {% else %} + Закрыто для просмотра + {% endif %} +
Доступно с {{ Block.time_start }} до {{ Block.time_end }} +{% endblock %} \ No newline at end of file diff --git a/templates/block_settings.html b/templates/block_settings.html new file mode 100644 index 0000000..68bfc6b --- /dev/null +++ b/templates/block_settings.html @@ -0,0 +1,126 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}{{ Block.name }}|настройки{% endblock %} + +{% block styles %} + input[type="file"] { + display: none; + } +{% endblock %} + +{% block scripts %} + function uploaded() { + document.getElementById('is_uploaded').style.display = 'block'; + document.getElementById('is_uploaded').nodeValue = document.getElementById('file-upload').nodeValue; + } +{% endblock %} + +{% block content %} +

{{ Block.name }}

+
Таски
+ {% for task in Block.tasks %} + {{ task.name }}
+ {% endfor %} + {% if is_superuser %} + + + +
+
Импортировать задачу из Я.Контест
+
+ {% csrf_token %} + + + +
+
+ {% endif %} +
+ {% if is_superuser %} +

Ограничения по времени

+
+ {% csrf_token %} + + + + + + + + + + + + + + + +
+ + + +
+ Открыто для просмотра +
+ Показывать рейтинг участникам +
+ Приоритет + +
+ +
+
+ {% endif %} +
+ Рейтинг + + Очередь тестирования + Проверка на списывание + {% if is_superuser %} +
+ {% csrf_token %} + + +
+ {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/cheating.html b/templates/cheating.html new file mode 100644 index 0000000..a524620 --- /dev/null +++ b/templates/cheating.html @@ -0,0 +1,133 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}{{ Block.name }} | списывание{% endblock %} + +{% block links %} + + + +{% endblock %} + +{% block scripts %} + function check_list(entity_type) { + var cbox = document.getElementById('check_' + entity_type + '_all'); + var boxes = document.getElementsByClassName('check_' + entity_type); + for (var i = 0; i < boxes.length; i++) { + boxes[i].checked = cbox.checked; + } + } + + function prepare_for_submit() { + var conf = confirm('Точно запускаем проверку? Новую проверку можно будет запустить только после завершения данной'); + if (conf) { + document.getElementById('main_form').submit(); + } + } +{% endblock %} + +{% block content %} +

Списывание в {{ Block.name }}

+
+
+ {% csrf_token %} +
Проверить таски
+ Все
+ {% for task in Block.tasks %} + {{ task.name }}
+ {% endfor %} +
+
Проверить пользователей
+ Все
+ {% for sub in Block.course.students %} + {{ sub.user.userinfo }}
+ {% endfor %} +
+
Дополнительно
+ + + + + + + + + + + + + + + + + +
+ + + Проверять только решения с лучшим результатом +
+ + + Проверять только последнее решение +
+ + + Проверять только прошедшие все тесты +
+ + + Какой процент схожести считать списыванием +
+
+ +
+
+

{{ Block.cheating_status }}

+ {% if Block.cheating_checked %} +

Результаты проверки

+ {% for data in Block.cheating_results %} + {% with user=data|user_by_id cheating_data=Block.cheating_results|dict_key:data %} +

{{ user.userinfo }} [{{ cheating_data|length }}]

+ + + {% endwith %} + {% endfor %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/docs.html b/templates/docs.html new file mode 100644 index 0000000..cadb565 --- /dev/null +++ b/templates/docs.html @@ -0,0 +1,235 @@ +{% extends 'base.html' %} + +{% load static %} + +{% block title %}Методичка{% endblock %} + +{% block styles %} + .screenshot { + width: 100%; + } + .screenshot_div { + margin-top: 20px; + width: 80%; + margin-bottom: 20px; + } + .code { + padding: 15px; + margin-top: 15px; + margin-bottom: 15px; + } +{% endblock %} + +{% block content %} +

Методичка

+
+

О системе

+

Структура сервиса

+ В сервисе существует два режима работы: + + И одна компанента: + +
+

Режим студента

+ Режим студента показывает курсы, на которые подписан пользователь, а также блоки, содержащиеся в них.
+ Напротив каждого блока стоит (или отсутствует) оценка за данный блок. +
+ +
+ При нажатии на блок пользователь попадает в настройки выбранного блока.
+ На странице отображаются таски и (опционально) оценки за них.
+ Под списком тасков отображается информация о возможности отправки решений в блок, а также временной интервал активности блока. +
+ +
+ Страница таска представляет из себя подробное описание задания, предлагаемого к решению.
+ Описание таска содержит в себе следующие атрибуты: + + Далее находится форма для отправки решения (.zip архив) и список отправленных решений.
+ Решение содержит в себе следующие атрибуты: + + Sprint поддерживает следующие варианты результатов тестирования: + +
+ +
+ При нажатии на результат тестирование можно получить информацию о том, какие именно тесты были пройдены, а какие - нет. +
+ +
+ При нажатии на id решения пользователь попадает на страницу решения.
+ Страница с решением содержит ту же информацию, что и на странице таска.
+ Помимо этого, на данной странице можно выставить оценку за решение и оставить комментарий. Для сохранения результата необходимо нажать на кнопку "Зачесть".
+ Для выставления оценки 0 или максимальной оценки за таск, необходимо нажать на кнопку "Незачесть" или "Выставить максимальный балл" соответственно.
+ Комментарий будет сохранен при нажатии на любую из кнопок.
+ Внизу страницы отображаются файлы решения, содержащие только символы Unicode, иначе говоря, файлы с кодом. +
+ +
+
+

Режим администратора

+ Некоторые функции доступны только преподавателям
+ Главная страница режима администратора показывает доступные для администрирования курсы, блоки в них и ссылку на данную методичку.
+ Добавление нового блока в курс происходит по нажатии кнопки "Новый блок". +
+ +
+ При нажатии на кнопку "Участники курса" открывается страница с настройками участников курса.
+ Для подписки существующих студентов или преподавателей на курс, необходимо ввести в форму "Добавить участников" ФИО студента или email, на который зарегестрирован аккаунт.
+ Для подписки всех студентов определенной группы, нужно ввести название группы.
+ Для отписки студента от курса, нужно нажать на кнопку "отписка" рядом с нужным студентом.
+ Для того, чтобы сделать подписчика курса ассистентом или снятия с него таких прав, необходимо выбрать почту этого студента в форме "Назначить или разжаловать" и нажать на кнопку применить. Статус студента будет отображен в таблице ниже. +
+ +
+ В Sprint отсутствует регистрация в обычном понимании данного термина, поэтому регистрация студентов происходит при первом добавлении его к некоторому курсу.
+ Прежде всего необходимо создать JSON файл по определенным правилам, описанным на странице участников курса. +
+ +
+ При нажатии на блок, отображается страница с настройками блока.
+ Страница настроек блока содержит следующие аттрибуты: + + Ограничения по времени были внедрены для того, чтобы отложить запуск блока. Блок будет доступен студентам только если данный момент времени входит в интервал между временем начала и временем конца (границы включаются), и блок открыт для отправки решений.
+ После установки нужных значений необходимо нажать на кнопку "Сохранить".
+ Для создания нового таска после списка тасков есть кнопка "Новый таск". +
+ +
+ При нажатии на таск из списка, откроется страница с настройками таска.
+ Поля "Легенда", "Входные данные", "Выходные данные" и "Спецификации" заполняются с помощью html разметки, которая потом будет отображена в режиме студента.
+
+ +
+ В настройках таска содержатся еще несколько аттрибутов: + + В форму загрузки тестов загружается .cs файл с Unit тестами библиотеки Nunit. По нажатии на кнопку "Посмотреть" можно посмотреть тесты и отредактировать их. Для сохранения файла необходимо нажать на кнопку "Сохранить" внизу страницы.
+ В форму "Дополнительные файлы" можно загрузить файлы, которые будут скорированы в директорию с исполняемыми файлами при тестировании. +
+ +
+ При нажатии на кнопку "Посмотреть решения" на странице настроек блока открывается страница решений, загруженных в текущий блок.
+ Функциональность таблицы решений внизу страницы схожа с подобной в режиме студента.
+ Фильтр на данной странице содержит следующие поля: + + При нажатии на кнопку "Перетест" отфильтрованные решения будут перетестированы. +
+ +
+
+

Darwin

+ Одним из ограниений Unit тестирования является невозможность тестирования приватных методов и приватных классов.
+ Для решения данной проблемы в Sprint содержится компонента Darwin. Загрузить библиотеку можно по данной ссылке.
+ Darwin позволяет создавать объекты приватных классов, задавать значения приватным полям и свойствам, а также вызывать приватные методы.
+ Основой библиотеки является класс DObject.
+ В конструктор передается имя того класса, объект которого необходимо создать, с указанием пространства имен. +
+
+using Darwin;
+using Nunit.Framework;
+
+[TestFixture]
+public class Tests
+{
+    [Test]
+    public void Test1()
+    {
+        DObject d = new DObject("Calculator.Calc");
+    }
+}
+
+
+ Для доступа к полям и свойствам класса нужно использовать индексатор.
+
+
+using Darwin;
+using Nunit.Framework;
+
+[TestFixture]
+public class Tests
+{
+    [Test]
+    public void Test1()
+    {
+        DObject d = new DObject("Calculator.Calc");
+        d["a"] = 5;
+        Assert.AreEqual(d["a"], 5);
+    }
+}
+
+
+ Для вызова методов класса используется метод InvokeMethod.
+ В качетсве параметров передается имя метода и массив параметров.
+ Тип возвращаемого значения метода InvokeMethod - object, поэтому необходимо проводить каст к нужному типу после вызова. +
+
+using Darwin;
+using Nunit.Framework;
+
+[TestFixture]
+public class Tests
+{
+    [Test]
+    public void Test1()
+    {
+        DObject d = new DObject("Calculator.Calc");
+        d["a"] = 5;
+        Assert.AreEqual(d["a"], 5);
+        int val = (int)d.InvokeMethod("GetNumber", new object[] { d["a"] });
+        Assert.AreEqual(val, d["a"]);
+    }
+}
+
+
+
+

Use cases

+

Как собрать таск

+
+

Как написать Unit тест правильно

+
+

Как написать тест на входные и выходные данные

+
+

Что необходимо указать в спецификациях

+
+

Полезные html тэги

+{% endblock %} \ No newline at end of file diff --git a/templates/enter.html b/templates/enter.html new file mode 100644 index 0000000..4eba83d --- /dev/null +++ b/templates/enter.html @@ -0,0 +1,68 @@ + + {% load static %} + + + + + Sprint + + + + + + + + + +
+
+
+

+ Sprint +

+
+
+
+ {% csrf_token %} +
+
+ +
+
+
+ +
+
+
+ + \ No newline at end of file diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..7053e11 --- /dev/null +++ b/templates/main.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}Главная{% endblock %} + +{% block content %} +

Доступные курсы

+ {% for key, value in blocks.items %} +
{{ key.name }}
+ {% for block in value %} + + + + + +
+ {{ block.name }} + + {% with mark=block|mark_for_block:user %} + {% if mark|marked %} +
+ {{ mark }} +
+ {% endif %} + {% endwith %} +
+ {% endfor %} + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/messages.html b/templates/messages.html new file mode 100644 index 0000000..a233217 --- /dev/null +++ b/templates/messages.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block content %} +

Сообщения

+ {% for message in Block.messages %} +
+
+
+ {% with userinfo=message.sender|userinfo_by_user %} +
+ + {{ userinfo }} | {{ userinfo.group }} | + {{ message.task.name }} + {% if is_admin %} | Решения участника{% endif %} + +
+
+ {{ message.text }} +
+
+ + +
+ {% endwith %} +
+
+
+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/queue.html b/templates/queue.html new file mode 100644 index 0000000..9cc3e5c --- /dev/null +++ b/templates/queue.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}{{ Block.name }} | очередь{% endblock %} + +{% block scripts %} + function doPoll() { + jQuery.get('/queue_table?block_id={{ Block.id }}', function(data) { + document.getElementById('body').innerHTML = data; + setTimeout(function() {doPoll()}, 2000); + }) + } +{% endblock %} + +{% block onload %}doPoll(){% endblock %} + +{% block content %} +

{{ Block.name }}

+

Очередь тестирования

+ + + + + + + + + + + +
idТаскДата и время отправкиСтудентСтатус
+{% endblock %} \ No newline at end of file diff --git a/templates/queue_table.html b/templates/queue_table.html new file mode 100644 index 0000000..812dc05 --- /dev/null +++ b/templates/queue_table.html @@ -0,0 +1,19 @@ +{% for solution in solutions %} + + + {{ solution.id }} + + + {{ solution.task.name }} + + + {{ solution.time_sent }} + + + {{ solution.userinfo.surname }} {{ solution.userinfo.name }} {{ solution.userinfo.middle_name }} + + + {{ solution.result }} + + +{% endfor %} \ No newline at end of file diff --git a/templates/rating.html b/templates/rating.html new file mode 100644 index 0000000..7ea0e84 --- /dev/null +++ b/templates/rating.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}Рейтинг{% endblock %} + +{% block content %} +

Рейтинг (beta)

+

{{ Block.name }}

+ + + + + {% for task in Block.tasks %} + + {% endfor %} + + + + + {% for sub in Block.course.students %} + + + {% for task in Block.tasks %} + {% with status=sub.user|mark_status:task %} + + {% endwith %} + {% endfor %} + + + {% endfor %} + +
Студент{% if admin_course %}{{ task.name }}{% else %}{{ task.name }}{% endif %}Оценка
{% if admin_course %}{{ sub.user|userinfo_by_user }}{% else %}{{ sub.user|userinfo_by_user }}{% endif %} + {% if current_page == 'admin' and status != '-' %} + {{ status }} + {% else %} + {{ status }} + {% endif %} + + {% with mark=Block|mark_for_block:sub.user %} + {% if mark|marked %} +
+ {{ mark }} +
+ {% endif %} + {% endwith %} +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/reset_password.html b/templates/reset_password.html new file mode 100644 index 0000000..fa3a6be --- /dev/null +++ b/templates/reset_password.html @@ -0,0 +1,29 @@ + + {% load static %} + + + + + Sprint + + + + + + +
+

+ Sprint +

+ {{ error }} +
+ {% csrf_token %} + + {{ form }} +
+ +
+
+ + \ No newline at end of file diff --git a/templates/restore.html b/templates/restore.html new file mode 100644 index 0000000..5c56e6c --- /dev/null +++ b/templates/restore.html @@ -0,0 +1,36 @@ + + {% load static %} + + + + + Восстановление пароля + + + + + + +
+

+ Sprint +

+
+ {% csrf_token %} + + + + + +
+ Почта + + +
+ +
+ Если данный электронный адрес есть в базе, то на него будет отправлена ссылка для восстановления пароля. +
+ + \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..4026cf5 --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Настройки{% endblock %} + +{% block content %} +

+ Сменить пароль +

+ {{ error }} +
+ {% csrf_token %} + + {{ form }} +
+ +
+
+{% endblock %} diff --git a/templates/solution.html b/templates/solution.html new file mode 100644 index 0000000..d273c71 --- /dev/null +++ b/templates/solution.html @@ -0,0 +1,271 @@ +{% extends 'base.html' %} + +{% block title %}{{ solution.task.name }}|решение{% endblock %} + +{% block links %} + + + +{% endblock %} + +{% load filters %} + +{% block scripts %} +{% if can_edit %} + function findGetParameter(parameterName) { + var result = null, + tmp = []; + location.search + .substr(1) + .split("&") + .forEach(function (item) { + tmp = item.split("="); + if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); + }); + return result; + } + function next() { + const solutions_request = findGetParameter('solutions'); + const solutions = solutions_request.split(' '); + const current = findGetParameter('id'); + const current_index = solutions.findIndex((element) => element == current); + if (current_index != solutions.length - 1) { + var next_element = document.getElementById('next'); + next_element.setAttribute('href', '/admin/solution?id=' + solutions[current_index + 1] + '&solutions=' + solutions_request); + next_element.innerHTML = '->'; + } + } + function previous() { + const solutions_request = findGetParameter('solutions'); + const solutions = solutions_request.split(' '); + const current = findGetParameter('id'); + const current_index = solutions.findIndex((element) => element == current); + if (current_index != 0) { + var next_element = document.getElementById('previous'); + next_element.setAttribute('href', '/admin/solution?id=' + solutions[current_index - 1] + '&solutions=' + solutions_request); + next_element.innerHTML = '<-'; + } + } + function fillContent() { + next(); + previous(); + } + {% if can_edit %} + function showHideTests() { + var text = document.getElementById('tests_text'); + var button = document.getElementById('tests_button'); + text.hidden = !text.hidden; + if (text.hidden) { + button.textContent = 'Показать тесты'; + } else { + button.textContent = 'Скрыть тесты'; + } + } + function showHideLog() { + var text = document.getElementById('log_text'); + var button = document.getElementById('log_button'); + text.hidden = !text.hidden; + if (text.hidden) { + button.textContent = 'Показать лог'; + } else { + button.textContent = 'Скрыть лог'; + } + } + function retest() { + let del = confirm("Подтвердите перетест"); + if (del) { + const sols = findGetParameter('solutions'); + const link = '/admin/retest?block_id={{ solution.task.block.id }}&solution_id={{ solution.id }}&next={% autoescape off %}{{ path }}?id={{ solution.id }}{% if current_page == 'admin' %}%26solutions={% endif %}{% endautoescape %}'{% if current_page == 'admin' %} + sols.replaceAll(' ', '%20'){% endif %}; + window.location.href = link; + } + } + {% endif %} +{% endif %} +{% endblock %} + +{% block onload %}{% if can_edit %}fillContent(){% endif %}{% endblock %} + +{% block content %} +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Блок + + {{ solution.task.block.name }} + {% if current_page == 'admin' %} + | К посылкам | К рейтингу + {% endif %} +
+ Таск + + {{ solution.task.name }} +
+ Студент + + {{ solution.userinfo.surname }} {{ solution.userinfo.name }} {{ solution.userinfo.middle_name }} +
+ id решения + +
+ {% if can_edit %} + + {% endif %} + {{ solution.id }} + {% if can_edit %} + + {% endif %} + +
+ Результат + + {% if can_edit or solution.task.show_details %} + + + + + + {% else %} + {% if solution.task.show_result %} + {{ solution.result }} + {% else %} + Accepted + {% endif %} + {% endif %} +
+ Оценка + + {% if can_edit %} + {% csrf_token %} + + {% else %} + {% if solution.task.show_result %} + {{ solution.mark_property }} + {% else %} + {% if solution.mark == null %} + Checking + {% else %} + Checked + {% endif %} + {% endif %} + {% endif %} +
+ Комментарий + + {% if can_edit %} + + {% else %} +
+{{ solution.comment_property }}
+
+ {% endif %} +
+ {% if can_edit %} + + + + + {% endif %} + +
+
+
+
+

Files

+ {% for filename, text in solution.files.items %} +
{{ filename }}
+ {% if filename|is_code %} +
+
+{{ text }}
+
+
+ {% else %} +
+
+{{ text }}
+
+
+ {% endif %} +
+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/solutions.html b/templates/solutions.html new file mode 100644 index 0000000..863f602 --- /dev/null +++ b/templates/solutions.html @@ -0,0 +1,315 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}{{ Block.name }} | решения{% endblock %} + +{% block scripts %} + function filter() { + const idi = document.getElementById('idi').value; + const task = document.getElementById('task_name').value; + const user = document.getElementById('user').value; + const group = document.getElementById('group').value; + const last_solution = document.getElementById('last_solution').checked; + const best_result = document.getElementById('best_result').checked; + const only_students = document.getElementById('only_students').checked; + const not_seen = document.getElementById('not_seen').checked; + var req = ''; + if (idi) { + req += '&solution_id=' + idi; + } + if (not_seen) { + req += '¬_seen=' + not_seen; + } + if (task) { + req += '&task_name=' + task; + } + if (user) { + req += '&user=' + user; + } + if (group) { + req += '&group=' + group; + } + if (last_solution) { + req += '&last_solution=' + last_solution; + } + if (best_result) { + req += '&best_result=' + best_result; + } + if (only_students) { + req += '&only_students=' + only_students; + } + window.location.href = '/admin/solutions?block_id={{ Block.id }}' + req; + } + function retest() { + let del = confirm("Подтвердите перетест"); + if (del) + window.location.href = '/admin/retest?block_id={{ Block.id }}{% autoescape off %}{{ req }}{% endautoescape %}'; + } + function download() { + let del = confirm("Подтвердите скачивание"); + if (del) { + window.location.href = '/admin/download?block_id={{ Block.id }}{% autoescape off %}{{ req }}{% endautoescape %}'; + } + } + function showHideTests() { + var text = document.getElementById('tests_text'); + var button = document.getElementById('tests_button'); + text.hidden = !text.hidden; + if (text.hidden) { + button.textContent = 'Показать тесты'; + } else { + button.textContent = 'Скрыть тесты'; + } + } + function showHideLog() { + var text = document.getElementById('log_text'); + var button = document.getElementById('log_button'); + text.hidden = !text.hidden; + if (text.hidden) { + button.textContent = 'Показать лог'; + } else { + button.textContent = 'Скрыть лог'; + } + } + function fillModalResults(id) { + jQuery.get('/get_result_data?id=' + id, function(data) { + const response = JSON.parse(data); + if (response['success'] == true) { + document.getElementById('resultModalLongTitle').innerHTML = 'Подробная информация о тестировании ' + id; + document.getElementById('results_text').innerHTML = response['results_text']; + document.getElementById('tests_text').innerHTML = '

Тесты


' + response['tests_text']; + document.getElementById('log_text').innerHTML = '

Лог


' + response['log_text']; + } + }); + } + function fillModalComments(id) { + jQuery.get('/get_comment_data?id=' + id, function(data) { + const response = JSON.parse(data); + if (response['success'] == true) { + document.getElementById('commentModalLongTitle').innerHTML = 'Комментарий к решению ' + id; + document.getElementById('comment_text').innerHTML = response['comment_text']; + } + }); + } +{% endblock %} + +{% block styles %} + .input_field { + width: 500px; + } +{% endblock %} + +{% block content %} + + + + +

{{ Block.name }}

+
+
+

Фильтр

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Id
Таск + +
Пользователь + +
Группа + +
Последнее решение{{ option.last_solution }}
Лучший результат
Только студенты
Еще не проверено
+ +
+
+
+ +
+
+

Статистика по таскам

+ + + + + + + + + + {% for task in Block.tasks %} + + + + + + + + {% endfor %} + + +
ТаскВерноЧастичноС ошибкойВсе
{{ task.name }}{{ task.correct_count }}{{ task.partially_passed }}{{ task.solutions_with_error }}{{ task.solutions_count }}
+
+ +
+
+
Решения
+
+ {% csrf_token %} +
+ + + + + + + + + + + + + + + + {% for solution in solutions %} + + + + + + + + + + + + {% endfor %} + +
idТаскПользовательВремя отправкиГруппаРезультатОценкаКомментарийДействия
+ {{ solution.id }} + + {{ solution.task.name }} + + {{ solution.userinfo.surname }} {{ solution.userinfo.name }} {{ solution.userinfo.middle_name }} + + {{ solution.time_sent }} + + {{ solution.userinfo.group }} + + + + + {{ solution.mark_property }} + + {% if solution.comment %} + + + {% else %} +

Нет комментария

+ {% endif %} +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/templates/solutions_table.html b/templates/solutions_table.html new file mode 100644 index 0000000..57b453c --- /dev/null +++ b/templates/solutions_table.html @@ -0,0 +1,45 @@ +{% for solution in solutions %} + + + {{ solution.id }} + + + {{ solution.time_sent }} + + + {% if can_edit or task.show_details %} + + + + {% if can_edit %} + + + {% endif %} + {% else %}{% if task.show_result %} + {{ solution.result }} + {% else %} + Accepted + {% endif %} + {% endif %} + + + + {% if task.show_result or can_edit %} + {{ solution.mark_property }} + {% else %} + {% if solution.mark == null %} + Checking + {% else %} + Checked + {% endif %} + {% endif %} + + +
+{{ solution.comment_property }}
+
+ + +{% endfor %} \ No newline at end of file diff --git a/templates/superuser.html b/templates/superuser.html new file mode 100644 index 0000000..d14e97e --- /dev/null +++ b/templates/superuser.html @@ -0,0 +1,85 @@ +{% extends 'base.html' %} + +{% block title %}Режим Бога{% endblock %} +{% block content %} +

Настройки системы

+
+ {% csrf_token %} +
+
+ + + + + + + + + {% for param in params %} + + + + + {% endfor %} + +
ПараметрЗначение
{{ param.key }}
+
+
+ +
+
+

Курсы

+ + + + + + + + + {% csrf_token %} + + + + + + + + {% for course in courses %} + + + + + + + {% endfor %} +
+ id + + Название + + Преподаватели +
+ + + + + +
+ {{ course.id }} + + {{ course.name }} + + {% for teacher in course.teachers %} + {{ teacher.surname }} {{ teacher.name }} {{ teacher.middle_name }} + {% endfor %} + +
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/task.html b/templates/task.html new file mode 100644 index 0000000..6c3e793 --- /dev/null +++ b/templates/task.html @@ -0,0 +1,290 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}{{ task.name }}{% endblock %} + +{% block styles %} + input[type="file"] { + display: none; + } +{% endblock %} + +{% block onload %}doPoll(){% endblock %} + +{% block links %} + +{% endblock %} + +{% block scripts %} + function partyhard() { + var elem = document.getElementById('paint'); + elem.hidden = false; + } + function uploaded() { + document.getElementById('is_uploaded').style.display = 'block'; + document.getElementById('is_uploaded').nodeValue = document.getElementById('file-upload').nodeValue; + } + function doPoll() { + jQuery.get('/solutions_table?id={{ task.id }}', function(data) { + if (data == 'done') { + return + } + else { + document.getElementById('solutions').innerHTML = data; + if (current_solution != null) { + {% if can_edit %} + document.getElementById('log').innerHTML = document.getElementById('log_' + current_solution).innerHTML; + {% endif %} + document.getElementById('details').innerHTML = document.getElementById('details_' + current_solution).innerHTML; + } + setTimeout(function() {doPoll()}, 2000); + } + }) + jQuery.get('/solutions_table?id={{ task.id }}&render=true', function(data) { + document.getElementById('solutions').innerHTML = data; + if (current_solution != null) { + {% if can_edit %} + document.getElementById('log').innerHTML = document.getElementById('log_' + current_solution).innerHTML; + {% endif %} + document.getElementById('details').innerHTML = document.getElementById('details_' + current_solution).innerHTML; + } + }) + } + {% if can_edit %} + function showHideTests(id) { + var text = document.getElementById('tests_text_' + id); + var button = document.getElementById('tests_button_' + id); + text.hidden = !text.hidden; + if (text.hidden) { + button.textContent = 'Показать тесты'; + } else { + button.textContent = 'Скрыть тесты'; + } + } + function showHideLog(id) { + var text = document.getElementById('log_text_' + id); + var button = document.getElementById('log_button_' + id); + text.hidden = !text.hidden; + if (text.hidden) { + button.textContent = 'Показать лог'; + } else { + button.textContent = 'Скрыть лог'; + } + } + {% endif %} + {% if task.show_details or can_edit %} + function showData(id) { + current_solution = id; + const dataTypes = ['details'{% if can_edit %}, 'tests', 'log'{% endif %}]; + for (const dt of dataTypes) { + document.getElementById(dt).innerHTML = document.getElementById(dt + '_' + id).innerHTML; + } + document.getElementById('resultModalLongTitle').innerHTML = 'Подробная информация о тестировании ' + id; + } + {% endif %} +{% endblock %} + +{% block content %} + +
+ Обратно к блоку +
+

+ {{ task.name }} + {% if can_edit %} + + {% endif %} + +

+ +
+
+
+

Легенда

+ {% autoescape off %} + {{ task.legend }} + {% endautoescape %} +
+

Формат входных данных

+ {% autoescape off %} + + {{ task.input }} + {% endautoescape %} + +
+

Формат выходных данных

+ {% autoescape off %} + + {{ task.output }} + {% endautoescape %} + +
+

Спецификации

+ {% autoescape off %} + + {{ task.specifications }} + {% endautoescape %} +
+
+
+
Таски
+ + {% for t in task.block.tasks %} + + {% with mark=t|mark_for_task:user %} + {% if mark|marked %} + + {% endif %} + {% endwith %} + + + {% endfor %} +
+
+ {{ mark }} +
+
+ {% if t.id == task.id %}{{ t.name }}{% else %}{{ t.name }}{% endif %}
+
+
+
+
+

Самплы

+ {% for sample in task.samples %} +
Пример {{ sample.input.num }}

+ + + + + + +
+ Входные данные + + Выходные данные +
+
+
+ + + + + +
+
+{{ sample.input.text }}
+
+
+
+{{ sample.output.text }}
+
+
+
+ {% endfor %} +
+ {% if can_send or can_edit %} +

Отправить решение

+
+ {% csrf_token %} + + + +
+
+ {% endif %} + {% if not can_edit and can_send %} + Осталось попыток: {{ user|last_attempts:task }} + {% endif %} + +{% if can_edit or task.show_details %} + + +{% endif %} +

Решения

+ + + + + + + + + + + + + +
idДата и время отправкиРезультатОценкаКомментарий
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/task_settings.html b/templates/task_settings.html new file mode 100644 index 0000000..2b38586 --- /dev/null +++ b/templates/task_settings.html @@ -0,0 +1,327 @@ +{% extends 'base.html' %} + +{% load filters %} + +{% block title %}{{ task.name }}|настройки{% endblock %} + +{% block scripts %} + function uploaded() { + document.getElementById('is_uploaded').style.display = 'block'; + document.getElementById('is_uploaded').nodeValue = document.getElementById('file-upload').nodeValue; + } + function uploaded1() { + document.getElementById('send').style.display = 'block'; + document.getElementById('is_uploaded1').nodeValue = document.getElementById('file-upload1').nodeValue; + } + function valueChanged() { + const other = document.getElementById('show_details'); + other.disabled = !document.getElementById('show_result').checked; + console.log('changed'); + if (other.disabled) { + other.checked = false; + } + } + function set_checkboxes() { + if (!document.getElementById('show_result').checked) { + document.getElementById('show_details').disabled = true; + } + } +{% endblock %} + +{% block onload %}set_checkboxes(){% endblock %} + +{% block styles %} + input[type="file"] { + display: none; + } + input[name="new_file"] { + display: none; + } + .my_td { + vertical-align: middle; + height: 20px; + } + .params { + margin-top: 20px; + } + .in { + width: 500%; + } +{% endblock %} +{% block content %} +
{{ task.block.name }}
+

{{ task.name }}

+
+ {% csrf_token %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Легенда + + +
+ Формат входных данных + + +
+ Формат выходных данных + + +
+ Спецификации + + +
+ Ограничения по времени + + +
+ Вес задачи + + +
+ Максимальная оценка + + +
+ Максимум решений + + +
+ Формула оценки + + +
+ Сдается полное решение + + +
+ Показывать результат тестирования + + +
+ Показывать детали тестирования + + +
+ Приоритет + + +
+ Тесты + + + + + + +
+ Дополнительные файлы и самплы + + + {% for file in task.files %} + + + + + + + {% endfor %} +
+ {% if file.can_be_sample %} + + {% endif %} + + + + + + {% if file.readable %} + + + + + {% else %} + + {% endif %} + +
+ + + + + + + +
+ {% csrf_token %} + +
+
+
+ + + {% if is_superuser %} +
+ {% csrf_token %} + + +
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/users_settings.html b/templates/users_settings.html new file mode 100644 index 0000000..095f892 --- /dev/null +++ b/templates/users_settings.html @@ -0,0 +1,165 @@ +{% extends 'base.html' %} + +{% block title %}{{ course.name }}|настройки{% endblock %} + +{% block scripts %} + function uploaded() { + document.getElementById('is_uploaded').style.display = 'block'; + document.getElementById('is_uploaded').nodeValue = document.getElementById('file-upload').nodeValue; + } +{% endblock %} + +{% block styles %} + input[type="file"] { + display: none; + } +{% endblock %} + +{% block content %} +

{{ course.name }}

+
Добавить участников
+
+ {% csrf_token %} + + + + + +
+ + + +
+
+
+ {% csrf_token %} + + + +
+
+ + + + + +
+

Участники

+ + + + + + + + + + + + + + {% for user in course.subscribes %} + + + + + + + + + + {% endfor %} + +
ФамилияИмяОтчествоГруппаПочтаРоль
+ {{ user.userinfo.surname }} + + {{ user.userinfo.name }} + + {{ user.userinfo.middle_name }} + + {{ user.userinfo.group }} + + {{ user.userinfo.user.email }} + + {% with role=user.role %} + {% if role == 'Студент' or role == 'Ассистент' %} +
+ {% csrf_token %} + +
+ {% else %} + {{ user.role }} + {% endif %} + {% endwith %} +
+
+ {% csrf_token %} + + +
+
+{% endblock %} \ No newline at end of file