checkpoint

This commit is contained in:
Egor Matveev 2021-09-05 15:28:24 +03:00
parent 1307c16ec1
commit 807c52bf2b
30 changed files with 394 additions and 26 deletions

View File

@ -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

View File

@ -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')

View File

@ -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):

35
Main/models/progress.py Normal file
View File

@ -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)

View File

@ -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)
)

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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'

View File

@ -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"]:

View File

@ -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()

View File

@ -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')

View File

@ -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}'

View File

@ -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':

View File

@ -1,6 +1,5 @@
from Main.models import Task
from SprintLib.BaseView import BaseView
from django.db.models import Q
class TasksView(BaseView):

View File

@ -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

View File

@ -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)

View File

@ -1,6 +1,5 @@
import asyncio
import sys
from time import sleep
class BaseDaemon:

View File

@ -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')

16
bot.py Normal file
View File

@ -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')

6
daemons/bot.py Normal file
View File

@ -0,0 +1,6 @@
from SprintLib.BaseDaemon import BaseDaemon
class Daemon(BaseDaemon):
def command(self):
return "python bot.py"

View File

@ -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"

View File

@ -87,4 +87,25 @@
<button type="submit" class="btn btn-light">Сменить пароль</button>
</form>
{% endif %}
{% if owner %}
<hr><hr>
<h2>Уведомления</h2>
<form method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="notifications">
<input type="text" name="chat_id" value="{{ user.userinfo.telegram_chat_id }}" placeholder="telegram chat id"> <a class="btn btn-link" target="_blank" rel="noopener noreferrer" href="https://t.me/sprint_notifications_bot">Бот</a>
<table>
<tr>
<td style="width: 200px;">
Результаты решений
</td>
<td>
<input type="checkbox" name="notification_solution_result" {% if user.userinfo.notification_solution_result %}checked{% endif %}>
</td>
</tr>
</table>
<button type="submit" class="btn btn-light" style="margin-top: 15px;"><i class="fa fa-save"></i> Сохранить</button>
</form>
{% endif %}
{% endblock %}

View File

