From b4c1a23ce270c8221b3dfc3674f208e24b476c7f Mon Sep 17 00:00:00 2001 From: hasslesstech Date: Tue, 28 May 2024 17:47:08 +0300 Subject: [PATCH] initial development checkpoint 2 --- server/cgi/db/question.py | 36 ++++++- server/cgi/db/response_option.py | 35 ++++--- server/cgi/db/test.py | 2 +- server/cgi/question_list.py | 15 +++ server/cgi/response_option_list.py | 15 +++ server/cgi/view.py | 150 ++++++++++++++++++++++++---- server/create-question.py | 47 +++++++++ server/create-response-option.py | 47 +++++++++ server/create-test.py | 40 ++++++++ server/css/base_style.css | 154 +++++++++++++++++++++++++++++ server/edit-question.py | 47 +++++++++ server/flip-correctness.py | 44 +++++++++ server/httputils.py | 20 ++++ server/index.py | 33 +------ 14 files changed, 618 insertions(+), 67 deletions(-) create mode 100644 server/cgi/question_list.py create mode 100644 server/cgi/response_option_list.py create mode 100644 server/create-question.py create mode 100644 server/create-response-option.py create mode 100644 server/create-test.py create mode 100644 server/edit-question.py create mode 100644 server/flip-correctness.py diff --git a/server/cgi/db/question.py b/server/cgi/db/question.py index c603bba..6edf398 100644 --- a/server/cgi/db/question.py +++ b/server/cgi/db/question.py @@ -1,4 +1,4 @@ -from object_pool import ObjectPool +from db.object_pool import ObjectPool class Question: def init(self, sID, title, max_time, test_id): @@ -24,6 +24,32 @@ class Question: def get_test_id(self): return self.test_id + def render_short(self): + total_time = self.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"{hours} год.") + if minutes: + total_label.append(f"{minutes} хв.") + if seconds: + total_label.append(f"{seconds} c.") + + total_time = " ".join(total_label) + + if int(self.max_time) > 0: + return f'
#{self.id}{self.title}На відповідь є {total_time}
' + else: + return f'
#{self.id}{self.title}Час на відповідь не обмежений
' + ''' class QuestionPool: def __init__(self): @@ -36,8 +62,14 @@ class QuestionPool: ''' class QuestionPool: - def __init__(self): + def __init__(self, db): self.object_pool = ObjectPool("question", Question) if db: self.object_pool.load_from_db(db) + + def iter(self): + return iter(self.object_pool.pool) + + def select_by_test_id(self, test_id): + return [i for i in self.object_pool.pool if i.get_test_id() == int(test_id)] diff --git a/server/cgi/db/response_option.py b/server/cgi/db/response_option.py index 405f936..45133dc 100644 --- a/server/cgi/db/response_option.py +++ b/server/cgi/db/response_option.py @@ -1,4 +1,4 @@ -from object_pool import ObjectPool +from db.object_pool import ObjectPool class ResponseOption: def init(self, sID, label, questionID, correctness): @@ -24,20 +24,31 @@ class ResponseOption: def get_correctness(self): return correctness -class ResponceOptionPool: - def __init__(self): + def render_short(self): + if self.correctness: + c_mark = "+ " + else: + c_mark = " " + + return f'{c_mark} {self.label}' + + def render_full(self): + if self.correctness: + c_mark = "+" + else: + c_mark = "-" + + return f'
{c_mark}{self.label}
' + +class ResponseOptionPool: + def __init__(self, db): self.object_pool = ObjectPool("response_option", ResponseOption) if db: self.object_pool.load_from_db(db) -''' -class ResponceOptionPool: - def __init__(self): - self.pool = [] + def iter(self): + return iter(self.object_pool.pool) - def load_from_db(self, cur): - db.execute("SELECT * FROM response_option;") - self.pool = [ResponseOption().init_from_data(i) for i in cur] - return self -''' + def select_by_question_id(self, question_id): + return [i for i in self.object_pool.pool if i.get_question_id() == int(question_id)] diff --git a/server/cgi/db/test.py b/server/cgi/db/test.py index ec899bf..d1ab333 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.sID} {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 new file mode 100644 index 0000000..3c32fef --- /dev/null +++ b/server/cgi/question_list.py @@ -0,0 +1,15 @@ +from db.question import QuestionPool + +class QuestionList: + def __init__(self, cursor): + self.cursor = cursor + + def render(self, test_id = None): + qp = QuestionPool(self.cursor) + + if test_id: + rendered_questions = [i.render_short() for i in qp.select_by_test_id(test_id)] + else: + rendered_questions = [i.render_short() for i in qp.iter()] + + return "\n".join(rendered_questions) diff --git a/server/cgi/response_option_list.py b/server/cgi/response_option_list.py new file mode 100644 index 0000000..f89d890 --- /dev/null +++ b/server/cgi/response_option_list.py @@ -0,0 +1,15 @@ +from db.response_option import ResponseOptionPool + +class ResponseOptionList: + def __init__(self, cursor): + self.cursor = cursor + + def render(self, question_id = None): + rop = ResponseOptionPool(self.cursor) + + if question_id: + rendered_response_options = [i.render_full() for i in rop.select_by_question_id(question_id)] + else: + rendered_response_options = [i.render_full() for i in rop.iter()] + + return "\n".join(rendered_response_options) diff --git a/server/cgi/view.py b/server/cgi/view.py index ca30e8e..1520ccb 100644 --- a/server/cgi/view.py +++ b/server/cgi/view.py @@ -3,6 +3,10 @@ import json import os from test_list import TestList +from question_list import QuestionList +from response_option_list import ResponseOptionList + +from httputils import escape_html def readfile(path): if os.path.exists(path): @@ -13,6 +17,15 @@ class View: self.args = query_args self.connector_data = connector_data self.db_connection = None + self.supported_modes = { + "test-list": self.render_test_list, + "create-test": self.render_create_test, + "view-test": self.render_view_test, + "create-question": self.render_create_question, + "edit-question": self.render_edit_question, + "view-question": self.render_view_question, + "create-response-option": self.render_create_response_option, + } def get_db_connection(self): if not self.db_connection: @@ -49,29 +62,124 @@ class View: else: mode = "test-list" - if mode == "test-list": - cur.execute("SELECT id FROM test;") - test_amount = len(list(cur)) - header = f'

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

' - - subheader = f'
Створити тест' - - content = TestList(cur).render() - - elif mode == "create-test": - header = f"

Створити новий тест" - subheader = "Введіть властивості нового тесту нижче" - - content = f'''
-
- - -
''' - - else: + if mode not in self.supported_modes: header = f"

No such view mode: {mode}

" subheader = f'Повернутися на головну сторінку' content = "" - + else: + header, subheader, content = self.supported_modes[mode](cur) + dbc.close() return self.format_page(header, subheader, content) + + def render_test_list(self, cur): + cur.execute("SELECT id FROM test;") + test_amount = len(list(cur)) + header = f'

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

' + + subheader = f'
Створити новий тест' + + content = TestList(cur).render() + + return header, subheader, content + + def render_create_test(self, cur): + header = f"

Створити новий тест

" + subheader = "Введіть властивості нового тесту нижче" + + 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] + + if not test_name: + header = f"

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

" + subheader = f'Повернутися на головну сторінку' + content = "" + return header, subheader, content + + header = f'#{self.args["id"]}{test_name}' + + subheader = f'Додати запитання' + + content = QuestionList(cur).render(self.args['id']) + + return header, subheader, content + + def render_create_question(self, cur): + header = f"

Додати нове запитання

" + subheader = "Введіть властивості нового запитання нижче" + + content = f'''
+ + +
+ +
+ +
''' + + return header, subheader, content + + def render_edit_question(self, cur): + cur.execute(f"SELECT title, mtime FROM question WHERE id = {self.args['id']};") + question_title, question_max_time = next(iter(cur), [None, None]) + + if not question_title: + header = f"

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

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

Змінити запитання

" + subheader = "Відредагуйте властивості запитання нижче" + + content = f'''
+ + +
+ +
+ +
''' + + return header, subheader, content + + def render_view_question(self, cur): + cur.execute(f"SELECT title FROM question WHERE id = {self.args['id']};") + question_name = next(iter(cur), [None])[0] + + if not question_name: + header = f"

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

" + subheader = f'Повернутися на головну сторінку' + content = "" + return header, subheader, content + + header = f'#{self.args["id"]}{question_name}' + + subheader = f'Додати варіант відповіді' + \ + f'Редагувати запитання' + + content = ResponseOptionList(cur).render(self.args['id']) + + return header, subheader, content + + def render_create_response_option(self, cur): + header = f"

Додати новий варіант відповіді

" + subheader = "Введіть властивості варіанту відповіді нижче" + + content = f'''
+ + +
+ +
''' + + return header, subheader, content diff --git a/server/create-question.py b/server/create-question.py new file mode 100644 index 0000000..0daf5aa --- /dev/null +++ b/server/create-question.py @@ -0,0 +1,47 @@ +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 'test_id' in args: + print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили ідентифікатор тесту, до якого має залежати запитання\r\n") + sys.exit(0) + +if not 'title' in args: + print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили текст запитання\r\n") + sys.exit(0) + +if not 'mtime' in args: + args['mtime'] = 0 + +cur = db_connection.cursor() + +try: + cur.execute(f"INSERT INTO question ( title, mtime, tstID ) VALUES ( '{escape_sql_string(args['title'])}', {args['mtime']}, {args['test_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-question&id={new_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/create-response-option.py b/server/create-response-option.py new file mode 100644 index 0000000..5e809b1 --- /dev/null +++ b/server/create-response-option.py @@ -0,0 +1,47 @@ +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 'question_id' in args: + print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили ідентифікатор запитання, якого стосується цей варіант відповіді\r\n") + sys.exit(0) + +if not 'label' in args: + print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили текст варіанту відповіді\r\n") + sys.exit(0) + +if not 'mtime' in args: + args['mtime'] = 0 + +cur = db_connection.cursor() + +try: + cur.execute(f"INSERT INTO response_option ( label, qstID, corct ) VALUES ( '{escape_sql_string(args['label'])}', {args['question_id']}, 0 );") + db_connection.commit() + + #cur.execute(f"SELECT id FROM response ORDER BY id DESC;") + + #new_id = iter(cur).__next__()[0] + print(f"Location: /index.py?mode=view-question&id={args['question_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/create-test.py b/server/create-test.py new file mode 100644 index 0000000..1253530 --- /dev/null +++ b/server/create-test.py @@ -0,0 +1,40 @@ +import mariadb as mdb +import json +import sys +import os + +from httputils import parse_query + +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 '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"INSERT INTO test ( name ) VALUES ( '{args['name']}' );") + db_connection.commit() + + cur.execute(f"SELECT id FROM test ORDER BY id DESC;") + + new_id = iter(cur).__next__()[0] + print(f"Location: /index.py?mode=view-test&id={new_id}\r\n\r\n") +except: + print(f"Content-Type: text/plain\r\n\r\nНе вдалося створити новий тест\r\n") diff --git a/server/css/base_style.css b/server/css/base_style.css index c74a4c7..81d44c3 100644 --- a/server/css/base_style.css +++ b/server/css/base_style.css @@ -1,8 +1,162 @@ +:root { + --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); + --red-gradient: linear-gradient(45deg, #ffd2cc, #ffcccc); +} + * { margin: 0; padding: 0; + font-family: sans-serif; } html { width: 100%; + background: var(--blue-gradient-subtle); +} + +body { + max-width: 600px; + margin: auto; +} + +header { + border: solid 2px; + padding: 10px; + margin-top: 20px; + margin-bottom: 20px; + border-radius: 8px; +} + +header .top-half, header .lower-half { + text-align: center; +} + +header .top-half { + margin-bottom: 12px; +} + +header .lower-half input { + margin-bottom: 12px; +} + +header .top-half .view-test-id-tag { + color: #333333; + font-size: 16pt; + margin-right: 12px; +} + +header .top-half .view-test-main-tag { + color: #000; + font-size: 20pt; + font-weight: 600; +} + +header .lower-half a.generic-button, +header .lower-half a.sub-button, +main * a.sub-button, +main * a.scary-button +{ + text-decoration: none; + border-radius: 15px; + color: #000; + text-align: center; + font-weight: 700; + margin: 4px; + margin-top: 0; + padding: 10px; + padding-left: 20px; + padding-right: 20px; + display: inline-block; +} + +header .lower-half a.generic-button { + background: var(--yellow-gradient); +} + +header .lower-half a.generic-button:hover { + background: var(--yellow-gradient-bright); +} + +header .lower-half a.sub-button, +main * a.sub-button +{ + border: 1px solid #000; +} + +main * div.controls +{ + display: grid; + grid-auto-flow: column; + margin-top: 13px; + grid-auto-columns: max-content; + justify-content: end; +} + +main * a.sub-button { +} + +main * a.scary-button +{ + background: var(--red-gradient); + color: #500; +} + +main .test-short, +main .question-short, +main .response-option +{ + padding: 20px; + background: var(--blue-gradient-glassy); + margin-top: 16px; + border-radius: 14px; + vertical-align: middle; +} + +main .test-short a.test-link, +main .question-short a.question-link { + display: block; + text-decoration: none; +} + +main .test-short a.test-link .main-label { + color: #002; + font-weight: 700; + font-size: 16pt; +} + +main .test-short a.test-link .sub-label, +main .question-short a.question-link .sub-label { + color: #333; + margin-right: 11px; +} + +main .test-short a.test-link { + display: block; + text-decoration: none; +} + +main .question-short a.question-link .main-label { + color: #002; + font-weight: 600; + font-size: 15pt; +} + +main * .sub-title { + display: block; + margin-top: 12px; +} + +main .response-option a.response-option-mark { + font-size: 16pt; + text-decoration: none; + display: inline-block; + padding: 2px 10px 3px 10px; + margin-right: 12px; + color: #001; + border-radius: 24px; + border: solid 1px; + font-family: monospace; } diff --git a/server/edit-question.py b/server/edit-question.py new file mode 100644 index 0000000..993b9aa --- /dev/null +++ b/server/edit-question.py @@ -0,0 +1,47 @@ +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 'question_id' in args: + print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили ідентифікатор тесту, до якого має залежати запитання\r\n") + sys.exit(0) + +if not 'title' in args: + print("Content-Type: text/plain; charset=UTF-8\r\n\r\nВи не зазначили текст запитання\r\n") + sys.exit(0) + +if not 'mtime' in args or not args['mtime']: + args['mtime'] = 0 + +cur = db_connection.cursor() + +try: + cur.execute(f"UPDATE question SET title = '{escape_sql_string(args['title'])}', mtime = {args['mtime']} WHERE id = {args['question_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-question&id={args['question_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/flip-correctness.py b/server/flip-correctness.py new file mode 100644 index 0000000..60f7d5b --- /dev/null +++ b/server/flip-correctness.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 'res_opt_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 corct, qstID FROM response_option WHERE id = {args['res_opt_id']};") + data = iter(cur).__next__() + new_correctness = not bool(data[0]) + + cur.execute(f"UPDATE response_option SET corct = {int(new_correctness)} WHERE id = {args['res_opt_id']};") + db_connection.commit() + + #cur.execute(f"SELECT id FROM response ORDER BY id DESC;") + + #new_id = iter(cur).__next__()[0] + print(f"Location: /index.py?mode=view-question&id={data[1]}\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/httputils.py b/server/httputils.py index dbbb6ae..a9920a3 100644 --- a/server/httputils.py +++ b/server/httputils.py @@ -1,3 +1,11 @@ +html_escaping = { + "<": "<", + ">": ">", + "\"": """ +} + + + def decode_url(url): i = 0 end = len(url) @@ -14,6 +22,9 @@ def decode_url(url): i += 3 except: i += 3 + elif url[i] == "+": + decode_buffer += " " + i += 1 else: decode_buffer += url[i] i += 1 @@ -29,3 +40,12 @@ def parse_query(query): query_dict[decode_url(k)] = decode_url(v) return query_dict + +def escape_sql_string(s): + return s.replace("'", "''") + +def escape_html(s): + for k, v in html_escaping.items(): + s = s.replace(k, v) + + return s diff --git a/server/index.py b/server/index.py index 6c0e679..d007c13 100644 --- a/server/index.py +++ b/server/index.py @@ -1,43 +1,14 @@ import sys import os -print(f"PWD: {os.getcwd()}", file=sys.stderr) +#print(f"PWD: {os.getcwd()}", file=sys.stderr) -print(f"Environ: {os.environ}", file=sys.stderr) +#print(f"Environ: {os.environ}", file=sys.stderr) sys.path.insert(0, "/root/ipz-server-1/server/cgi/") from view import View from httputils import parse_query -''' -def decode_url(url): - i = 0 - end = len(url) - decode_buffer = '' - char_buffer = bytearray() - - while i < end: - if url[i] == '%': - try: - char_buffer.append(int(f'0x{url[i+1:i+3]}', 16)) - decode_buffer += char_buffer.decode("UTF-8") - del char_buffer[:] - i += 3 - except: - i += 3 - else: - decode_buffer += url[i] - i += 1 - - return decode_buffer - -query = [i for i in os.environ['QUERY_STRING'].split("&") if i] - -query_dict = {} -for i in query: - k, v = i.split("=") - query_dict[decode_url(k)] = decode_url(v) -''' query_dict = parse_query(os.environ['QUERY_STRING'])