diff --git a/Main/models/__init__.py b/Main/models/__init__.py index cf117fa..a85fb68 100644 --- a/Main/models/__init__.py +++ b/Main/models/__init__.py @@ -8,3 +8,4 @@ from Main.models.settask import SetTask from Main.models.solution import Solution from Main.models.language import Language from Main.models.extrafile import ExtraFile +from Main.models.progress import Progress diff --git a/Main/models/extrafile.py b/Main/models/extrafile.py index 210aa14..bb7c511 100644 --- a/Main/models/extrafile.py +++ b/Main/models/extrafile.py @@ -1,27 +1,50 @@ from os import remove from os.path import join, exists +from django.core.exceptions import ObjectDoesNotExist from django.db import models from Sprint.settings import DATA_ROOT class ExtraFile(models.Model): - task = models.ForeignKey('Task', on_delete=models.CASCADE) + task = models.ForeignKey("Task", on_delete=models.CASCADE) filename = models.TextField() is_test = models.BooleanField(null=True) + is_sample = models.BooleanField(null=True) readable = models.BooleanField(null=True) test_number = models.IntegerField(null=True) @property def path(self): - return join(DATA_ROOT, 'extra_files', str(self.id)) + return join(DATA_ROOT, "extra_files", str(self.id)) + + @property + def can_be_sample(self): + return ( + self.is_test + and not self.filename.endswith(".a") + and len( + ExtraFile.objects.filter(task=self.task, filename=self.filename + ".a") + ) + ) @property def text(self): - return open(self.path, 'r').read() + return open(self.path, "r").read() def delete(self, using=None, keep_parents=False): if exists(self.path): remove(self.path) + if self.is_test and self.filename.endswith('.a'): + try: + ef = ExtraFile.objects.get(task=self.task, filename=self.filename.rstrip('.a'), is_test=True) + ef.is_sample = False + ef.save() + except ObjectDoesNotExist: + pass super().delete(using=using, keep_parents=keep_parents) + + @property + def answer(self): + return ExtraFile.objects.get(task=self.task, is_test=True, filename=self.filename + '.a') \ No newline at end of file diff --git a/Main/models/language.py b/Main/models/language.py index e0fbb30..475a3d3 100644 --- a/Main/models/language.py +++ b/Main/models/language.py @@ -7,6 +7,7 @@ class Language(models.Model): file_type = models.TextField(null=True) logo = models.ImageField(upload_to="logos", null=True) image = models.TextField(default='ubuntu') + highlight = models.TextField(default='plaintext') opened = models.BooleanField(default=False) def __str__(self): diff --git a/Main/models/progress.py b/Main/models/progress.py new file mode 100644 index 0000000..ca8db42 --- /dev/null +++ b/Main/models/progress.py @@ -0,0 +1,35 @@ +from datetime import timedelta + +from django.contrib.auth.models import User +from django.db import models +from django.utils import timezone + +from Main.models import Task + + +class Progress(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + task = models.ForeignKey(Task, on_delete=models.CASCADE) + start_time = models.DateTimeField(default=timezone.now) + finished_time = models.DateTimeField(null=True) + score = models.IntegerField(default=0) + finished = models.BooleanField(default=False) + + @property + def time(self): + if not self.finished: + self.finished_time = timezone.now() + return self.finished_time - self.start_time + + def increment_rating(self): + if self.task.creator == self.user: + return + delta = timedelta(minutes=self.task.time_estimation) + self.score = int(delta / self.time * 100) + self.save() + self.user.userinfo.rating += self.score + self.user.userinfo.save() + + @staticmethod + def by_solution(solution): + return Progress.objects.get(task=solution.task, user=solution.user) diff --git a/Main/models/set.py b/Main/models/set.py index 32f9444..a22569a 100644 --- a/Main/models/set.py +++ b/Main/models/set.py @@ -1,8 +1,20 @@ from django.contrib.auth.models import User from django.db import models +from django.utils import timezone class Set(models.Model): name = models.TextField() public = models.BooleanField(default=False) creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) + opened = models.BooleanField(default=False) + start_time = models.DateTimeField(default=timezone.now) + end_time = models.DateTimeField(default=timezone.now) + + @property + def available(self): + return ( + self.opened + and (self.start_time is None or timezone.now() >= self.start_time) + and (self.end_time is None or timezone.now() <= self.end_time) + ) diff --git a/Main/models/solution.py b/Main/models/solution.py index 60d91c1..6b205bd 100644 --- a/Main/models/solution.py +++ b/Main/models/solution.py @@ -1,9 +1,10 @@ -from os import mkdir +from os import mkdir, walk from os.path import join, exists from shutil import rmtree from subprocess import call from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils import timezone @@ -24,6 +25,30 @@ class Solution(models.Model): rmtree(self.directory) super().delete(using=using, keep_parents=keep_parents) + @property + def files(self): + data = [] + for path, _, files in walk(self.directory): + if path.startswith(self.testing_directory): + continue + for file in files: + try: + entity = { + 'filename': file, + 'text': open(join(path, file), 'r').read() + } + end = file.split('.')[-1] + try: + highlight = 'language-' + Language.objects.get(file_type=end).highlight + except ObjectDoesNotExist: + highlight = 'nohighlight' + entity['highlight'] = highlight + data.append(entity) + except: + continue + data.sort(key=lambda x: x['filename']) + return data + def create_dirs(self): mkdir(self.directory) mkdir(self.testing_directory) diff --git a/Main/models/task.py b/Main/models/task.py index cca8c87..42d037a 100644 --- a/Main/models/task.py +++ b/Main/models/task.py @@ -12,6 +12,7 @@ class Task(models.Model): output_format = models.TextField(default="") specifications = models.TextField(default="") time_limit = models.IntegerField(default=10000) + time_estimation = models.IntegerField(default=5) creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) def __str__(self): @@ -24,3 +25,25 @@ class Task(models.Model): @property def tests(self): return ExtraFile.objects.filter(task=self, is_test=True) + + @property + def samples(self): + data = [] + for test in self.tests.order_by('test_number'): + if test.is_sample and test.readable: + data.append({ + 'input': test.text, + 'output': test.answer.text + }) + count = 1 + for entity in data: + entity["num"] = count + count += 1 + return data + + def delete(self, using=None, keep_parents=False): + from Main.models.progress import Progress + for progress in Progress.objects.filter(task=self): + progress.user.userinfo.rating -= progress.score + progress.user.userinfo.save() + super().delete(using=using, keep_parents=keep_parents) diff --git a/Main/models/userinfo.py b/Main/models/userinfo.py index c448c05..921ec99 100644 --- a/Main/models/userinfo.py +++ b/Main/models/userinfo.py @@ -17,6 +17,8 @@ class UserInfo(models.Model): profile_picture = models.ImageField(upload_to="profile_pictures", null=True) rating = models.IntegerField(default=0) user = models.OneToOneField(User, on_delete=models.CASCADE, null=True) + telegram_chat_id = models.TextField(default="") + notification_solution_result = models.BooleanField(default=False) def _append_task(self, task, tasks): if task.creator == self.user or task.public: diff --git a/Main/views/AccountView.py b/Main/views/AccountView.py index 308a0d9..c5aa6db 100644 --- a/Main/views/AccountView.py +++ b/Main/views/AccountView.py @@ -38,3 +38,11 @@ class AccountView(BaseView): self.request.user.save() login(self.request, self.request.user) return "/account?error_message=Пароль успешно установлен" + + def post_notifications(self): + self.request.user.userinfo.telegram_chat_id = self.request.POST['chat_id'] + for attr in dir(self.request.user.userinfo): + if attr.startswith('notification'): + setattr(self.request.user.userinfo, attr, attr in self.request.POST.keys()) + self.request.user.userinfo.save() + return '/account' diff --git a/Main/views/RegisterView.py b/Main/views/RegisterView.py index dacfbe6..8da6f69 100644 --- a/Main/views/RegisterView.py +++ b/Main/views/RegisterView.py @@ -12,8 +12,7 @@ class RegisterView(BaseView): self.context["error_message"] = self.request.GET.get("error_message", "") def post(self): - data = {**self.request.POST} - data["password"] = data["password"].strip() + data = self.request.POST if len(data["password"]) < 8: return "/register?error_message=Пароль слишком слабый" if data["password"] != data["repeat_password"]: diff --git a/Main/views/SolutionView.py b/Main/views/SolutionView.py new file mode 100644 index 0000000..3773355 --- /dev/null +++ b/Main/views/SolutionView.py @@ -0,0 +1,10 @@ +from SprintLib.BaseView import BaseView, AccessError + + +class SolutionView(BaseView): + view_file = 'solution.html' + required_login = True + + def pre_handle(self): + if self.entities.solution.user != self.request.user: + raise AccessError() diff --git a/Main/views/TaskRuntimeView.py b/Main/views/TaskRuntimeView.py new file mode 100644 index 0000000..a852541 --- /dev/null +++ b/Main/views/TaskRuntimeView.py @@ -0,0 +1,17 @@ +from django.http import HttpResponse + +from Main.models import Progress +from SprintLib.BaseView import BaseView + + +class TaskRuntimeView(BaseView): + view_file = 'task_runtime.html' + required_login = True + + def get(self): + progress = Progress.objects.get(task=self.entities.task, user=self.request.user) + self.context['progress'] = progress + if 'render' in self.request.GET.keys(): + return + if progress.finished: + return HttpResponse('done') diff --git a/Main/views/TaskSettingsView.py b/Main/views/TaskSettingsView.py index 55c7cb0..4f5213a 100644 --- a/Main/views/TaskSettingsView.py +++ b/Main/views/TaskSettingsView.py @@ -1,7 +1,7 @@ -from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse +from django.utils import timezone -from Main.models import ExtraFile +from Main.models import ExtraFile, Progress from SprintLib.BaseView import BaseView, AccessError @@ -12,6 +12,10 @@ class TaskSettingsView(BaseView): def pre_handle(self): if self.entities.task not in self.request.user.userinfo.available_tasks: raise AccessError() + if self.request.method == 'POST': + for progress in Progress.objects.filter(task=self.entities.task, finished=False): + progress.start_time = timezone.now() + progress.save() def get(self): self.context['error_message'] = self.request.GET.get('error_message', '') @@ -29,8 +33,10 @@ class TaskSettingsView(BaseView): name = filename.strip('.a') if not name.isnumeric(): return f'/admin/task?task_id={self.entities.task.id}&error_message=Формат файла не соответствует тесту' - ef, created = ExtraFile.objects.get_or_create(task=self.entities.task, is_test=True, test_number=int(name)) + ef, created = ExtraFile.objects.get_or_create(task=self.entities.task, is_test=True, filename=filename) if not created: + ef.is_sample = False + ef.save() return f'/admin/task?task_id={self.entities.task.id}' if ef is None or created is None: ef, created = ExtraFile.objects.get_or_create( @@ -78,3 +84,11 @@ class TaskSettingsView(BaseView): def post_create_test(self): return self._create(True) + + def post_save_test(self): + ef = ExtraFile.objects.get(id=self.request.POST['test_id']) + with open(ef.path, 'w') as fs: + fs.write(self.request.POST['text']) + ef.is_sample = 'is_sample' in self.request.POST.keys() + ef.save() + return f'/admin/task?task_id={self.entities.task.id}' diff --git a/Main/views/TaskView.py b/Main/views/TaskView.py index 0ea6e1c..38ee2a2 100644 --- a/Main/views/TaskView.py +++ b/Main/views/TaskView.py @@ -1,6 +1,6 @@ from zipfile import ZipFile -from Main.models import Solution +from Main.models import Solution, Progress from Main.tasks import start_testing from SprintLib.BaseView import BaseView, Language from SprintLib.testers import * @@ -12,6 +12,8 @@ class TaskView(BaseView): def get(self): self.context['languages'] = Language.objects.filter(opened=True).order_by('name') + progress, _ = Progress.objects.get_or_create(user=self.request.user, task=self.entities.task) + self.context['progress'] = progress def pre_handle(self): if self.request.method == 'GET': diff --git a/Main/views/TasksView.py b/Main/views/TasksView.py index 6c1ecf9..c2f3ef1 100644 --- a/Main/views/TasksView.py +++ b/Main/views/TasksView.py @@ -1,6 +1,5 @@ from Main.models import Task from SprintLib.BaseView import BaseView -from django.db.models import Q class TasksView(BaseView): diff --git a/Main/views/__init__.py b/Main/views/__init__.py index 9d75970..079cea6 100644 --- a/Main/views/__init__.py +++ b/Main/views/__init__.py @@ -9,3 +9,5 @@ from Main.views.RatingView import RatingView from Main.views.SetsView import SetsView from Main.views.TaskView import TaskView from Main.views.SolutionsTableView import SolutionsTableView +from Main.views.TaskRuntimeView import TaskRuntimeView +from Main.views.SolutionView import SolutionView diff --git a/Sprint/urls.py b/Sprint/urls.py index 7fa9686..cfa739e 100644 --- a/Sprint/urls.py +++ b/Sprint/urls.py @@ -14,7 +14,9 @@ urlpatterns = [ path("admin/task", TaskSettingsView.as_view()), path("sets", SetsView.as_view()), path("task", TaskView.as_view()), + path("solution", SolutionView.as_view()), path("solutions_table", SolutionsTableView.as_view()), + path("task_runtime", TaskRuntimeView.as_view()), path("", MainView.as_view()), path("admin/", admin.site.urls), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/SprintLib/BaseDaemon.py b/SprintLib/BaseDaemon.py index d509ff5..ba288e4 100644 --- a/SprintLib/BaseDaemon.py +++ b/SprintLib/BaseDaemon.py @@ -1,6 +1,5 @@ import asyncio import sys -from time import sleep class BaseDaemon: diff --git a/SprintLib/testers/BaseTester.py b/SprintLib/testers/BaseTester.py index ae98322..c66d8f7 100644 --- a/SprintLib/testers/BaseTester.py +++ b/SprintLib/testers/BaseTester.py @@ -4,8 +4,10 @@ from shutil import copyfile, rmtree from subprocess import call, TimeoutExpired from Main.models import ExtraFile +from Main.models.progress import Progress from Sprint.settings import CONSTS from SprintLib.utils import copy_content +from bot import bot class TestException(Exception): @@ -68,6 +70,12 @@ class BaseTester: self.test(test.filename) self.after_test() self.solution.result = CONSTS["ok_status"] + progress = Progress.objects.get(user=self.solution.user, task=self.solution.task) + if progress.finished_time is None: + progress.finished_time = self.solution.time_sent + progress.finished = True + progress.save() + progress.increment_rating() except TestException as e: self.solution.result = str(e) except TimeoutExpired: @@ -78,3 +86,11 @@ class BaseTester: self.solution.save() call(f"docker rm --force solution_{self.solution.id}", shell=True) rmtree(self.solution.testing_directory) + self.solution.user.userinfo.refresh_from_db() + if self.solution.user.userinfo.notification_solution_result: + bot.send_message(self.solution.user.userinfo.telegram_chat_id, + f'Задача: {self.solution.task.name}\n' + f'Результат: {self.solution.result}\n' + f'Очки решения: {Progress.by_solution(self.solution).score}\n' + f'Текущий рейтинг: {self.solution.user.userinfo.rating}', + parse_mode='html') diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..3d0261f --- /dev/null +++ b/bot.py @@ -0,0 +1,16 @@ +import telebot + + +bot = telebot.TeleBot("1994460106:AAGrGsCZjF6DVG_T-zycELuVfxnWw8x7UyU") + + +@bot.message_handler(commands=["start"]) +@bot.message_handler(content_types=["text"]) +def do_action(message): + bot.send_message(chat_id=message.chat.id, text=f"ID чата: {message.chat.id}") + + +if __name__ == '__main__': + print('bot is starting') + bot.polling() + print('bot failed') diff --git a/daemons/bot.py b/daemons/bot.py new file mode 100644 index 0000000..f73667b --- /dev/null +++ b/daemons/bot.py @@ -0,0 +1,6 @@ +from SprintLib.BaseDaemon import BaseDaemon + + +class Daemon(BaseDaemon): + def command(self): + return "python bot.py" diff --git a/daemons/web.py b/daemons/web.py index 398f212..dbeacb0 100644 --- a/daemons/web.py +++ b/daemons/web.py @@ -3,4 +3,4 @@ from SprintLib.BaseDaemon import BaseDaemon class Daemon(BaseDaemon): def command(self): - return "python manage.py runserver" + return "python manage.py runserver 0.0.0.0:80" diff --git a/templates/account.html b/templates/account.html index de74bbb..bd65305 100644 --- a/templates/account.html +++ b/templates/account.html @@ -87,4 +87,25 @@ {% endif %} + {% if owner %} +

+

Уведомления

+
+ {% csrf_token %} + + Бот + + + + + +
+ Результаты решений + + +
+ + +
+ {% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index c896bcd..504572b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,6 +24,10 @@ + + +