diff --git a/Checker/__init__.py b/Checker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Checker/admin.py b/Checker/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Checker/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Checker/apps.py b/Checker/apps.py new file mode 100644 index 0000000..1ad2f2e --- /dev/null +++ b/Checker/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CheckerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'Checker' diff --git a/Checker/migrations/0001_initial.py b/Checker/migrations/0001_initial.py new file mode 100644 index 0000000..64f1621 --- /dev/null +++ b/Checker/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.4 on 2022-02-15 18:22 + +import Checker.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('Main', '0020_languageapply'), + ] + + operations = [ + migrations.CreateModel( + name='Checker', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.CharField(db_index=True, default=Checker.models.generate_token, max_length=30)), + ('last_request', models.DateTimeField()), + ('set', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.set')), + ('testing_solution', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='Main.solution')), + ], + ), + ] diff --git a/Checker/migrations/0002_alter_checker_set.py b/Checker/migrations/0002_alter_checker_set.py new file mode 100644 index 0000000..549016c --- /dev/null +++ b/Checker/migrations/0002_alter_checker_set.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.4 on 2022-02-15 18:23 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0020_languageapply'), + ('Checker', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='checker', + name='set', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkers', to='Main.set'), + ), + ] diff --git a/Checker/migrations/0003_checker_name.py b/Checker/migrations/0003_checker_name.py new file mode 100644 index 0000000..96fe6e5 --- /dev/null +++ b/Checker/migrations/0003_checker_name.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.4 on 2022-02-15 18:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Checker', '0002_alter_checker_set'), + ] + + operations = [ + migrations.AddField( + model_name='checker', + name='name', + field=models.CharField(default='', max_length=50), + ), + ] diff --git a/Checker/migrations/0004_alter_checker_token.py b/Checker/migrations/0004_alter_checker_token.py new file mode 100644 index 0000000..0c3d57d --- /dev/null +++ b/Checker/migrations/0004_alter_checker_token.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.4 on 2022-02-15 18:48 + +import Checker.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Checker', '0003_checker_name'), + ] + + operations = [ + migrations.AlterField( + model_name='checker', + name='token', + field=models.CharField(db_index=True, default=Checker.models.generate_token, max_length=30, unique=True), + ), + ] diff --git a/Checker/migrations/0005_checker_dynamic_token.py b/Checker/migrations/0005_checker_dynamic_token.py new file mode 100644 index 0000000..fc38c85 --- /dev/null +++ b/Checker/migrations/0005_checker_dynamic_token.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.4 on 2022-02-16 07:33 + +import Checker.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('Checker', '0004_alter_checker_token'), + ] + + operations = [ + migrations.AddField( + model_name='checker', + name='dynamic_token', + field=models.CharField(db_index=True, default=Checker.models.generate_token, max_length=30, unique=True), + ), + ] diff --git a/Checker/migrations/__init__.py b/Checker/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Checker/models.py b/Checker/models.py new file mode 100644 index 0000000..be31577 --- /dev/null +++ b/Checker/models.py @@ -0,0 +1,28 @@ +from random import choice + +from django.db import models +from django.utils import timezone + +from Main.models import Solution, Set + + +def generate_token(): + letters = '1234567890qwertyuiopasdfghjklzxcvbnm!@#$%^&*()QWERTYUIOPASDFGHJKLZXCVBNM' + return ''.join([choice(letters) for _ in range(30)]) + + +class Checker(models.Model): + token = models.CharField(max_length=30, default=generate_token, db_index=True, unique=True) + dynamic_token = models.CharField(max_length=30, default=generate_token, db_index=True, unique=True) + testing_solution = models.ForeignKey(Solution, on_delete=models.SET_NULL, null=True) + set = models.ForeignKey(Set, on_delete=models.CASCADE, related_name="checkers") + last_request = models.DateTimeField() + name = models.CharField(max_length=50, default='') + + @property + def status(self): + if self.testing_solution is not None: + return 'Testing' + if (timezone.now() - self.last_request).total_seconds() > 3: + return 'Disabled' + return 'Active' diff --git a/Checker/tests.py b/Checker/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Checker/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Checker/urls.py b/Checker/urls.py new file mode 100644 index 0000000..2a20f34 --- /dev/null +++ b/Checker/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from Checker import views + +urlpatterns = [ + path("status", views.status), + path("available", views.available), + path("get_dynamic", views.get_dynamic), + path("set_result", views.set_result), +] diff --git a/Checker/views.py b/Checker/views.py new file mode 100644 index 0000000..786d167 --- /dev/null +++ b/Checker/views.py @@ -0,0 +1,73 @@ +from os.path import join +from tempfile import TemporaryDirectory +from zipfile import ZipFile + +from django.core.exceptions import ObjectDoesNotExist +from django.http import JsonResponse, HttpResponse + +from django.utils import timezone + +from Checker.models import Checker, generate_token +from FileStorage.sync import synchronized_method +from Main.models import Solution, SolutionFile, ExtraFile + + +def get_dynamic(request): + try: + checker = Checker.objects.get(token=request.GET['token']) + if checker.status == 'Active': + return JsonResponse({"status": "Another checker is working"}, status=403) + checker.dynamic_token = generate_token() + checker.save() + return JsonResponse({"token": checker.dynamic_token}) + except ObjectDoesNotExist: + return JsonResponse({"status": "incorrect token"}, status=403) + + +def status(request): + try: + checker = Checker.objects.get(dynamic_token=request.GET['token']) + now = timezone.now() + checker.last_request = now + checker.save() + return JsonResponse({"status": "ok"}) + except ObjectDoesNotExist: + return JsonResponse({"status": "incorrect token"}, status=403) + + +@synchronized_method +def available(request): + try: + checker = Checker.objects.get(dynamic_token=request.GET['token']) + solution = Solution.objects.filter(set=checker.set, result="In queue").order_by('time_sent').first() + if solution is None: + return JsonResponse({"id": None}) + solution.result = "Testing" + solution.save() + with TemporaryDirectory() as tempdir: + with ZipFile(join(tempdir, "package.zip"), 'w') as zip_file: + for sf in SolutionFile.objects.filter(solution=solution): + zip_file.writestr(sf.path, sf.bytes) + for ef in ExtraFile.objects.filter(task=solution.task): + zip_file.writestr(ef.filename, ef.bytes) + response = HttpResponse(open(join(tempdir, 'package.zip'), 'rb').read(), content_type='application/octet-stream', status=201) + response.headers['language_id'] = solution.language_id + response.headers['solution_id'] = solution.id + response.headers['timeout'] = solution.task.time_limit + return response + except ObjectDoesNotExist: + return JsonResponse({"status": "incorrect token"}, status=403) + + +def set_result(request): + try: + checker = Checker.objects.get(dynamic_token=request.GET['token']) + solution = Solution.objects.get(id=request.GET['solution_id']) + result = request.GET['result'] + if checker.set != solution.set: + return JsonResponse({"status": "incorrect solution"}, status=403) + solution.result = result + solution.save() + return JsonResponse({"status": True}) + except ObjectDoesNotExist: + return JsonResponse({"status": "incorrect token"}, status=403) diff --git a/CheckerExecutor/Dockerfile b/CheckerExecutor/Dockerfile new file mode 100644 index 0000000..14d8a77 --- /dev/null +++ b/CheckerExecutor/Dockerfile @@ -0,0 +1,15 @@ +FROM docker:dind + +RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python +RUN python3 -m ensurepip +RUN apk update && apk add postgresql-dev gcc python3-dev musl-dev jpeg-dev zlib-dev libjpeg +RUN pip3 install --no-cache --upgrade pip setuptools +RUN addgroup -S docker + +ENV PYTHONUNBUFFERED 1 +RUN mkdir -p /usr/src/app/ +WORKDIR /usr/src/app/ + +COPY . /usr/src/app/ + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/CheckerExecutor/__init__.py b/CheckerExecutor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CheckerExecutor/language.py b/CheckerExecutor/language.py new file mode 100644 index 0000000..28d3145 --- /dev/null +++ b/CheckerExecutor/language.py @@ -0,0 +1,64 @@ +from dataclasses import dataclass + + +@dataclass +class Language: + id: int + name: str + work_name: str + file_type: str + logo_url: str + image: str + highlight: str + + def __str__(self): + return self.name + + +languages = [ + Language( + id=0, + name="Python3", + work_name="Python3", + file_type="py", + logo_url="https://entredatos.es/wp-content/uploads/2021/05/1200px-Python-logo-notext.svg.png", + image="python:3.6", + highlight="python", + ), + Language( + id=1, + name="Kotlin", + work_name="Kotlin", + file_type="kt", + logo_url="https://upload.wikimedia.org/wikipedia/commons/0/06/Kotlin_Icon.svg", + image="zenika/kotlin", + highlight="kotlin", + ), + Language( + id=2, + name="C++", + work_name="Cpp", + file_type="cpp", + logo_url="https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/ISO_C%2B%2B_Logo.svg/1822px-ISO_C%2B%2B_Logo.svg.png", + image="gcc", + highlight="cpp", + ), + Language( + id=3, + name="Java", + work_name="Java", + file_type="java", + logo_url="https://upload.wikimedia.org/wikipedia/ru/thumb/3/39/Java_logo.svg/1200px-Java_logo.svg.png", + image="openjdk", + highlight="java", + ), + Language( + id=4, + name="C#", + work_name="CSharp", + file_type="cs", + logo_url="https://cdn.worldvectorlogo.com/logos/c--4.svg", + image="mono", + highlight="csharp", + ), +] diff --git a/CheckerExecutor/main.py b/CheckerExecutor/main.py new file mode 100644 index 0000000..df5279e --- /dev/null +++ b/CheckerExecutor/main.py @@ -0,0 +1,67 @@ +from multiprocessing import Process +from os import getenv +from os.path import join +from tempfile import TemporaryDirectory +from time import sleep +from zipfile import ZipFile + +from requests import get + +from language import languages +from testers import * + + +def process_solution(path, data, language_id, solution_id, timeout): + with open(join(path, "package.zip"), 'wb') as fs: + fs.write(data) + with ZipFile(join(path, "package.zip"), 'r') as zip_ref: + zip_ref.extractall(path) + language = languages[language_id] + try: + result = eval(language.work_name + "Tester")(path, solution_id, language_id, timeout).execute() + except Exception as e: + print(str(e)) + result = "TE" + return result + + +def poll(token): + while True: + print(get("http://127.0.0.1:8000/checker/status", params={"token": token}).json()) + sleep(2) + + +def main(): + request = get("http://127.0.0.1:8000/checker/get_dynamic", params={"token": getenv("TOKEN")}) + if request.status_code != 200: + print("Error happened: " + request.json()['status']) + exit(1) + dynamic_token = request.json()['token'] + p = Process(target=poll, args=(dynamic_token,)) + p.start() + while True: + data = get("http://127.0.0.1:8000/checker/available", params={"token": dynamic_token}) + if data.status_code == 200: + sleep(2) + continue + elif data.status_code == 201: + with TemporaryDirectory() as tempdir: + result = process_solution( + tempdir, + data.content, + int(data.headers['language_id']), + int(data.headers['solution_id']), + int(data.headers['timeout']), + ) + get("http://127.0.0.1:8000/checker/set_result", params={ + "token": dynamic_token, + "solution_id": data.headers['solution_id'], + "result": result + }) + + else: + print("unknown status") + + +if __name__ == '__main__': + main() diff --git a/CheckerExecutor/testers/BaseTester.py b/CheckerExecutor/testers/BaseTester.py new file mode 100644 index 0000000..cca3157 --- /dev/null +++ b/CheckerExecutor/testers/BaseTester.py @@ -0,0 +1,115 @@ +from os import listdir +from os.path import join, exists +from subprocess import call, TimeoutExpired + +from language import * + + +class TestException(Exception): + pass + + +class BaseTester: + working_directory = "app" + checker_code = None + + def exec_command(self, command, working_directory="app", timeout=None): + return call( + f'docker exec -i solution sh -c "cd {working_directory} && {command}"', + shell=True, + timeout=timeout, + ) + + def before_test(self): + files = [ + file + for file in listdir(self.path) + if file.endswith("." + self.language.file_type) + ] + code = self.exec_command( + f'{self.build_command} {" ".join(files)}', + working_directory=self.working_directory, + ) + if code != 0: + raise TestException("CE") + + def test(self, filename): + code = self.exec_command( + f"< {filename} {self.command} > output.txt", + timeout=self.timeout / 1000, + ) + if code != 0: + raise TestException("RE") + result = open(join(self.path, "output.txt"), "r").read().strip().replace('\r\n', '\n') + print("got result", result) + if self.checker_code is not None: + print('using checker') + with open(join(self.path, 'expected.txt'), 'w') as fs: + fs.write(self.predicted) + with open(join(self.path, 'checker.py'), 'w') as fs: + fs.write(self.checker_code) + code = call(f'docker exec -i checker sh -c "cd app && python checker.py"', shell=True, timeout=1) + if code != 0: + raise TestException("WA") + else: + print('using simple check') + if result != self.predicted: + print('incorrect') + raise TestException("WA") + print('correct') + + def after_test(self): + pass + + @property + def command(self): + return "./executable.exe" + + @property + def build_command(self): + return "" + + @property + def path(self): + return self._path + + @property + def language(self): + return languages[self.language_id] + + def __init__(self, path, solution_id, language_id, timeout): + self.solution_id = solution_id + self._path = path + self.language_id = language_id + self.timeout = timeout + + def execute(self): + docker_command = f"docker run --name solution --volume={self.path}:/{self.working_directory} -t -d {self.language.image}" + print(docker_command) + call(docker_command, shell=True) + checker = join(self.path, 'checker.py') + if exists(checker): + self.checker_code = open(checker, 'r').read() + call(f"docker run --name checker --volume={self.path}:/app -t -d python:3.6", shell=True) + print("Container created") + result = None + try: + self.before_test() + print("before test finished") + for file in listdir(self.path): + if not file.endswith(".a") and exists(join(self.path, file + '.a')): + self.predicted = open(join(self.path, file + '.a'), 'r').read().strip().replace('\r\n', '\n') + print('predicted:', self.predicted) + self.test(file) + self.after_test() + result = "OK" + except TestException as e: + result = str(e) + except TimeoutExpired: + result = "TL" + except Exception as e: + print(str(e)) + result = "TE" + call(f"docker rm --force solution", shell=True) + call(f"docker rm --force checker", shell=True) + return result diff --git a/CheckerExecutor/testers/CSharpTester.py b/CheckerExecutor/testers/CSharpTester.py new file mode 100644 index 0000000..cff4465 --- /dev/null +++ b/CheckerExecutor/testers/CSharpTester.py @@ -0,0 +1,11 @@ +from .BaseTester import BaseTester + + +class CSharpTester(BaseTester): + @property + def build_command(self): + return "csc /out:executable.exe" + + @property + def command(self): + return "mono executable.exe" diff --git a/CheckerExecutor/testers/CppTester.py b/CheckerExecutor/testers/CppTester.py new file mode 100644 index 0000000..d6b627c --- /dev/null +++ b/CheckerExecutor/testers/CppTester.py @@ -0,0 +1,7 @@ +from .BaseTester import BaseTester + + +class CppTester(BaseTester): + @property + def build_command(self): + return "g++ -o executable.exe" diff --git a/CheckerExecutor/testers/GoTester.py b/CheckerExecutor/testers/GoTester.py new file mode 100644 index 0000000..ae697ae --- /dev/null +++ b/CheckerExecutor/testers/GoTester.py @@ -0,0 +1,8 @@ +from .BaseTester import BaseTester + + +class GoTester(BaseTester): + working_directory = "../app" + + def build_command(self): + return "go build -o executable.exe" diff --git a/CheckerExecutor/testers/JavaTester.py b/CheckerExecutor/testers/JavaTester.py new file mode 100644 index 0000000..64b2fda --- /dev/null +++ b/CheckerExecutor/testers/JavaTester.py @@ -0,0 +1,27 @@ +from os import listdir + +from .BaseTester import BaseTester, TestException + + +class JavaTester(BaseTester): + _executable = None + + def before_test(self): + files = [ + file + for file in listdir(self.path) + if file.endswith(".java") + ] + code = self.exec_command(f"javac {' '.join(files)}") + if code != 0: + raise TestException("CE") + for file in listdir(self.path): + if file.endswith(".class"): + self._executable = file.rstrip(".class") + break + if self._executable is None: + raise TestException("TE") + + @property + def command(self): + return f"java -classpath . {self._executable}" diff --git a/CheckerExecutor/testers/KotlinTester.py b/CheckerExecutor/testers/KotlinTester.py new file mode 100644 index 0000000..4921379 --- /dev/null +++ b/CheckerExecutor/testers/KotlinTester.py @@ -0,0 +1,21 @@ +from os import listdir + +from .BaseTester import BaseTester, TestException + + +class KotlinTester(BaseTester): + def before_test(self): + files = [ + file + for file in listdir(self.path) + if file.endswith(".kt") + ] + code = self.exec_command( + f'kotlinc {" ".join(files)} -include-runtime -d solution.jar' + ) + if code != 0: + raise TestException("CE") + + @property + def command(self): + return "java -jar solution.jar" diff --git a/CheckerExecutor/testers/Python3Tester.py b/CheckerExecutor/testers/Python3Tester.py new file mode 100644 index 0000000..ec2b9f1 --- /dev/null +++ b/CheckerExecutor/testers/Python3Tester.py @@ -0,0 +1,19 @@ +from os import listdir + +from .BaseTester import BaseTester, TestException + + +class Python3Tester(BaseTester): + file = None + + def before_test(self): + for file in listdir(self.path): + if file.endswith(".py") and file != 'checker.py': + self.file = file + break + if self.file is None: + raise TestException("TE") + + @property + def command(self): + return f"python3 {self.file}" diff --git a/CheckerExecutor/testers/__init__.py b/CheckerExecutor/testers/__init__.py new file mode 100644 index 0000000..cd44294 --- /dev/null +++ b/CheckerExecutor/testers/__init__.py @@ -0,0 +1,7 @@ +from .BaseTester import BaseTester +from .Python3Tester import Python3Tester +from .CppTester import CppTester +from .GoTester import GoTester +from .JavaTester import JavaTester +from .CSharpTester import CSharpTester +from .KotlinTester import KotlinTester diff --git a/Main/migrations/0021_auto_20220215_2211.py b/Main/migrations/0021_auto_20220215_2211.py new file mode 100644 index 0000000..d73a9d3 --- /dev/null +++ b/Main/migrations/0021_auto_20220215_2211.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.4 on 2022-02-15 19:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('Main', '0020_languageapply'), + ] + + operations = [ + migrations.AddField( + model_name='solution', + name='set', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='Main.set'), + ), + migrations.AddIndex( + model_name='solution', + index=models.Index(fields=['set', '-time_sent'], name='Main_soluti_set_id_19e6c7_idx'), + ), + ] diff --git a/Main/models/mixins.py b/Main/models/mixins.py index 7058a2a..21c14a9 100644 --- a/Main/models/mixins.py +++ b/Main/models/mixins.py @@ -4,9 +4,14 @@ from SprintLib.utils import get_bytes, write_bytes, delete_file class FileStorageMixin: + + @cached_property + def bytes(self): + return get_bytes(self.fs_id) + @cached_property def text(self): - return get_bytes(self.fs_id).decode("utf-8") + return self.bytes.decode("utf-8") def write(self, bytes): self.fs_id = write_bytes(bytes) diff --git a/Main/models/solution.py b/Main/models/solution.py index f1d379b..2de34f1 100644 --- a/Main/models/solution.py +++ b/Main/models/solution.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import User from django.db import models from django.utils import timezone +from Main.models import Set from Main.models.solution_file import SolutionFile from Main.models.task import Task from Sprint.settings import CONSTS @@ -20,11 +21,13 @@ class Solution(models.Model): time_sent = models.DateTimeField(default=timezone.now) result = models.TextField(default=CONSTS["in_queue_status"]) test = models.IntegerField(default=None, null=True) + set = models.ForeignKey(Set, null=True, on_delete=models.SET_NULL) class Meta: indexes = [ models.Index(fields=['task', 'user', '-time_sent']), - models.Index(fields=['task', '-time_sent']) + models.Index(fields=['task', '-time_sent']), + models.Index(fields=['set', '-time_sent']), ] @property diff --git a/Main/templatetags/filters.py b/Main/templatetags/filters.py index 1e7e00b..d64d6de 100644 --- a/Main/templatetags/filters.py +++ b/Main/templatetags/filters.py @@ -1,6 +1,6 @@ from django import template -from Main.models import Solution +from Main.models import Solution, SetTask register = template.Library() @@ -13,3 +13,8 @@ def solved(user, task): if len(solutions) != 0: return False return None + + +@register.filter('settask') +def settask(set, task): + return SetTask.objects.get(set=set, task=task) diff --git a/Main/views/CheckersView.py b/Main/views/CheckersView.py new file mode 100644 index 0000000..59f8b7d --- /dev/null +++ b/Main/views/CheckersView.py @@ -0,0 +1,24 @@ +import datetime +from typing import Optional + +import pytz +from django.utils import timezone + +from Checker.models import Checker +from Main.models import SetTask, Set +from SprintLib.BaseView import BaseView, AccessError +from SprintLib.language import languages + + +class CheckersView(BaseView): + required_login = True + view_file = "checkers.html" + endpoint = "admin/checkers" + + def pre_handle(self): + self.current_set = self.entities.set + if ( + self.request.user != self.current_set.creator + and self.request.user.username not in self.current_set.editors + ): + raise AccessError() diff --git a/Main/views/SetSettingsView.py b/Main/views/SetSettingsView.py index d4a47cb..d7010e0 100644 --- a/Main/views/SetSettingsView.py +++ b/Main/views/SetSettingsView.py @@ -4,6 +4,7 @@ from typing import Optional import pytz from django.utils import timezone +from Checker.models import Checker from Main.models import SetTask, Set from SprintLib.BaseView import BaseView, AccessError from SprintLib.language import languages @@ -114,3 +115,11 @@ class SetSettingsView(BaseView): self.entities.set.languages.remove(t) self.entities.set.save() return "/admin/set?set_id=" + str(self.entities.set.id) + + def post_new_checker(self): + Checker.objects.create(name=self.request.POST['name'], set=self.entities.set, last_request=timezone.now() - datetime.timedelta(days=1)) + return '/admin/set?set_id=' + str(self.entities.set.id) + + def post_delete_checker(self): + Checker.objects.get(id=self.request.POST['checker_id']).delete() + return '/admin/set?set_id=' + str(self.entities.set.id) diff --git a/Main/views/TaskView.py b/Main/views/TaskView.py index af28763..3b4e588 100644 --- a/Main/views/TaskView.py +++ b/Main/views/TaskView.py @@ -37,6 +37,7 @@ class TaskView(BaseView): task=self.entities.task, user=self.request.user, language_id=int(self.request.POST["language"]), + set=self.entities.set if hasattr(self.entities, 'setTask') else None ) def post_0(self): @@ -47,8 +48,8 @@ class TaskView(BaseView): solution=self.solution, fs_id=fs_id, ) - send_testing(self.solution.id) - return "task?task_id=" + str(self.entities.task.id) + send_testing(self.solution) + return ("/task?setTask_id=" + str(self.entities.setTask.id)) if hasattr(self.entities, 'setTask') else ("/task?task_id=" + str(self.entities.task.id)) def post_1(self): # отправка решения через файл @@ -73,5 +74,5 @@ class TaskView(BaseView): solution=self.solution, fs_id=fs_id, ) - send_testing(self.solution.id) - return "task?task_id=" + str(self.entities.task.id) + send_testing(self.solution) + return ("/task?setTask_id=" + str(self.entities.setTask.id)) if hasattr(self.entities, 'setTask') else ("/task?task_id=" + str(self.entities.task.id)) diff --git a/Main/views/__init__.py b/Main/views/__init__.py index 0efb8cc..a82e291 100644 --- a/Main/views/__init__.py +++ b/Main/views/__init__.py @@ -20,3 +20,4 @@ from Main.views.ChatWithView import ChatWithView from Main.views.MessagesView import MessagesView from Main.views.SetView import SetView from Main.views.GroupView import GroupView +from Main.views.CheckersView import CheckersView diff --git a/Sprint/settings.py b/Sprint/settings.py index bb591e7..2e3f2c9 100644 --- a/Sprint/settings.py +++ b/Sprint/settings.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "Main.apps.MainConfig", + "Checker.apps.CheckerConfig", ] MIDDLEWARE = [ diff --git a/Sprint/urls.py b/Sprint/urls.py index 7bbf3fa..b26f9b7 100644 --- a/Sprint/urls.py +++ b/Sprint/urls.py @@ -1,10 +1,13 @@ from django.contrib import admin -from django.urls import path +from django.urls import path, include import Main.views from Main.views import * -urlpatterns = [] + +urlpatterns = [ + path("checker/", include("Checker.urls")) +] for v in dir(Main.views): try: diff --git a/SprintLib/queue.py b/SprintLib/queue.py index e30de9a..c7a8fbd 100644 --- a/SprintLib/queue.py +++ b/SprintLib/queue.py @@ -3,7 +3,9 @@ import pika from Sprint import settings -def send_testing(solution_id): +def send_testing(solution): + if solution.set is not None and len(solution.set.checkers.all()) != 0: + return with pika.BlockingConnection( pika.ConnectionParameters(host=settings.RABBIT_HOST, port=settings.RABBIT_PORT) ) as connection: @@ -12,5 +14,5 @@ def send_testing(solution_id): channel.basic_publish( exchange="", routing_key="test", - body=bytes(str(solution_id), encoding="utf-8"), + body=bytes(str(solution.id), encoding="utf-8"), ) diff --git a/templates/checkers.html b/templates/checkers.html new file mode 100644 index 0000000..4ba24d4 --- /dev/null +++ b/templates/checkers.html @@ -0,0 +1,34 @@ +{% for checker in set.checkers.all %} + {% with status=checker.status %} +
+ {% csrf_token %} + + + {{ status }}{% if status != 'Testing' %}{% endif %}
+
+ {% endwith %} + +{% endfor %} \ No newline at end of file diff --git a/templates/set_settings.html b/templates/set_settings.html index 1308289..124ea74 100644 --- a/templates/set_settings.html +++ b/templates/set_settings.html @@ -3,12 +3,25 @@ {% block title %}{{ set.name }}{% endblock %} {% block scripts %} + var saved_data = ""; function handle(value) { const elem = document.getElementById(value); elem.hidden = !elem.hidden; } + function doPoll() { + jQuery.get('/admin/checkers?set_id={{ set.id }}', function(data) { + var e = document.getElementById('checkers'); + if (saved_data.length != data.length) { + saved_data = data; + e.innerHTML = data; + } + setTimeout(function() {doPoll()}, 2000); + }) + } {% endblock %} +{% block onload %}doPoll(){% endblock %} + {% block main %}
{% csrf_token %} @@ -160,4 +173,37 @@
+

+

Чекеры

+
+ + {% endblock %} \ No newline at end of file diff --git a/templates/solution.html b/templates/solution.html index c35acfb..a2b06a5 100644 --- a/templates/solution.html +++ b/templates/solution.html @@ -1,5 +1,7 @@ {% extends 'base_main.html' %} +{% load filters %} + {% block main %}

@@ -16,7 +18,7 @@ Задача
- {{ solution.task.name }} + {{ solution.task.name }}