initial development checkpoint 4

This commit is contained in:
ІО-23 Шмуляр Олег 2024-06-01 19:45:47 +03:00
parent 54377708b0
commit fbdc014bf4
11 changed files with 393 additions and 34 deletions

View File

@ -54,11 +54,17 @@ class Question:
rop = ResponseOptionPool(cur)
return "<br>".join([i.render_short() for i in rop.select_by_question_id(self.id)])
def get_correct_response_percentage(self, cur):
rop = ResponseOptionPool(cur)
response_options = rop.select_by_question_id(self.id)
return sum([int(i.get_correctness()) for i in response_options]) / len(response_options) * 100
def render_short(self, cur):
time_label = self.get_time_label()
response_options = self.get_response_option_short_list(cur)
correct_percentage = round(self.get_correct_response_percentage(cur))
return f'<div class="question-short"><a class="question-link" href="/index.py?mode=view-question&id={self.id}"><span class="sub-label">#{self.id}</span><span class="main-label">{self.title}</span></a><span class="sub-title">{time_label}</span><div class="response-option-short-list">{response_options}</div><div class="controls"><a class="sub-button" href="/index.py?mode=edit-question&id={self.id}">Редагувати</a><a class="scary-button" href="/index.py?mode=delete-question&id={self.id}">Видалити</a></div></div>'
return f'<div class="question-short"><a class="question-link" href="/index.py?mode=view-question&id={self.id}"><span class="sub-label">#{self.id}</span><span class="main-label">{self.title}</span></a><span class="sub-title">{time_label}<br>{correct_percentage}% відповідей правильні</span><div class="response-option-short-list">{response_options}</div><div class="controls"><a class="magic-button" href="?mode=generate-response-options&id={self.id}">Згенерувати відповіді</a><a class="sub-button" href="/index.py?mode=edit-question&id={self.id}">Редагувати</a><a class="scary-button" href="/index.py?mode=delete-question&id={self.id}">Видалити</a></div></div>'
class QuestionPool:
def __init__(self, db):

View File

@ -22,7 +22,7 @@ class ResponseOption:
return self.questionID
def get_correctness(self):
return correctness
return self.correctness
def render_short(self):
if self.correctness:

View File

@ -17,7 +17,7 @@ class Test:
return self.name
def render_short(self):
return f'<div class="test-short"><a class="test-link" href="/index.py?mode=view-test&id={self.id}"><span class="sub-label">#{self.id}</span><span class="main-label">{self.name}</span></a></div>'
return f'<div class="test-short"><a class="test-link" href="/index.py?mode=view-test&id={self.id}"><span class="sub-label">#{self.id}</span><span class="main-label">{self.name}</span></a><div class="controls"><a class="sub-button" href="?mode=edit-test&id={self.id}">Редагувати</a><a class="scary-button" href="?mode=delete-test&id={self.id}">Видалити</a></div></div>'
class TestPool:
def __init__(self, db):

View File

@ -13,3 +13,34 @@ class QuestionList:
rendered_questions = [i.render_short(self.cursor) for i in qp.iter()]
return "\n".join(rendered_questions)
def count(self):
qp = QuestionPool(self.cursor)
return len(qp.object_pool.pool)
def get_avg_time(self):
qp = QuestionPool(self.cursor)
avg_time = sum([i.get_max_time() for i in qp.iter()]) / len(qp.object_pool.pool)
return self.get_time_label(avg_time)
def get_time_label(self, max_time):
total_time = max_time
hours = total_time // 3600
total_time -= hours * 3600
minutes = total_time // 60
total_time -= minutes * 60
seconds = total_time
total_label = []
if hours:
total_label.append(f"{round(hours)} год.")
if minutes:
total_label.append(f"{round(minutes)} хв.")
if seconds:
total_label.append(f"{round(seconds)} c.")
return " ".join(total_label)

View File

@ -13,3 +13,10 @@ class ResponseOptionList:
rendered_response_options = [i.render_full() for i in rop.iter()]
return "\n".join(rendered_response_options)
def count(self, correct = False):
rop = ResponseOptionPool(self.cursor)
if correct:
return len([i for i in rop.object_pool.pool if i.get_correctness()])
else:
return len(rop.object_pool.pool)

View File