@ -24,6 +24,10 @@
<script type="text/javascript" id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js">
</script>
<link rel="stylesheet"
href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/styles/default.min.css">
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<script type="text/javascript" src={% static "js/scripts.js" %}></script>
<style type="text/css">
.center {
@ -65,6 +69,8 @@
}
.button-right {
display: flex;
position: absolute;
right:10%;
}
.task-settings-input {
width: 50%;
@ -74,9 +80,10 @@
resize: none;
}
@media screen and (max-width: 700px){
@media screen and (max-width: 1200px){
.header-button {
width:50px;
width:140px;
font-size: 15px;
}
}
{% block styles %}{% endblock %}

View File

@ -19,7 +19,7 @@
onclick="window.location.href='/rating'">
<i class="fa fa-arrow-up"></i> Рейтинг
</button>
<div class="button-right" style="margin-top: -35px;">
<div class="button-right">
<button class="btn btn-light header-button"
onclick="window.location.href='/account'">
<i class="fa fa-user"></i> Аккаунт

46
templates/solution.html Normal file
View File

@ -0,0 +1,46 @@
{% extends 'base_main.html' %}
{% block main %}
<h4>
<table class="table" style="width: 30%;">
<tr>
<td>
Id решения
</td>
<td>
{{ solution.id }}
</td>
</tr>
<tr>
<td>
Задача
</td>
<td>
<a href="/task?task_id={{ solution.task.id }}">{{ solution.task.name }}</a>
</td>
</tr>
<tr>
<td>
Язык
</td>
<td>
<img src="{{ solution.language.logo.url }}" width="30px" height="30px">
</td>
</tr>
<tr>
<td>
Результат
</td>
<td>
<span class="badge badge-{% if solution.result == in_queue_status %}secondary{% else %}{% if solution.result == ok_status %}success{% else %}{% if solution.result == testing_status %}info{% else %}danger{% endif %}{% endif %}{% endif %}">{% if solution.result == testing_status %}<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="circle-notch" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width: 20px;" class="svg-inline--fa fa-circle-notch fa-w-16 fa-spin fa-lg"><path fill="currentColor" d="M288 39.056v16.659c0 10.804 7.281 20.159 17.686 23.066C383.204 100.434 440 171.518 440 256c0 101.689-82.295 184-184 184-101.689 0-184-82.295-184-184 0-84.47 56.786-155.564 134.312-177.219C216.719 75.874 224 66.517 224 55.712V39.064c0-15.709-14.834-27.153-30.046-23.234C86.603 43.482 7.394 141.206 8.003 257.332c.72 137.052 111.477 246.956 248.531 246.667C393.255 503.711 504 392.788 504 256c0-115.633-79.14-212.779-186.211-240.236C302.678 11.889 288 23.456 288 39.056z" class=""></path></svg> {% endif %}{{ solution.result }}</span>
</td>
</tr>
</table>
</h4>
<h4>Файлы решения</h4>
{% for entity in solution.files %}
<h5>{{ entity.filename }}</h5>
<pre><code class="{{ entity.highlight }}" style="border: 1px solid black;">{{ entity.text }}</code></pre>
<hr>
{% endfor %}
{% endblock %}

View File

@ -1,7 +1,7 @@
{% for solution in solutions %}
<tr>
<td>
<b><a href="">{{ solution.id }}</a></b>
<b><a href="/solution?solution_id={{ solution.id }}">{{ solution.id }}</a></b>
</td>
<td>
{{ solution.time_sent }}

View File

@ -19,24 +19,31 @@
}
function doPoll() {
jQuery.get('/solutions_table?task_id={{ task.id }}', function(data) {
if (data == 'done') {
return
}
else {
document.getElementById('solutions').innerHTML = data;
jQuery.get('/task_runtime?task_id={{ task.id }}', function(data1) {
if (data == 'done' && data1 == 'done')
return
if (data != 'done') {
document.getElementById('solutions').innerHTML = data;
}
if (data1 != 'done') {
document.getElementById('runtime').innerHTML = data1;
}
setTimeout(function() {doPoll()}, 2000);
}
})
})
jQuery.get('/solutions_table?task_id={{ task.id }}&render=true', function(data) {
jQuery.get('/solutions_table?id={{ task.id }}&render=true', function(data) {
document.getElementById('solutions').innerHTML = data;
})
jQuery.get('/task_runtime?id={{ task.id }}&render=true', function(data) {
document.getElementById('runtime').innerHTML = data;
})
}
{% endblock %}
{% block onload %}doPoll(){% endblock %}
{% block main %}
<h2>{{ task.name }}</h2>
<div id="runtime"></div>
{% if task.legend %}
<h4>Легенда</h4>
{% autoescape off %}
@ -64,6 +71,40 @@
{{ task.specifications }}
{% endautoescape %}
<hr>
{% endif %}
{% if task.samples %}
<h4 style="">Примеры</h4>
{% for sample in task.samples %}
<h5>Пример {{ sample.num }}</h5>
<b>
<table style="width: 100%">
<tr>
<td>
Входные данные
</td>
<td>
Выходные данные
</td>
</tr>
</table>
</b>
<hr>
<table style="width: 100%;">
<tr>
<td style="width: 50%; vertical-align: top;">
<pre>
{{ sample.input }}
</pre>
</td>
<td style="width: 50%; vertical-align: top;">
<pre>
{{ sample.output }}
</pre>
</td>
</tr>
</table>
<hr>
{% endfor %}
{% endif %}
<h2>Отправить решение</h2>
<table style="margin-bottom: 10px;">

View File

@ -0,0 +1 @@
<h2>{{ task.name }}<span style="margin-left: 15px;" class="badge badge-{% if progress.finished %}success{% else %}danger{% endif %}">{{ progress.time }}</span></h2>

View File

@ -74,6 +74,14 @@
<input type="text" name="time_limit" value="{{ task.time_limit }}" class="task-settings-input">
</td>
</tr>
<tr>
<td>
Оценка времени решения (мин)
</td>
<td>
<input type="text" name="time_estimation" value="{{ task.time_estimation }}" class="task-settings-input">
</td>
</tr>
</table>
<button type="submit" class="btn btn-light" style="margin-top: 15px;"><i class="fa fa-save"></i> Сохранить</button>
</form>
@ -93,8 +101,40 @@
<td>
{% for test in task.tests %}
<div id="file_{{ test.id }}">
<i class="fa fa-file"></i> <button class="btn btn-link" {% if not test.readable %}style="color: red;"{% endif %}>{{ test.filename }}</button><button class="btn btn-link" style="color: black;" onclick="deleteFile({{ test.id }});"><i class="fa fa-times"></i> </button><br>
<i class="fa fa-file"></i> <button class="btn btn-link" {% if not test.readable %}style="color: red;" {% else %}data-toggle="modal" data-target="#filesModalLong{{ test.id }}"{% endif %}>{{ test.filename }}</button><button class="btn btn-link" style="color: black;" onclick="deleteFile({{ test.id }});"><i class="fa fa-times"></i> </button><br>
{% if test.readable %}
<form method="POST">{% csrf_token %}
<!-- Modal -->
<div class="modal fade bd-example-modal-lg" id="filesModalLong{{ test.id }}" tabindex="-1" role="dialog" aria-labelledby="filesModalLongTitle{{ test.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="filesModalLongTitle{{ test.id }}">{{ test.filename }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<textarea cols="82" rows="30" name="text">{{ test.text }}</textarea>
</div>
</div>
</div>
</div>
<div class="modal-footer">
{% if test.can_be_sample %}Использовать как пример <input type="checkbox" name="is_sample" {% if test.is_sample %}checked{% endif %}>{% endif %}
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-times"></i> Close</button>
<input name="action" value="save_test" type="hidden">
<input name="test_id" value="{{ test.id }}" type="hidden">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> Save</button>
</div>
</div>
</div>
</div>
</form>
{% endif %}
{% endfor %}
<input type="file" style="display: none;" form="form_test_upload" onchange="this.form.submit();" class="btn form-control-file" id="test-upload" value="Выбрать файл" name="file">
<label for="test-upload" class="btn btn-primary"><i class="fa fa-upload"></i> Загрузить тесты</label><button style="margin-left: 10px; margin-top: -8px;" class="btn btn-success" data-toggle="modal" data-target="#exampleModalLongnewtest" onclick="setActionCreate('create_test');"><i class="fa fa-plus"></i></button>