From fbdc014bf4041faa98ad6ed666a6481bffecffcd Mon Sep 17 00:00:00 2001 From: hasslesstech Date: Sat, 1 Jun 2024 19:45:47 +0300 Subject: [PATCH] initial development checkpoint 4 --- server/cgi/db/question.py | 8 +- server/cgi/db/response_option.py | 2 +- server/cgi/db/test.py | 2 +- server/cgi/question_list.py | 31 +++++++ server/cgi/response_option_list.py | 7 ++ server/cgi/test_list.py | 11 ++- server/cgi/view.py | 104 +++++++++++++++++++++--- server/css/base_style.css | 121 +++++++++++++++++++++++----- server/delete-test.py | 41 ++++++++++ server/edit-test.py | 44 ++++++++++ server/generate-response-options.py | 56 +++++++++++++ 11 files changed, 393 insertions(+), 34 deletions(-) create mode 100644 server/delete-test.py create mode 100644 server/edit-test.py create mode 100644 server/generate-response-options.py diff --git a/server/cgi/db/question.py b/server/cgi/db/question.py index 530fc43..2b389c5 100644 --- a/server/cgi/db/question.py +++ b/server/cgi/db/question.py @@ -54,11 +54,17 @@ class Question: rop = ResponseOptionPool(cur) return "
".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'
#{self.id}{self.title}{time_label}
{response_options}
' + return f'
#{self.id}{self.title}{time_label}
{correct_percentage}% відповідей правильні
{response_options}
' class QuestionPool: def __init__(self, db): diff --git a/server/cgi/db/response_option.py b/server/cgi/db/response_option.py index 8b822f5..877992c 100644 --- a/server/cgi/db/response_option.py +++ b/server/cgi/db/response_option.py @@ -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: diff --git a/server/cgi/db/test.py b/server/cgi/db/test.py index d1ab333..f9fb612 100644 --- a/server/cgi/db/test.py +++ b/server/cgi/db/test.py @@ -17,7 +17,7 @@ class Test: return self.name def render_short(self): - return f'
#{self.id}{self.name}
' + return f'
#{self.id}{self.name}
' class TestPool: def __init__(self, db): diff --git a/server/cgi/question_list.py b/server/cgi/question_list.py index 90774eb..e22d815 100644 --- a/server/cgi/question_list.py +++ b/server/cgi/question_list.py @@ -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) diff --git a/server/cgi/response_option_list.py b/server/cgi/response_option_list.py index f89d890..e1e82fd 100644 --- a/server/cgi/response_option_list.py +++ b/server/cgi/response_option_list.py @@ -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) diff --git a/server/cgi/test_list.py b/server/cgi/test_list.py index 91ab572..187b95a 100644 --- a/server/cgi/test_list.py +++ b/server/cgi/test_list.py @@ -6,7 +6,14 @@ class TestList: def __init__(self, cursor): self.cursor = cursor - def render(self): + def render(self, *args): tp = TestPool(self.cursor) - rendered_chunks = [i.render_short() for i in tp.object_pool.pool] + 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) diff --git a/server/cgi/view.py b/server/cgi/view.py index c4f170b..af19a96 100644 --- a/server/cgi/view.py +++ b/server/cgi/view.py @@ -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): @@ -79,10 +83,19 @@ class View: cur.execute("SELECT id FROM test;") test_amount = len(list(cur)) header = f'

Всього тестів: {test_amount}

' + + if 'search' in self.args: + search = self.args['search'] + else: + search = "" - subheader = f'
Створити новий тест' + subheader = f'

Створити новий тестПереглянути статистику' - content = TestList(cur).render() + 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'''

- +
''' 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"

Такого тесту не існує: {self.args['id']}

" + subheader = f'Повернутися на головну сторінку' + content = "" + return header, subheader, content + + header = f"

Редагувати тест

" + subheader = "Вкажіть нові властивості тесту нижче" + + content = f'''
+ + +
+ +
''' + + 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"

Такого тесту не існує: {self.args['id']}

" + subheader = f'Повернутися на головну сторінку' + content = "" + return header, subheader, content + + header = f"

Точно видалити цей тест?

" + subheader = f"{test_name}" + + content = f'''
Так, видалити
Ні, залишити
''' + + 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'#{self.args["id"]}{test_name}' - subheader = f'Додати запитання' + subheader = f'Додати запитанняРедагувати тест' content = QuestionList(cur).render(self.args['id']) content += '
Назад до переліку тестів
' @@ -127,7 +179,7 @@ class View:

- + ''' return header, subheader, content @@ -151,7 +203,7 @@ class View:

- + ''' 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'''Так, видалити
Ні, залишити''' + content = f'''
Так, видалити
Ні, залишити
''' return header, subheader, content @@ -211,7 +263,7 @@ class View:
- + ''' return header, subheader, content @@ -227,7 +279,7 @@ class View:
- + ''' 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'''Так, видалити
Ні, залишити''' + content = f'''
Так, видалити
Ні, залишити
''' + + return header, subheader, content + + def render_generate_response_options(self, cur): + header = f"

Генерація варіантів відповідей

" + subheader = f"Вкажіть параметри генерації нижче" + + content = f'''
+ + +
+ +
''' + + return header, subheader, content + + def render_view_stats(self, cur): + header = f"

Статистика системи

" + subheader = f'Повернутися на головну сторінку' + content = f'''''' + + test_count = TestList(cur).count() + content += f'
ТестиВсього тестів: {test_count}
' + + question_count = QuestionList(cur).count() + question_avg_time = QuestionList(cur).get_avg_time() + content += f'
ЗапитанняВсього запитань: {question_count} (в середньому по {round(question_count/test_count, 3)} зап./тест)
В середньому на запитання дається {question_avg_time}
' + + rol = ResponseOptionList(cur) + response_option_count = rol.count() + response_option_count_correct = rol.count(correct = True) + content += f'
Варіанти відповідейВсього варіантів відповідей: {response_option_count}
З-поміж них правильних: {response_option_count_correct} ({round(response_option_count_correct/response_option_count*100, 2)}%)
' return header, subheader, content diff --git a/server/css/base_style.css b/server/css/base_style.css index b9a06f7..15b4297 100644 --- a/server/css/base_style.css +++ b/server/css/base_style.css @@ -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); + } +} diff --git a/server/delete-test.py b/server/delete-test.py new file mode 100644 index 0000000..077b99d --- /dev/null +++ b/server/delete-test.py @@ -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") diff --git a/server/edit-test.py b/server/edit-test.py new file mode 100644 index 0000000..4da2754 --- /dev/null +++ b/server/edit-test.py @@ -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") diff --git a/server/generate-response-options.py b/server/generate-response-options.py new file mode 100644 index 0000000..7464362 --- /dev/null +++ b/server/generate-response-options.py @@ -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")