@ -6,7 +6,14 @@ class TestList:
def __init__(self, cursor):
self.cursor = cursor
def render(self):
def render(self, *args):
tp = TestPool(self.cursor)
if len(args) > 0:
rendered_chunks = [i.render_short() for i in tp.object_pool.pool if args[0] in i.get_name()]
else:
rendered_chunks = [i.render_short() for i in tp.object_pool.pool]
return "\n".join(rendered_chunks)
def count(self):
tp = TestPool(self.cursor)
return len(tp.object_pool.pool)

View File

@ -20,6 +20,8 @@ class View:
self.supported_modes = {
"test-list": self.render_test_list,
"create-test": self.render_create_test,
"edit-test": self.render_edit_test,
"delete-test": self.render_delete_test,
"view-test": self.render_view_test,
"create-question": self.render_create_question,
"edit-question": self.render_edit_question,
@ -28,6 +30,8 @@ class View:
"create-response-option": self.render_create_response_option,
"edit-response-option": self.render_edit_response_option,
"delete-response-option": self.render_delete_response_option,
"generate-response-options": self.render_generate_response_options,
"view-stats": self.render_view_stats
}
def get_db_connection(self):
@ -80,9 +84,18 @@ class View:
test_amount = len(list(cur))
header = f'<h2>Всього тестів: {test_amount}</h2>'
subheader = f'<input placeholder="Шукати тести"><br><a class="generic-button" href="?mode=create-test">Створити новий тест</a>'
if 'search' in self.args:
search = self.args['search']
else:
search = ""
content = TestList(cur).render()
subheader = f'<form action="/index.py"><input name="search" placeholder="Шукати тести" value="{search}"></form><br><a class="generic-button" href="?mode=create-test">Створити новий тест</a><a class="sub-button" href="?mode=view-stats">Переглянути статистику</a>'
tl = TestList(cur)
if 'search' in self.args:
content = tl.render(self.args['search'])
else:
content = tl.render()
return header, subheader, content
@ -93,11 +106,50 @@ class View:
content = f'''<form action="/create-test.py">
<label for="name">Назва тесту:</label>
<input type="text" name="name" placeholder="Введіть назву..." required><br>
<input type="submit" value="Створити">
<input type="submit" class="submit-button" value="Створити">
</form>'''
return header, subheader, content
def render_edit_test(self, cur):
cur.execute(f"SELECT name FROM test WHERE id = {self.args['id']};")
test_name = next(iter(cur), [None])[0]
if not test_name:
header = f"<h2>Такого тесту не існує: {self.args['id']}</h2>"
subheader = f'<a href="/index.py">Повернутися на головну сторінку</a>'
content = ""
return header, subheader, content
header = f"<h2>Редагувати тест</h2>"
subheader = "<i>Вкажіть нові властивості тесту нижче</i>"
content = f'''<form action="/edit-test.py">
<input type="text" name="id" value="{self.args['id']}" style="display:none;">
<label for="name">Назва тесту:</label>
<input type="text" name="name" placeholder="Введіть назву..." value="{test_name}" required><br>
<input type="submit" class="submit-button" value="Зберегти">
</form>'''
return header, subheader, content
def render_delete_test(self, cur):
cur.execute(f"SELECT name FROM test WHERE id = {self.args['id']};")
test_name = next(iter(cur), [None])[0]
if not test_name:
header = f"<h2>Такого тесту не існує: {self.args['id']}</h2>"
subheader = f'<a href="/index.py">Повернутися на головну сторінку</a>'
content = ""
return header, subheader, content
header = f"<h2>Точно видалити цей тест?</h2>"
subheader = f"<i>{test_name}</i>"
content = f'''<div class="return-button-centerer"><a class="delete-button" href="/delete-test.py?id={self.args["id"]}">Так, видалити</a><br><a class="cancel-button" href="/">Ні, залишити</a></div>'''
return header, subheader, content
def render_view_test(self, cur):
cur.execute(f"SELECT name FROM test WHERE id = {self.args['id']};")
test_name = next(iter(cur), [None])[0]
@ -110,7 +162,7 @@ class View:
header = f'<span class="view-test-id-tag">#{self.args["id"]}</span><span class="view-test-main-tag">{test_name}</span>'
subheader = f'<a class="generic-button" href="?mode=create-question&test_id={self.args["id"]}">Додати запитання</a>'
subheader = f'<a class="generic-button" href="?mode=create-question&test_id={self.args["id"]}">Додати запитання</a><a class="sub-button" href="?mode=edit-test&id={self.args["id"]}">Редагувати тест</a>'
content = QuestionList(cur).render(self.args['id'])
content += '<div class="return-button-centerer"><a class="return-button" href="/index.py">Назад до переліку тестів</a></div>'
@ -127,7 +179,7 @@ class View:
<input type="text" name="title" placeholder="Введіть запитання..." required><br>
<label for="mtime">Максимальний час на відповідь (в сек):</label>
<input type="number" name="mtime" placeholder="Наприклад, 120"><br>
<input type="submit" value="Додати">
<input type="submit" class="submit-button" value="Додати">
</form>'''
return header, subheader, content
@ -151,7 +203,7 @@ class View:
<input type="text" name="title" placeholder="Введіть запитання..." value="{escape_html(question_title)}" required><br>
<label for="mtime">Максимальний час на відповідь (в сек):</label>
<input type="number" name="mtime" placeholder="Наприклад, 120" value="{question_max_time}"><br>
<input type="submit" value="Зберегти зміни">
<input type="submit" class="submit-button" value="Зберегти">
</form>'''
return header, subheader, content
@ -172,7 +224,7 @@ class View:
cur.execute(f"SELECT test.id FROM test JOIN question ON test.id = question.tstID WHERE question.id = {self.args['id']};")
test_id = iter(cur).__next__()[0]
content = f'''<a class="delete-button" href="/delete-question.py?id={self.args["id"]}">Так, видалити</a><br><a class="cancel-button" href="/index.py?mode=view-test&id={test_id}">Ні, залишити</a>'''
content = f'''<div class="return-button-centerer"><a class="delete-button" href="/delete-question.py?id={self.args["id"]}">Так, видалити</a><br><a class="cancel-button" href="/index.py?mode=view-test&id={test_id}">Ні, залишити</a></div>'''
return header, subheader, content
@ -211,7 +263,7 @@ class View:
<input type="text" name="question_id" value="{self.args["question_id"]}" style="display:none;">
<label for="label">Текст відповіді:</label>
<input type="text" name="label" placeholder="Введіть текст відповіді..." required><br>
<input type="submit" value="Додати">
<input type="submit" class="submit-button" value="Додати">
</form>'''
return header, subheader, content
@ -227,7 +279,7 @@ class View:
<input type="text" name="id" value="{self.args["id"]}" style="display:none;">
<label for="label">Текст відповіді:</label>
<input type="text" name="label" placeholder="Введіть текст відповіді..." value="{respose_option_label}" required><br>
<input type="submit" value="Оновити">
<input type="submit" class="submit-button" value="Зберегти">
</form>'''
return header, subheader, content
@ -242,6 +294,38 @@ class View:
cur.execute(f"SELECT question.id FROM question JOIN response_option ON question.id = response_option.qstID WHERE response_option.id = {self.args['id']};")
quest_id = iter(cur).__next__()[0]
content = f'''<a class="delete-button" href="/delete-response-option.py?id={self.args["id"]}">Так, видалити</a><br><a class="cancel-button" href="/index.py?mode=view-question&id={quest_id}">Ні, залишити</a>'''
content = f'''<div class="return-button-centerer"><a class="delete-button" href="/delete-response-option.py?id={self.args["id"]}">Так, видалити</a><br><a class="cancel-button" href="/index.py?mode=view-question&id={quest_id}">Ні, залишити</a></div>'''
return header, subheader, content
def render_generate_response_options(self, cur):
header = f"<h2>Генерація варіантів відповідей</h2>"
subheader = f"<i>Вкажіть параметри генерації нижче</i>"
content = f'''<form action="/generate-response-options.py">
<input type="text" name="id" value="{self.args["id"]}" style="display:none;">
<label for="label">Кількість варіантів:</label>
<input type="number" name="amount" placeholder="Наприклад, 4" required><br>
<input type="submit" class="magic-button" value="Згенерувати">
</form>'''
return header, subheader, content
def render_view_stats(self, cur):
header = f"<h2>Статистика системи</h2>"
subheader = f'<a class="sub-button" href="/">Повернутися на головну сторінку</a>'
content = f''''''
test_count = TestList(cur).count()
content += f'<div class="piece-of-stats"><span class="header">Тести</span>Всього тестів: {test_count}</div>'
question_count = QuestionList(cur).count()
question_avg_time = QuestionList(cur).get_avg_time()
content += f'<div class="piece-of-stats"><span class="header">Запитання</span>Всього запитань: {question_count} (в середньому по {round(question_count/test_count, 3)} зап./тест)<br>В середньому на запитання дається {question_avg_time}</div>'
rol = ResponseOptionList(cur)
response_option_count = rol.count()
response_option_count_correct = rol.count(correct = True)
content += f'<div class="piece-of-stats"><span class="header">Варіанти відповідей</span>Всього варіантів відповідей: {response_option_count}<br>З-поміж них правильних: {response_option_count_correct} ({round(response_option_count_correct/response_option_count*100, 2)}%)</div>'
return header, subheader, content

View File

@ -2,8 +2,9 @@
--yellow-gradient: linear-gradient(45deg, #ffe88d, #fff8b7);
--yellow-gradient-bright: linear-gradient(45deg, #ffeba1, #fffacb);
--blue-gradient-subtle: linear-gradient(45deg, #e7ebff, #e7efff);
--blue-gradient-glassy: linear-gradient(45deg, #fff9, #fff9);
--blue-glassy: #fff9;
--red-gradient: linear-gradient(45deg, #ffd2cc, #ffcccc);
--magical: linear-gradient(45deg, #e0f2ff, #e4fff0, #eee0ff);
}
* {
@ -34,11 +35,9 @@ header .top-half, header .lower-half {
text-align: center;
}
header .top-half {
margin-bottom: 12px;
}
header .lower-half input {
header .top-half,
header .lower-half input
{
margin-bottom: 12px;
}
@ -58,7 +57,10 @@ header .lower-half a.generic-button,
header .lower-half a.sub-button,
main * a.sub-button,
main * a.scary-button,
main a.return-button
main a.return-button,
main a.delete-button,
main a.cancel-button,
main a.magic-button
{
text-decoration: none;
border-radius: 15px;
@ -67,9 +69,7 @@ main a.return-button
font-weight: 700;
margin: 4px;
margin-top: 0;
padding: 10px;
padding-left: 20px;
padding-right: 20px;
padding: 10px 20px 10px 20px;
display: inline-block;
}
@ -83,7 +83,8 @@ header .lower-half a.generic-button:hover {
header .lower-half a.sub-button,
main * a.sub-button,
main a.return-button
main a.return-button,
main a.cancel-button
{
border: 1px solid #000;
}
@ -97,26 +98,45 @@ main * div.controls
justify-content: end;
}
main * a.sub-button {
}
main * a.scary-button
main * a.scary-button,
main a.delete-button
{
background: var(--red-gradient);
color: #500;
}
main a.magic-button
{
background: var(--magical);
/*
animation-name: magic-ani;
animation-duration: 6s;
animation-iteration-count: infinite;
animation-timing-function: linear;
*/
}
main .test-short,
main .question-short,
main .response-option
main .response-option,
main .piece-of-stats
{
padding: 20px;
background: var(--blue-gradient-glassy);
background: var(--blue-glassy);
margin-bottom: 16px;
border-radius: 14px;
vertical-align: middle;
}
main .piece-of-stats .header
{
display: block;
font-size: 16pt;
font-weight: 600;
margin-bottom: 10px;
}
main .test-short a.test-link,
main .question-short a.question-link {
display: block;
@ -151,6 +171,52 @@ main * .sub-title {
margin-top: 12px;
}
main form label
{
display: block;
margin-bottom: 10px;
font-size: 12pt;
}
main form input,
header .lower-half form input
{
display: block;
border-radius: 20px;
padding: 12px 30px 12px 30px;
font-size: 15pt;
width: 90%;
margin: auto;
border: 1px solid #fff;
transition-duration: 300ms;
}
main form input:focus,
header .lower-half form input:focus
{
outline: none;
border: 1px solid;
transition-duration: 300ms;
}
main form input[type=submit]
{
width: auto;
border: none;
border-radius: 20px;
padding: 12px 30px 12px 30px;
background: var(--yellow-gradient);
font-weight: 600;
font-size: 15pt;
}
main form input[type=submit]:hover
{
cursor: pointer;
background: var(--yellow-gradient-bright);
}
main * .response-option-short-list
{
margin-top: 12px;
@ -166,7 +232,8 @@ main * .response-option-short
font-weight: 500;
}
main .response-option a.response-option-mark {
main .response-option a.response-option-mark
{
font-size: 16pt;
text-decoration: none;
display: inline-block;
@ -183,8 +250,24 @@ main .return-button-centerer
display: grid;
}
main a.return-button
main a.return-button,
main a.delete-button,
main a.cancel-button
{
margin: auto;
margin-bottom: 15px;
}
header .lower-half form input
{
transition-duration: 300ms;
@keyframes magic-ani
{
0% {
filter: hue-rotate(0deg);
}
100% {
filter: hue-rotate(360deg);
}
}

41
server/delete-test.py Normal file
View File

@ -0,0 +1,41 @@
import mariadb as mdb
import traceback
import json
import sys
import os
from httputils import parse_query, escape_sql_string
def readfile(path):
if os.path.exists(path):
return open(path).read()
args = {"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "",
"database": "test_holder"}
settings = json.loads(readfile("cgi/db-settings.json"))
args.update(settings)
db_connection = mdb.connect(**args)
args = parse_query(os.environ['QUERY_STRING'])
if not 'id' in args:
print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили ідентифікатор тесту\r\n")
sys.exit(0)
cur = db_connection.cursor()
try:
#cur.execute(f"SELECT test.id FROM test JOIN question ON test.id = question.tstID WHERE question.id = {args['id']};")
#test_id = iter(cur).__next__()[0]
cur.execute(f"DELETE FROM test WHERE id = {args['id']};")
db_connection.commit()
print(f"Location: /\r\n\r\n")
except Exception as e:
print(f"Content-Type: text/plain; charset=UTF-8\r\n\r\nНе вдалося видалити запитання ({e})\r\n{traceback.format_exc()}\r\n")

44
server/edit-test.py Normal file
View File

@ -0,0 +1,44 @@
import mariadb as mdb
import json
import sys
import os
from httputils import parse_query, escape_sql_string
def readfile(path):
if os.path.exists(path):
return open(path).read()
args = {"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "",
"database": "test_holder"}
settings = json.loads(readfile("cgi/db-settings.json"))
args.update(settings)
db_connection = mdb.connect(**args)
args = parse_query(os.environ['QUERY_STRING'])
if not 'id' in args:
print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили ідентифікатор тесту\r\n")
sys.exit(0)
if not 'name' in args:
print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили назву тесту\r\n")
sys.exit(0)
cur = db_connection.cursor()
try:
cur.execute(f"UPDATE test SET name = '{escape_sql_string(args['name'])}' WHERE id = {args['id']};")
db_connection.commit()
#cur.execute(f"SELECT id FROM question ORDER BY id DESC;")
#new_id = iter(cur).__next__()[0]
print(f"Location: /index.py?mode=view-test&id={args['id']}\r\n\r\n")
except Exception as e:
print(f"Content-Type: text/plain; charset=UTF-8\r\n\r\nНе вдалося змінити тест ({e})\r\n")

View File

@ -0,0 +1,56 @@
import mariadb as mdb
import random
import json
import sys
import os
from httputils import parse_query, escape_sql_string
def readfile(path):
if os.path.exists(path):
return open(path).read()
args = {"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "",
"database": "test_holder"}
settings = json.loads(readfile("cgi/db-settings.json"))
args.update(settings)
db_connection = mdb.connect(**args)
args = parse_query(os.environ['QUERY_STRING'])
if not 'id' in args:
print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили ідентифікатор запитання, до якого генеруватимуться варіанти відповідей\r\n")
sys.exit(0)
if not 'amount' in args:
print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили кількість нових варіантів відповідей\r\n")
sys.exit(0)
cur = db_connection.cursor()
try:
chosen_the_right_one = False
for i in range(int(args['amount'])):
if not chosen_the_right_one:
if int(args['amount']) == i + 1:
correct = True
else:
correct = random.random() > 0.6
else:
correct = False
label = str(random.randint(0, 120))
if correct:
chosen_the_right_one = True
cur.execute(f"INSERT INTO response_option ( label, qstID, corct ) VALUES ( '{escape_sql_string(label)}', {args['id']}, {correct} );")
db_connection.commit()
print(f"Location: /index.py?mode=view-question&id={args['id']}\r\n\r\n")
except Exception as e:
print(f"Content-Type: text/plain; charset=UTF-8\r\n\r\nНе вдалося згенерувати варіанти відповідей ({e})\r\n")