summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew <saintruler@gmail.com>2019-07-21 12:14:09 +0400
committerAndrew <saintruler@gmail.com>2019-07-21 12:14:09 +0400
commit2305ced85888a23f86ecfcdfb64a3b69c4997a4c (patch)
tree9d5cc4ad1c322ca2bf46b990498ad0a79d508aff
parent8c8712f0e6b165f6967f5d2958f300df6182296c (diff)
Переписан бекенд. Добавлена валидация при обновлении значений в ряду.
Удалена предыдущая версия приложения без vue.
-rw-r--r--day9/task5/database.py43
-rw-r--r--day9/task5/index.html47
-rw-r--r--day9/task5/main.py192
-rw-r--r--day9/task5/router.py41
-rw-r--r--day9/task5/static/css/ContentBoxStyle.css26
-rw-r--r--day9/task5/static/css/FormStyle.css61
-rw-r--r--day9/task5/static/css/NewEntryStyle.css38
-rw-r--r--day9/task5/static/css/TableStyle.css22
-rw-r--r--day9/task5/static/images/upload_icon.pngbin57506 -> 0 bytes
-rw-r--r--day9/task5/static/js/config.js2
-rw-r--r--day9/task5/static/js/editForm.js142
-rw-r--r--day9/task5/static/js/main.js70
-rw-r--r--day9/task5/static/js/overlayButtonsCallbacks.js26
-rw-r--r--day9/task5/static/js/tableCallbacks.js116
-rw-r--r--day9/task5/static/js/utils.js33
-rw-r--r--day9/task5_vue/backend/core/response_types.py (renamed from day9/task5_vue/backend/core.py)2
-rw-r--r--day9/task5_vue/backend/core/router.py49
-rw-r--r--day9/task5_vue/backend/core/server.py (renamed from day9/task5_vue/backend/server.py)36
-rw-r--r--day9/task5_vue/backend/core/utils.py (renamed from day9/task5_vue/backend/utils.py)0
-rw-r--r--day9/task5_vue/backend/database.py43
-rw-r--r--day9/task5_vue/backend/database/database.py29
-rw-r--r--day9/task5_vue/backend/database/field_types.py128
-rw-r--r--day9/task5_vue/backend/database/scheme.py46
-rw-r--r--day9/task5_vue/backend/database/validators.py96
-rw-r--r--day9/task5_vue/backend/database/wrappers.py149
-rw-r--r--day9/task5_vue/backend/router.py41
-rw-r--r--day9/task5_vue/backend/routes.py15
-rw-r--r--day9/task5_vue/backend/schemes.py23
-rw-r--r--day9/task5_vue/backend/settings.py18
-rw-r--r--day9/task5_vue/backend/views.py (renamed from day9/task5_vue/backend_serve.py)120
-rw-r--r--day9/task5_vue/package-lock.json2
-rw-r--r--day9/task5_vue/package.json2
-rw-r--r--day9/task5_vue/runserver.py27
-rw-r--r--day9/task5_vue/src/components/EditFormBox.vue104
34 files changed, 717 insertions, 1072 deletions
diff --git a/day9/task5/database.py b/day9/task5/database.py
deleted file mode 100644
index b30f8ca..0000000
--- a/day9/task5/database.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import MySQLdb
-from day9.task5_vue.config import *
-
-import logging
-
-
-def db_column_names():
- cursor = db.cursor()
- cursor.execute('DESCRIBE table_task1;')
- table_structure = cursor.fetchall()
- table_headers = [field[0] for field in table_structure]
- return table_headers
-
-
-logger = logging.getLogger('tableApp')
-logger.info(f'Trying to connect to database "{DATABASE_NAME}@{HOST}"...')
-# В файле config.py создайте соответствующие переменные
-db = MySQLdb.connect(
- host=HOST,
- user=USERNAME,
- passwd=PASSWORD,
- db=DATABASE_NAME
-)
-logger.info('Connected')
-
-logger.info(f'Preparing table "table_task1"...')
-db.cursor().execute(
- '''
- CREATE TABLE IF NOT EXISTS `table_task1` (
- `service_id` int(11) NOT NULL AUTO_INCREMENT,
- `servtype` varchar(20) NOT NULL DEFAULT 'hosting',
- `subtype` varchar(32) NOT NULL DEFAULT '',
- `user_id` bigint(20) NOT NULL,
- `referrer_user_id` bigint(20) NOT NULL,
- `state` varchar(1) NOT NULL DEFAULT 'N',
- `creation_date` date NOT NULL DEFAULT '0000-01-01',
- `creation_time` time NOT NULL DEFAULT '00:00:00',
- `creation_request_sent_date` datetime DEFAULT NULL,
- `notified_about_expiration` smallint(6) NOT NULL DEFAULT '0',
- PRIMARY KEY (`service_id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=35109400 DEFAULT CHARSET=utf8;
- '''
-)
diff --git a/day9/task5/index.html b/day9/task5/index.html
deleted file mode 100644
index eca610b..0000000
--- a/day9/task5/index.html
+++ /dev/null
@@ -1,47 +0,0 @@
-<html lang="ru">
-
-<head>
- <meta charset="UTF-8">
- <title>"table_task1" table view</title>
-
- <link rel="stylesheet" href="/static/css/ContentBoxStyle.css">
- <link rel="stylesheet" href="/static/css/FormStyle.css">
- <link rel="stylesheet" href="/static/css/NewEntryStyle.css">
- <link rel="stylesheet" href="/static/css/TableStyle.css">
-</head>
-
-<body onload="onPageLoad()">
- <div id="backgroundTint" class="backgroundTint hidden">
- <div id="contentBox" class="contentBox hidden"></div>
- </div>
-
- <table id="table"></table>
-
- <form action="/api/upload" method="post" enctype="multipart/form-data">
- <input type="file" name="csvFile" id="csvFileInput"
- accept="text/csv" style="display: none;"
- onchange="form.submit()">
- </form>
-
- <button class="fixedButton uploadFileBtn" id="uploadFileBtn"
- onmouseenter="uploadFileOnHover()"
- onmouseleave="uploadFileOnEndHover()"
- onclick="triggerUploadFile()">
- </button>
-
- <button class="fixedButton newEntryBtn" id="newEntryBtn"
- onclick="showCreateForm()"
- onmouseenter="addNewRowOnHover()"
- onmouseleave="addNewRowOnEndHover()">
- +
- </button>
-
- <script src="/static/js/main.js"></script>
- <script src="/static/js/editForm.js"></script>
- <script src="/static/js/utils.js"></script>
- <script src="/static/js/overlayButtonsCallbacks.js"></script>
- <script src="/static/js/tableCallbacks.js"></script>
- <script src="/static/js/config.js"></script>
-</body>
-
-</html> \ No newline at end of file
diff --git a/day9/task5/main.py b/day9/task5/main.py
deleted file mode 100644
index 872a663..0000000
--- a/day9/task5/main.py
+++ /dev/null
@@ -1,192 +0,0 @@
-from day9.task5_vue.router import route
-from day9.task5_vue.utils import render_template, NOT_FOUND_CODE
-from day9.task5_vue.config import SERVER_HOST, SERVER_PORT, STATIC_FILES_PATH
-
-from day9.task5_vue.core import ErrorResponse, TextFileResponse, ImageResponse, HtmlResponse, JsonResponse
-
-import logging
-import csv
-from sys import stdout
-
-
-@route('/static/(.*?)/(.*?)/?')
-def return_static(query, *args):
- if args[0] in ['css', 'js']:
- try:
- return TextFileResponse(
- path=STATIC_FILES_PATH + f'/{args[0]}/{args[1]}',
- extension={'js': 'javascript', 'css': 'css'}.get(args[0])
- )
-
- except (PermissionError, FileNotFoundError):
- return ErrorResponse(404)
-
- elif args[0] == 'images':
- try:
- return ImageResponse(STATIC_FILES_PATH + f'/{args[0]}/{args[1]}', args[1].split('.')[-1])
-
- except (PermissionError, FileNotFoundError):
- return ErrorResponse(404)
-
-
-@route('/api/upload', ['POST'])
-def upload_file(query, *args):
- base_html = '<a href="/">Return to main page</a><br>%s'
-
- try:
- data = query['files'][0]['data'].decode()
- except (UnicodeDecodeError, KeyError, IndexError):
- return ErrorResponse(500, base_html % '<h1>Error while reading file</h1>')
-
- permitted_headers = db_column_names()
-
- try:
- data = list(csv.reader(data.strip().splitlines(), delimiter=';', quotechar='"'))
- if len(data[0]) != len(permitted_headers) or set(data[0]) != set(permitted_headers):
- return ErrorResponse(500, base_html % '<h1>File format error</h1>')
- except IndexError:
- return ErrorResponse(500, base_html % '<h1>File format error</h1>')
-
- cursor = db.cursor()
- cursor.execute("START TRANSACTION; DELETE FROM `table_task1`; COMMIT;")
- cursor.close()
-
- for row in data[1:]:
- values = []
- for i in range(len(row)):
- if row[i] == 'NULL':
- if data[0][i] == 'creation_request_sent_date':
- value = 'NULL'
- else:
- value = '"NULL"'
- else:
- value = f'"{row[i]}"'
-
- values.append(value)
-
- cursor = db.cursor()
- cursor.execute(
- "START TRANSACTION; "
- "INSERT INTO `table_task1` ({}) VALUES ({}); "
- "COMMIT;".format(
- ','.join(data[0]), ','.join(values)
- )
- )
- cursor.close()
-
- return HtmlResponse(base_html % '<h1>File uploaded</h1>')
-
-
-@route('/api/update', ['POST'])
-def update_post(query, *args):
- print(query)
- service_id = query['service_id']
- permitted_fields = db_column_names()
- permitted_fields.remove('service_id')
-
- query_set = []
- for field_name, value in query.items():
- if field_name in permitted_fields:
- query_set.append(f'{field_name}="{value}"')
-
- cursor = db.cursor()
- cursor.execute("START TRANSACTION; UPDATE `table_task1` SET {} WHERE {}; COMMIT;".format(
- ','.join(query_set), f'service_id="{service_id}"'
- ))
- cursor.close()
-
- return HtmlResponse('<a href="/">Return to main page</a><br><h1>Database Updated</h1>')
-
-
-@route('/api/delete', ['POST'])
-def delete_post(query, *args):
- service_id = query['service_id']
- cursor = db.cursor()
- cursor.execute(f'START TRANSACTION; DELETE FROM `table_task1` WHERE service_id="{service_id}"; COMMIT;')
-
- return HtmlResponse('<a href="/">Return to main page</a><br><h1>Database Updated</h1>')
-
-
-@route('/api/add', ['POST'])
-def add_post(query, *args):
- header_fields = db_column_names()
- headers = []
- values = []
- for field_name in query:
- if field_name in header_fields:
- headers.append(field_name)
- values.append(f'"{query[field_name]}"')
-
- if len(headers) == len(header_fields):
- cursor = db.cursor()
- cursor.execute("START TRANSACTION; INSERT INTO `table_task1` ({}) VALUES ({}); COMMIT;".format(
- ','.join(headers), ','.join(values)
- ))
- cursor.close()
-
- return HtmlResponse('<a href="/">Return to main page</a><br><h1>Database Updated</h1>')
-
-
-@route('/get', ['POST'])
-def db_get(query, *args):
- table_headers = db_column_names()
- cursor = db.cursor()
-
- if query['type'] == 'full':
- cursor.execute('SELECT * FROM table_task1;')
- content = cursor.fetchall()
-
- cursor.close()
-
- json_content = []
- for row in content:
- new_row = []
- for col in row:
- if not isinstance(col, (float, bool, int, dict, list, tuple)):
- col = str(col)
- new_row.append(col)
- json_content.append(new_row)
-
- return JsonResponse({'headers': table_headers, 'content': json_content})
-
- elif query['type'] == 'single_id':
- cursor.execute(f'SELECT * FROM table_task1 WHERE service_id="{query["service_id"]}";')
- content = cursor.fetchone()
-
- cursor.close()
-
- json_content = []
- for col in content:
- if not isinstance(col, (float, bool, int, dict, list, tuple)):
- col = str(col)
- json_content.append(col)
-
- return JsonResponse(dict(zip(table_headers, json_content)))
-
- return ErrorResponse(NOT_FOUND_CODE)
-
-
-@route('/')
-def index_get(query, *args):
- data = render_template('index.html')
- return HtmlResponse(data)
-
-
-def prepare_logger():
- logger = logging.getLogger('tableApp')
- logger.setLevel(logging.INFO)
-
- sh = logging.StreamHandler(stdout)
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
- sh.setFormatter(formatter)
- logger.addHandler(sh)
-
-
-if __name__ == '__main__':
- prepare_logger()
-
- from day9.task5_vue.server import start_server
- from day9.task5_vue.database import db, db_column_names
-
- logging.getLogger('tableApp').info(f'Starting server...')
- start_server(SERVER_HOST, SERVER_PORT)
diff --git a/day9/task5/router.py b/day9/task5/router.py
deleted file mode 100644
index 0ca5f66..0000000
--- a/day9/task5/router.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import re
-from day9.task5_vue.utils import NOT_FOUND_CODE, BAD_REQUEST_CODE
-from day9.task5_vue.core import Response, ErrorResponse
-
-
-def route(url_format, methods=None):
- if methods is None:
- methods = ['GET']
-
- def wrapper(func):
- def inner(url, query, *args, **kwargs):
- pattern = re.compile(url_format)
- match = re.fullmatch(pattern, url)
-
- if match is None or len(match.groups()) != pattern.groups:
- return ErrorResponse(BAD_REQUEST_CODE)
-
- return func(query, *match.groups(), *args, **kwargs)
-
- _router_tree[url_format] = _router_tree.get(url_format, {})
- for method in methods:
- _router_tree[url_format][method] = inner
-
- return inner
-
- return wrapper
-
-
-def run(request) -> Response:
- res = ErrorResponse(NOT_FOUND_CODE)
-
- method, url = request['method'], request['url']
- for url_pattern in _router_tree:
- if re.fullmatch(url_pattern, url) and method in _router_tree[url_pattern]:
- res = _router_tree[url_pattern][method](url, request['query'])
- break
-
- return res
-
-
-_router_tree = {}
diff --git a/day9/task5/static/css/ContentBoxStyle.css b/day9/task5/static/css/ContentBoxStyle.css
deleted file mode 100644
index abe9ee1..0000000
--- a/day9/task5/static/css/ContentBoxStyle.css
+++ /dev/null
@@ -1,26 +0,0 @@
-.contentBox {
- padding: 20px;
- width: 500px;
- height: 695px;
- background-color: #f0f0f0;
- border-radius: 10px;
- box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.3);
-}
-
-.backgroundTint {
- width: 100vw;
- height: 100vh;
- background-color: rgba(0, 0, 0, 0.6);
-}
-
-.shown {
- display: inline;
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
-}
-
-.hidden {
- display: none;
-} \ No newline at end of file
diff --git a/day9/task5/static/css/FormStyle.css b/day9/task5/static/css/FormStyle.css
deleted file mode 100644
index 63e51dd..0000000
--- a/day9/task5/static/css/FormStyle.css
+++ /dev/null
@@ -1,61 +0,0 @@
-.formRow {
- width: 100%;
- display: flex;
- flex-direction: column;
-}
-
-.formRow-label {
- font-size: 20px;
- color: #404040;
-}
-
-.formRow-input > input, .formRow-input > select {
- margin: 3px;
- width: 100%;
- border-radius: 5px;
- border: none;
- box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2);
- padding: 5px;
- font-size: 1.4em;
-
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-}
-
-.formRow-input > input[readonly="readonly"] {
- background-color: #bebebe;
- color: #4b4b4b;
-}
-
-.formInput[type="date"], .formInput[type="time"] {
- padding: 5px;
- font-size: 1.6em;
-}
-
-.formRow-input > select:hover, .formRow-input > input:not([readonly="readonly"]):hover {
- background-color: #fbfbfb;
-}
-
-.formButtons {
- display: flex;
- flex-direction: row;
- margin-top: 5px;
-}
-
-.formButtons > button {
- margin: 5px;
- width: 50%;
- padding: 5px;
- border: none;
- border-radius: 7px;
- box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.4);
-}
-
-.formButtons > button:hover {
- background-color: #dadada;
-}
-
-.formButtons > button:focus {
- background-color: #d5d5d5;
-} \ No newline at end of file
diff --git a/day9/task5/static/css/NewEntryStyle.css b/day9/task5/static/css/NewEntryStyle.css
deleted file mode 100644
index be2a4f0..0000000
--- a/day9/task5/static/css/NewEntryStyle.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.fixedButton {
- overflow: hidden;
- white-space: nowrap;
-
- position: fixed;
-
- padding: 5px;
- border-radius: 50px;
- width: 100px;
- height: 100px;
- border: none;
- background-color: #2871e2;
- color: white;
- font-size: 45px;
-
- transition-property: width;
- transition-duration: 0.6s;
-}
-
-.fixedButton:hover {
- width: 300px;
- font-size: 30px;
- background-color: #286bd6;
-}
-
-.fixedButton:focus {
- background-color: #2861c3;
-}
-
-.newEntryBtn {
- bottom: 50px;
- left: 50px;
-}
-
-.uploadFileBtn {
- bottom: 175px;
- left: 50px;
-}
diff --git a/day9/task5/static/css/TableStyle.css b/day9/task5/static/css/TableStyle.css
deleted file mode 100644
index 0631fbe..0000000
--- a/day9/task5/static/css/TableStyle.css
+++ /dev/null
@@ -1,22 +0,0 @@
-table {
- border-spacing: 0;
- padding: 0;
- border: 1px solid black;
-}
-
-thead > tr > th {
- padding: 6px;
- font-size: 24px;
- background-color: #d5d5d5;
- border: 1px black solid;
-}
-
-td {
- padding: 8px;
- font-size: 21px;
- border: 1px black solid;
-}
-
-.editableField:hover {
- background-color: rgba(0, 0, 0, 0.1);
-}
diff --git a/day9/task5/static/images/upload_icon.png b/day9/task5/static/images/upload_icon.png
deleted file mode 100644
index 7d27fb5..0000000
--- a/day9/task5/static/images/upload_icon.png
+++ /dev/null
Binary files differ
diff --git a/day9/task5/static/js/config.js b/day9/task5/static/js/config.js
deleted file mode 100644
index 080060e..0000000
--- a/day9/task5/static/js/config.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// HOST = 'http://localhost:8000';
-HOST = '';
diff --git a/day9/task5/static/js/editForm.js b/day9/task5/static/js/editForm.js
deleted file mode 100644
index f371664..0000000
--- a/day9/task5/static/js/editForm.js
+++ /dev/null
@@ -1,142 +0,0 @@
-const baseFormFields = `<form id="tableForm" action="/api/update" method="post">
- <div class="formRow">
- <div class="formRow-label">service_id</div>
- <div class="formRow-input"><input type="text" name="service_id" class="formInput" id="formServiceId" readonly="readonly"></div>
- </div>
-
- <div class="formRow">
- <div class="formRow-label"> servtype </div>
- <div class="formRow-input">
- <input type="text" name="servtype" class="formInput">
- </div>
- </div>
-
- <div class="formRow">
- <div class="formRow-label"> subtype </div>
- <div class="formRow-input">
- <input type="text" name="subtype" class="formInput">
- </div>
- </div>
-
- <div class="formRow">
- <div class="formRow-label"> user_id </div>
- <div class="formRow-input">
- <input type="text" name="user_id" class="formInput">
- </div>
- </div>
-
- <div class="formRow">
- <div class="formRow-label"> referrer_user_id </div>
- <div class="formRow-input">
- <input type="text" name="referrer_user_id" class="formInput">
- </div>
- </div>
-
- <div class="formRow">
- <div class="formRow-label"> state </div>
- <div class="formRow-input">
- <select name="state">
- <option>N</option>
- <option>A</option>
- <option>S</option>
- <option>D</option>
- <option>O</option>
- </select>
- </div>
- </div>
-
- <div class="formRow">
- <div class="formRow-label"> creation_date </div>
- <div class="formRow-input">
- <input type="date" name="creation_date" class="formInput">
- </div>
- </div>
- <div class="formRow">
- <div class="formRow-label"> creation_time </div>
- <div class="formRow-input">
- <input type="time" name="creation_time" class="formInput">
- </div>
- </div>
-
- <div class="formRow">
- <div class="formRow-label"> creation_request_sent_date </div>
- <div class="formRow-input" style="display: flex; flex-direction: row">
- <input type="date" name="creation_request_sent_date" class="formInput" style="width: 50%;">
- <input type="time" name="creation_request_sent_time" class="formInput" style="width: 50%;">
- </div>
- </div>
-
- <div class="formRow">
- <div class="formRow-label"> notified_about_expiration </div>
- <div class="formRow-input">
- <input type="text" name="notified_about_expiration" class="formInput">
- </div>
- </div>
-</form>`;
-
-const formCreateButtons = `<div class="formButtons">
- <button type="button" onclick="hideForm()">Cancel</button>
- <button type="submit">Create</button>
-</div>`;
-
-const formEditButtons = `<div class="formButtons">
- <button type="button" onclick="hideForm()">Cancel</button>
- <button type="submit">Update</button>
-</div>`;
-
-function showContentBox() {
- let elem = document.getElementById('backgroundTint');
- elem.classList.remove('hidden');
- elem.classList.add('shown');
-
- elem = document.getElementById('contentBox');
- elem.classList.remove('hidden');
- elem.classList.add('shown');
-}
-
-function hideContentBox() {
- let elem = document.getElementById('backgroundTint');
- elem.classList.remove('shown');
- elem.classList.add('hidden');
-
- elem = document.getElementById('contentBox');
- elem.classList.remove('shown');
- elem.classList.add('hidden');
-}
-
-function showForm() {
- let newEntryBtn = document.getElementById('newEntryBtn');
- newEntryBtn.style.display = 'none';
-
- showContentBox();
- let contentBox = document.getElementById('contentBox');
- while (contentBox.firstChild)
- contentBox.removeChild(contentBox.firstChild);
-
- contentBox.appendChild(elementFromHTML(baseFormFields));
- return contentBox;
-}
-
-function hideForm() {
- let newEntryBtn = document.getElementById('newEntryBtn');
- newEntryBtn.style.display = 'inline';
-
- let contentBox = document.getElementById('contentBox');
- while (contentBox.firstChild)
- contentBox.removeChild(contentBox.firstChild);
-
- hideContentBox();
-}
-
-function showEditForm() {
- let contentBox = showForm();
- contentBox.firstChild.appendChild(elementFromHTML(formEditButtons));
- contentBox.firstChild.action = '/api/update';
-}
-
-function showCreateForm() {
- let contentBox = showForm();
- contentBox.firstChild.appendChild(elementFromHTML(formCreateButtons));
- contentBox.firstChild.action = '/api/add';
- document.getElementById('formServiceId').disabled = false;
-}
diff --git a/day9/task5/static/js/main.js b/day9/task5/static/js/main.js
deleted file mode 100644
index 2a45341..0000000
--- a/day9/task5/static/js/main.js
+++ /dev/null
@@ -1,70 +0,0 @@
-function onPageLoad() {
- request.post(`${HOST}/get`, renderTable, {'type': 'full'});
-
- let btn = document.getElementById('uploadFileBtn');
- btn.innerHTML = uploadFileHtml;
-}
-
-function renderTable(text) {
- let data = JSON.parse(text);
-
- let table = document.getElementById('table');
-
- while (table.firstChild) table.removeChild(table.firstChild);
-
- let tableHeaders = document.createElement('thead');
- let headerRow = document.createElement('tr');
- tableHeaders.appendChild(headerRow);
- data['headers'].forEach((field) => {
- let header = document.createElement('th');
- header.innerText = field;
- headerRow.appendChild(header);
- });
- headerRow.appendChild(document.createElement('th'));
- headerRow.appendChild(document.createElement('th'));
-
- let tableContent = document.createElement('tbody');
- data['content'].forEach((row) => {
- row.push(`<button value="${row[0]}" onclick="setupEditFormFields('${row[0]}')">&#9998</button>`);
- row.push(`<button value="${row[0]}" onclick="removeField('${row[0]}')">&#10006</button>`);
-
- let contentRow = document.createElement('tr');
- contentRow.setAttribute('id', `row_${row[0]}`);
-
- row.forEach((column, colIndex) => {
- let columnNode = document.createElement('td');
-
- if (colIndex !== 0 && colIndex < data['headers'].length) {
- let fieldId = `${data['headers'][colIndex]}-${row[0]}`;
- columnNode.classList.add('editableField');
- columnNode.onclick = () => { onFieldClick(fieldId) };
- columnNode.setAttribute('id', fieldId);
- }
-
- columnNode.innerHTML = column;
- contentRow.appendChild(columnNode);
- });
-
- tableContent.appendChild(contentRow);
- });
-
- table.appendChild(tableHeaders);
- table.appendChild(tableContent);
-}
-
-function setupEditFormFields(service_id) {
- showEditForm();
- request.post(`${HOST}/get`, (text) => {
- let row = JSON.parse(text);
-
- Object.keys(row).forEach((element) => {
- document.getElementsByName(element)[0].value = row[element];
- });
-
- let [sent_date, sent_time] = row['creation_request_sent_date'].split(' ');
- document.getElementsByName('creation_request_sent_date')[0].value = sent_date;
- document.getElementsByName('creation_request_sent_time')[0].value = sent_time;
- }, {
- 'type': 'single_id', 'service_id': service_id
- });
-}
diff --git a/day9/task5/static/js/overlayButtonsCallbacks.js b/day9/task5/static/js/overlayButtonsCallbacks.js
deleted file mode 100644
index 866a302..0000000
--- a/day9/task5/static/js/overlayButtonsCallbacks.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const uploadFileHtml = '<img src="/static/images/upload_icon.png" alt="" style="width: 70%;height: 70%;">';
-
-function addNewRowOnHover() {
- let btn = document.getElementById('newEntryBtn');
- btn.innerText = 'Add new row';
-}
-
-function addNewRowOnEndHover() {
- let btn = document.getElementById('newEntryBtn');
- btn.innerText = '+';
-}
-
-function uploadFileOnHover() {
- let btn = document.getElementById('uploadFileBtn');
- btn.innerHTML = 'Upload File';
-}
-
-function uploadFileOnEndHover() {
- let btn = document.getElementById('uploadFileBtn');
- btn.innerHTML = uploadFileHtml;
-}
-
-function triggerUploadFile() {
- let fileInput = document.getElementById('csvFileInput');
- fileInput.click()
-}
diff --git a/day9/task5/static/js/tableCallbacks.js b/day9/task5/static/js/tableCallbacks.js
deleted file mode 100644
index d024d16..0000000
--- a/day9/task5/static/js/tableCallbacks.js
+++ /dev/null
@@ -1,116 +0,0 @@
-const defaultColumnInputs = {
- 'service_id': `<input type="text">`,
- 'servtype': `<input type="text">`,
- 'subtype': `<input type="text">`,
- 'user_id': `<input type="text">`,
- 'referrer_user_id': `<input type="text">`,
- 'state': `<select>
- <option>N</option>
- <option>A</option>
- <option>S</option>
- <option>D</option>
- <option>O</option>
- </select>`,
-
- 'creation_date': `<input type="date">`,
- 'creation_time': `<input type="time">`,
- 'creation_request_sent_date': `<input type="date"><input type="time">`,
- 'notified_about_expiration': `<input type="text">`
-};
-
-function onFieldClick(fieldId) {
- let fieldElement = document.getElementById(fieldId);
- fieldElement.onclick = () => {};
-
- if (fieldElement.firstChild.nodeName !== 'INPUT' && fieldElement.firstChild.nodeName !== 'SELECT') {
- let [columnName, serviceId] = fieldId.split('-');
- let previousValue = fieldElement.innerText;
-
- fieldElement.innerHTML = defaultColumnInputs[columnName];
- let submitBtn = elementFromHTML('<button type="button">Submit</button>');
- let cancelBtn = elementFromHTML('<button type="button">&#10006</button>');
-
- fieldElement.appendChild(submitBtn);
- fieldElement.appendChild(cancelBtn);
-
- if (columnName === 'creation_request_sent_date') {
- let dateElement = fieldElement.firstChild;
- let timeElement = fieldElement.childNodes[1];
-
- submitBtn.onclick = () => fieldEditSubmitBtn(fieldId, `${dateElement.value} ${timeElement.value}`);
- cancelBtn.onclick = () => fieldEditCancelBtn(fieldId, previousValue);
-
- dateElement.onkeyup = timeElement.onkeyup = (event) => {
- if (event.code === 'Enter') {
- if (dateElement.value !== '' && timeElement.value !== '')
- fieldEditSubmit(fieldId, `${dateElement.value} ${timeElement.value}`);
- }
- else if (event.code === 'Escape')
- fieldEditCancel(fieldId, previousValue);
- };
- }
- else {
- let inputElement = fieldElement.firstChild;
-
- submitBtn.onclick = () => fieldEditSubmitBtn(fieldId, inputElement.value);
- cancelBtn.onclick = () => fieldEditCancelBtn(fieldId, previousValue);
-
- inputElement.onkeyup = (event) => {
- if (event.code === 'Enter')
- fieldEditSubmit(fieldId, inputElement.value);
- else if (event.code === 'Escape')
- fieldEditCancel(fieldId, previousValue);
- };
- }
- }
-}
-
-function _fieldEditSubmit(fieldId, value) {
- let [columnName, serviceId] = fieldId.split('-');
- request.post(`${HOST}/api/update`, () => {}, {
- 'service_id': serviceId, [columnName]: value
- });
-}
-
-function fieldEditSubmit(fieldId, value) {
- _fieldEditSubmit(fieldId, value);
-
- let fieldElement = document.getElementById(fieldId);
- fieldElement.innerHTML = value;
- fieldElement.onclick = () => onFieldClick(fieldId);
-}
-
-function fieldEditSubmitBtn(fieldId, value) {
- _fieldEditSubmit(fieldId, value);
-
- let fieldElement = document.getElementById(fieldId);
- fieldElement.innerHTML = value;
- fieldElement.onclick = () => {
- fieldElement.onclick = () => onFieldClick(fieldId);
- }
-}
-
-function fieldEditCancel(fieldId, value) {
- let fieldElement = document.getElementById(fieldId);
- fieldElement.innerHTML = value;
- fieldElement.onclick = () => onFieldClick(fieldId);
-}
-
-function fieldEditCancelBtn(fieldId, value) {
- let fieldElement = document.getElementById(fieldId);
- fieldElement.innerHTML = value;
- fieldElement.onclick = () => {
- fieldElement.onclick = () => onFieldClick(fieldId);
- }
-}
-
-function removeField(service_id) {
- request.post(`${HOST}/api/delete`, () => {}, {'service_id': service_id});
- let tableBody = document.getElementById('table').childNodes[1];
- for (let node of tableBody.childNodes) {
- if (node.getAttribute('id') === `row_${service_id}`) {
- tableBody.removeChild(node);
- break
- }
- }
-}
diff --git a/day9/task5/static/js/utils.js b/day9/task5/static/js/utils.js
deleted file mode 100644
index d76c547..0000000
--- a/day9/task5/static/js/utils.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const request = {
- get: function (url, callback) {
- let xmlHttp = new XMLHttpRequest();
- xmlHttp.onreadystatechange = function() {
- if (xmlHttp.readyState === 4 && xmlHttp.status === 200)
- callback(xmlHttp.responseText);
- };
- xmlHttp.open("GET", url, true);
- xmlHttp.send();
- },
-
- post: function (url, callback, data) {
- let xmlHttp = new XMLHttpRequest();
- xmlHttp.onreadystatechange = function() {
- if (xmlHttp.readyState === 4 && xmlHttp.status === 200)
- callback(xmlHttp.responseText);
- };
- xmlHttp.open("POST", url, true);
- xmlHttp.send(this.formatParams(data));
- },
-
- formatParams: function (params) {
- return Object.keys(params).map((key) => {
- return key+"="+encodeURIComponent(params[key])
- }).join("&")
- }
- };
-
-function elementFromHTML(html) {
- let elem = document.createElement('div');
- elem.innerHTML = html;
- return elem.firstChild;
-}
diff --git a/day9/task5_vue/backend/core.py b/day9/task5_vue/backend/core/response_types.py
index 71c6d5a..ca3a9f2 100644
--- a/day9/task5_vue/backend/core.py
+++ b/day9/task5_vue/backend/core/response_types.py
@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
import json
-from backend.utils import HTTP_STATUS_CODES
+from backend.core.utils import HTTP_STATUS_CODES
class Response(ABC):
diff --git a/day9/task5_vue/backend/core/router.py b/day9/task5_vue/backend/core/router.py
new file mode 100644
index 0000000..135b241
--- /dev/null
+++ b/day9/task5_vue/backend/core/router.py
@@ -0,0 +1,49 @@
+import re
+from backend.core.response_types import *
+from backend.core.utils import NOT_FOUND_CODE
+
+
+class ResolutionError(Exception):
+ pass
+
+
+class Route:
+ """
+ Представляет из себя класс роута. При инициализации принимает допустимые
+ методы и регулярное выражение. Содержит методы для работы роутинга.
+ Чтобы созданный путь стал обрабатываться необходимо поместить объект Route в таблицу роутера.
+ """
+
+ def __init__(self, callback, url_format, methods=None):
+ self.methods = ['GET'] if methods is None else methods
+ self.url_pattern = re.compile(url_format)
+ self.callback = callback
+
+ def resolve(self, url, method, query):
+ match = self.url_pattern.fullmatch(url)
+ if match is not None:
+ groups = match.groups()
+
+ matched = len(groups) == self.url_pattern.groups and method in self.methods
+ if not matched:
+ raise ResolutionError()
+ else:
+ raise ResolutionError()
+
+ return self.callback(query, *match.groups())
+
+
+def run(request) -> Response:
+ response = ErrorResponse(NOT_FOUND_CODE)
+
+ method, url = request['method'], request['url']
+ for route in router_table:
+ try:
+ response = route.resolve(url, method, request['query'])
+ except ResolutionError:
+ continue
+
+ return response
+
+
+router_table = []
diff --git a/day9/task5_vue/backend/server.py b/day9/task5_vue/backend/core/server.py
index 48d5946..7c284c1 100644
--- a/day9/task5_vue/backend/server.py
+++ b/day9/task5_vue/backend/core/server.py
@@ -1,8 +1,8 @@
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs
-from backend.router import run
-from backend.utils import parse_multipart_form
+from backend.core.router import run
+from backend.core.utils import parse_multipart_form
import logging
import json
@@ -34,32 +34,24 @@ class MyHTTPRequestHandler(BaseHTTPRequestHandler):
content_length = int(self.headers['Content-Length'])
content_type = self.headers['Content-type']
post_data = self.rfile.read(content_length)
+ query = {}
if content_type.split(';')[0] == 'multipart/form-data':
- files = parse_multipart_form(post_data)
- self.finalize_request(run({
- 'url': self.path,
- 'method': 'POST',
- 'query': {'files': files}
- }))
+ query = {'files': parse_multipart_form(post_data)}
elif content_type.split(';')[0] in ['text/plain', 'application/x-www-form-urlencoded']:
- post_data = parse_qs(post_data.decode('utf-8'))
- for key in post_data:
- post_data[key] = post_data[key][0]
-
- self.finalize_request(run({
- 'url': self.path,
- 'method': 'POST',
- 'query': post_data
- }))
+ query = parse_qs(post_data.decode('utf-8'))
+ for key in query:
+ query[key] = query[key][0]
elif content_type.split(';')[0] == 'application/json':
- self.finalize_request(run({
- 'url': self.path,
- 'method': 'POST',
- 'query': json.loads(post_data.decode('utf-8'))
- }))
+ query = json.loads(post_data.decode('utf-8'))
+
+ self.finalize_request(run({
+ 'url': self.path,
+ 'method': 'POST',
+ 'query': query
+ }))
def finalize_request(self, response):
self._set_response(response.status_code, response.content_type)
diff --git a/day9/task5_vue/backend/utils.py b/day9/task5_vue/backend/core/utils.py
index c1b3a04..c1b3a04 100644
--- a/day9/task5_vue/backend/utils.py
+++ b/day9/task5_vue/backend/core/utils.py
diff --git a/day9/task5_vue/backend/database.py b/day9/task5_vue/backend/database.py
deleted file mode 100644
index 5484041..0000000
--- a/day9/task5_vue/backend/database.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import MySQLdb
-from backend.config import *
-
-import logging
-
-
-def db_column_names():
- cursor = db.cursor()
- cursor.execute('DESCRIBE table_task1;')
- table_structure = cursor.fetchall()
- table_headers = [field[0] for field in table_structure]
- return table_headers
-
-
-logger = logging.getLogger('tableApp')
-logger.info(f'Trying to connect to database "{DATABASE_NAME}@{HOST}"...')
-# В файле config.py создайте соответствующие переменные
-db = MySQLdb.connect(
- host=HOST,
- user=USERNAME,
- passwd=PASSWORD,
- db=DATABASE_NAME
-)
-logger.info('Connected')
-
-logger.info(f'Preparing table "table_task1"...')
-db.cursor().execute(
- '''
- CREATE TABLE IF NOT EXISTS `table_task1` (
- `service_id` int(11) NOT NULL AUTO_INCREMENT,
- `servtype` varchar(20) NOT NULL DEFAULT 'hosting',
- `subtype` varchar(32) NOT NULL DEFAULT '',
- `user_id` bigint(20) NOT NULL,
- `referrer_user_id` bigint(20) NOT NULL,
- `state` varchar(1) NOT NULL DEFAULT 'N',
- `creation_date` date NOT NULL DEFAULT '0000-01-01',
- `creation_time` time NOT NULL DEFAULT '00:00:00',
- `creation_request_sent_date` datetime DEFAULT NULL,
- `notified_about_expiration` smallint(6) NOT NULL DEFAULT '0',
- PRIMARY KEY (`service_id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=35109400 DEFAULT CHARSET=utf8;
- '''
-)
diff --git a/day9/task5_vue/backend/database/database.py b/day9/task5_vue/backend/database/database.py
new file mode 100644
index 0000000..87982cf
--- /dev/null
+++ b/day9/task5_vue/backend/database/database.py
@@ -0,0 +1,29 @@
+from backend.database.wrappers import Wrapper, MySQLWrapper
+
+import logging
+
+
+def initialize_databases(configs, schemes):
+ for config in configs:
+ if config['type'] == 'mysql':
+ logger = logging.getLogger('backendDebug')
+
+ logger.debug('Connected')
+ logger.debug(f'Trying to connect to database "{config["db_name"]}@{config["host"]}"...')
+
+ wrapper = MySQLWrapper(config['host'], config['username'], config['password'], config['db_name'])
+ wrappers[config['name']] = wrapper
+
+ cursor = wrapper.connection.cursor()
+ for scheme in schemes:
+ logger.debug(f'Preparing table "{scheme.meta["name"]}"...')
+ cursor.execute('START TRANSACTION; {} COMMIT;'.format(scheme.get_create_line()))
+ wrapper.schemes[scheme.meta['name']] = scheme
+ cursor.close()
+
+
+def get_wrapper_for(database_name) -> Wrapper:
+ return wrappers.get(database_name, None)
+
+
+wrappers = {}
diff --git a/day9/task5_vue/backend/database/field_types.py b/day9/task5_vue/backend/database/field_types.py
new file mode 100644
index 0000000..d0b2c3b
--- /dev/null
+++ b/day9/task5_vue/backend/database/field_types.py
@@ -0,0 +1,128 @@
+from abc import ABC, abstractmethod
+
+
+class Field(ABC):
+ def __init__(self, validators=None):
+ self.validators = []
+ if validators is not None:
+ self.validators.extend(validators)
+
+ def _validate_attributes(self):
+ pass
+
+ @property
+ @abstractmethod
+ def sql_line(self):
+ pass
+
+ def validate(self, value):
+ for validator in self.validators:
+ validator.validate(value, self)
+
+
+class TextField(Field):
+ def __init__(self, max_length, is_variable_length, nullable, default, validators=None):
+ super().__init__(validators)
+
+ self.max_length = max_length
+ self.is_variable_length = is_variable_length
+ self.nullable = nullable
+ self.default = default
+
+ self.data_type = str
+
+ @property
+ def sql_line(self):
+ request = [
+ ('varchar' if self.is_variable_length else 'char') + f'({self.max_length})',
+ 'NULL' if self.nullable else 'NOT NULL'
+ ]
+
+ if self.default is not None or self.nullable:
+ request.append(f'DEFAULT "{self.default}"')
+
+ return ' '.join(request)
+
+
+class IntegerField(Field):
+ def __init__(self, max_length, nullable, is_auto_increment, default, validators=None):
+ super().__init__(validators)
+
+ self.max_length = max_length
+ self.nullable = nullable
+ self.is_auto_increment = is_auto_increment
+ self.default = default
+
+ self.data_type = int
+
+ @property
+ def sql_line(self):
+ request = [
+ 'int' + (f'({self.max_length})' if self.max_length is not None else ''),
+ 'NULL' if self.nullable else 'NOT NULL'
+ ]
+
+ if self.default is not None:
+ request.append(f'DEFAULT "{self.default}"')
+
+ if self.is_auto_increment:
+ request.append('AUTO_INCREMENT')
+
+ return ' '.join(request)
+
+
+class DateField(Field):
+ def __init__(self, nullable, default, validators=None):
+ super().__init__(validators)
+
+ self.nullable = nullable
+ self.default = default
+
+ self.data_type = str
+
+ @property
+ def sql_line(self):
+ request = ['date', 'NULL' if self.nullable else 'NOT NULL']
+
+ if self.default is not None:
+ request.append(f'DEFAULT "{self.default}"')
+
+ return ' '.join(request)
+
+
+class TimeField(Field):
+ def __init__(self, nullable, default, validators=None):
+ super().__init__(validators)
+
+ self.nullable = nullable
+ self.default = default
+
+ self.data_type = str
+
+ @property
+ def sql_line(self):
+ request = ['time', 'NULL' if self.nullable else 'NOT NULL']
+
+ if self.default is not None:
+ request.append(f'DEFAULT "{self.default}"')
+
+ return ' '.join(request)
+
+
+class DatetimeField(Field):
+ def __init__(self, nullable, default, validators=None):
+ super().__init__(validators)
+
+ self.nullable = nullable
+ self.default = default
+
+ self.data_type = str
+
+ @property
+ def sql_line(self):
+ request = ['datetime', 'NULL' if self.nullable else 'NOT NULL']
+
+ if self.default is not None:
+ request.append(f'DEFAULT "{self.default}"')
+
+ return ' '.join(request)
diff --git a/day9/task5_vue/backend/database/scheme.py b/day9/task5_vue/backend/database/scheme.py
new file mode 100644
index 0000000..f783f79
--- /dev/null
+++ b/day9/task5_vue/backend/database/scheme.py
@@ -0,0 +1,46 @@
+from backend.database.validators import ValidationError
+
+
+class DatabaseScheme:
+ meta = {}
+ fields = {}
+
+ @classmethod
+ def get_create_line(cls) -> str:
+ fields = cls.fields
+ meta = cls.meta
+
+ lines = [f'`{name}` {field.sql_line}' for name, field in fields.items()]
+
+ if 'primary_key' in meta:
+ primary_key = ', PRIMARY KEY (`{}`)'.format(meta['primary_key'])
+ else:
+ primary_key = ''
+
+ return 'CREATE TABLE IF NOT EXISTS `{name}` (\n{fields}{primary_key}\n);'.format(
+ name=meta['name'], fields=',\n'.join(lines), primary_key=primary_key
+ )
+
+ @classmethod
+ def validate(cls, data: dict) -> dict:
+ data_fields = set(data.keys())
+ scheme_fields = set(cls.fields.keys())
+
+ defined_fields = scheme_fields & data_fields
+ not_defined_fields = scheme_fields ^ defined_fields
+
+ validation_results = {'error': False}
+ for field in not_defined_fields:
+ validation_results['error'] = True
+ validation_results[field] = 'Field is not defined'
+
+ for field in defined_fields:
+ try:
+ cls.fields[field].validate(data[field])
+ except ValidationError as e:
+ validation_results['error'] = True
+ validation_results[field] = str(e)
+ else:
+ validation_results[field] = None
+
+ return validation_results
diff --git a/day9/task5_vue/backend/database/validators.py b/day9/task5_vue/backend/database/validators.py
new file mode 100644
index 0000000..e4b7310
--- /dev/null
+++ b/day9/task5_vue/backend/database/validators.py
@@ -0,0 +1,96 @@
+from abc import ABC, abstractmethod
+import re
+
+
+class ValidationError(Exception):
+ pass
+
+
+class Validator(ABC):
+ @staticmethod
+ @abstractmethod
+ def validate(value, field_object):
+ pass
+
+
+class ValidateNull(Validator):
+ @staticmethod
+ def validate(value, field_object):
+ if value is None and not field_object.nullable:
+ raise ValidationError('Value cannot be NULL')
+
+
+class ValidateType(Validator):
+ @staticmethod
+ def validate(value, field_object):
+ if field_object.nullable and value is None:
+ return
+ if not isinstance(value, field_object.data_type):
+ raise ValidationError('Value is of wrong type')
+
+
+class ValidateLength(Validator):
+ @staticmethod
+ def validate(value, field_object):
+ if len(str(value)) > field_object.max_length:
+ raise ValidationError('Value has too many digits')
+
+
+class ValidateTime(Validator):
+ @staticmethod
+ def validate(value, field_object):
+ match = re.fullmatch(r'(\d\d):(\d\d):(\d\d)', value)
+ if not match:
+ raise ValidationError('Wrong time format')
+ else:
+ hour, minute, second = map(int, match.groups())
+ if hour not in range(0, 24):
+ raise ValidationError('Wrong hour value')
+
+ if minute not in range(0, 60):
+ raise ValidationError('Wrong minute value')
+
+ if second not in range(0, 60):
+ raise ValidationError('Wrong second value')
+
+
+class ValidateDate(Validator):
+ @staticmethod
+ def validate(value, field_object):
+ match = re.fullmatch(r'(\d\d\d\d)-(\d\d)-(\d\d)', value)
+ if not match:
+ raise ValidationError('Wrong date format')
+ else:
+ year, month, day = map(int, match.groups())
+ if year < 0:
+ raise ValidationError('Wrong year value')
+
+ if month not in range(1, 12):
+ raise ValidationError('Wrong month value')
+
+ if month == 2:
+ if year % 4 == 0 and year % 100 != 0 or year % 400 == 0:
+ febr_range = range(1, 29)
+ else:
+ febr_range = range(1, 28)
+ if day not in febr_range:
+ raise ValidationError('Wrong day value')
+ else:
+ days_count = {
+ 1: 31, 3: 31, 4: 30, 5: 31,
+ 6: 30, 7: 31, 8: 31, 9: 30,
+ 10: 31, 11: 30, 12: 31
+ }.get(month)
+ if day not in range(1, days_count):
+ raise ValidationError('Wrong day value')
+
+
+class ValidateDatetime(Validator):
+ @staticmethod
+ def validate(value, field_object):
+ datetime = value.split()
+ if len(datetime) != 2:
+ raise ValidationError('Wrong datetime format')
+ else:
+ ValidateDate.validate(datetime[0], {})
+ ValidateTime.validate(datetime[1], {}) \ No newline at end of file
diff --git a/day9/task5_vue/backend/database/wrappers.py b/day9/task5_vue/backend/database/wrappers.py
new file mode 100644
index 0000000..824d19e
--- /dev/null
+++ b/day9/task5_vue/backend/database/wrappers.py
@@ -0,0 +1,149 @@
+from abc import ABC, abstractmethod
+
+import MySQLdb
+
+
+class Wrapper(ABC):
+ def __init__(self):
+ self.schemes = {}
+
+ @abstractmethod
+ def clear_table(self, table_name):
+ pass
+
+ @abstractmethod
+ def get_column_names(self):
+ pass
+
+ @abstractmethod
+ def insert_one(self, table_name, row, field_names=None):
+ pass
+
+ @abstractmethod
+ def update(self, table_name, expressions, conditions):
+ pass
+
+ @abstractmethod
+ def delete_from(self, table_name, conditions):
+ pass
+
+ @abstractmethod
+ def get_data(self, table_name):
+ pass
+
+ @abstractmethod
+ def get_rows(self, table_name, conditions):
+ pass
+
+
+class MySQLWrapper(Wrapper):
+ def __init__(self, host, username, password, db_name):
+ super().__init__()
+ self.connection = MySQLdb.connect(
+ host=host,
+ user=username,
+ passwd=password,
+ db=db_name
+ )
+
+ def clear_table(self, table_name):
+ cursor = self.connection.cursor()
+ cursor.execute(f"START TRANSACTION; DELETE FROM `{table_name}`; COMMIT;")
+ cursor.close()
+
+ def get_column_names(self):
+ cursor = self.connection.cursor()
+ cursor.execute('DESCRIBE table_task1;')
+ table_structure = cursor.fetchall()
+ table_headers = [field[0] for field in table_structure]
+ return table_headers
+
+ def insert_one(self, table_name, row, field_names=None):
+ cursor = self.connection.cursor()
+
+ if field_names is not None:
+ field_names_formatted = []
+ for name in field_names:
+ if name != 'NULL' or not name.isnumeric():
+ name = f'`{name}`'
+ field_names_formatted.append(name)
+ field_names_formatted = f'({",".join(field_names_formatted)})'
+ else:
+ field_names_formatted = ''
+
+ row_formatted = []
+ for value in row:
+ if value == 'NULL' or value.isnumeric():
+ row_formatted.append(value)
+ else:
+ row_formatted.append(f'"{value}"')
+
+ request = "START TRANSACTION; INSERT INTO `{}` {} VALUES ({}); COMMIT;".format(
+ table_name, field_names_formatted, ",".join(row_formatted)
+ )
+
+ print(request)
+
+ cursor.execute(request)
+ cursor.close()
+
+ def update(self, table_name, expressions, conditions):
+ cursor = self.connection.cursor()
+
+ expressions_formatted = []
+ for field_name, value in expressions.items():
+ if value != 'NULL' or not value.isnumeric():
+ value = f'"{value}"'
+ expressions_formatted.append(f'`{field_name}`={value}')
+
+ conditions_formatted = []
+ for field_name, value in conditions.items():
+ if value != 'NULL' or not value.isnumeric():
+ value = f'"{value}"'
+ conditions_formatted.append(f'`{field_name}`={value}')
+
+ cursor.execute("START TRANSACTION; UPDATE `{}` SET {} WHERE {}; COMMIT;".format(
+ table_name, ','.join(expressions_formatted), ' AND '.join(conditions_formatted)
+ ))
+
+ cursor.close()
+
+ def delete_from(self, table_name, conditions):
+ cursor = self.connection.cursor()
+
+ conditions_formatted = []
+ for field_name, value in conditions.items():
+ if value != 'NULL' or not value.isnumeric():
+ value = f'"{value}"'
+ conditions_formatted.append(f'`{field_name}`={value}')
+
+ cursor.execute("START TRANSACTION; DELETE FROM `{}` WHERE {}; COMMIT;".format(
+ table_name, ' AND '.join(conditions_formatted)
+ ))
+
+ cursor.close()
+
+ def get_data(self, table_name):
+ cursor = self.connection.cursor()
+
+ cursor.execute(f'SELECT * FROM `{table_name}`;')
+ content = list(map(list, cursor.fetchall()))
+
+ cursor.close()
+ return content
+
+ def get_rows(self, table_name, conditions):
+ cursor = self.connection.cursor()
+
+ conditions_formatted = []
+ for field_name, value in conditions.items():
+ if value != 'NULL' or not value.isnumeric():
+ value = f'"{value}"'
+ conditions_formatted.append(f'`{field_name}`={value}')
+
+ cursor.execute(f'SELECT * FROM `{table_name}` WHERE {" AND ".join(conditions_formatted)};')
+ content = list(map(list, cursor.fetchall()))
+
+ cursor.close()
+
+ return content
diff --git a/day9/task5_vue/backend/router.py b/day9/task5_vue/backend/router.py
deleted file mode 100644
index be8305e..0000000
--- a/day9/task5_vue/backend/router.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import re
-from backend.utils import NOT_FOUND_CODE, BAD_REQUEST_CODE
-from backend.core import Response, ErrorResponse
-
-
-def route(url_format, methods=None):
- if methods is None:
- methods = ['GET']
-
- def wrapper(func):
- def inner(url, query, *args, **kwargs):
- pattern = re.compile(url_format)
- match = re.fullmatch(pattern, url)
-
- if match is None or len(match.groups()) != pattern.groups:
- return ErrorResponse(BAD_REQUEST_CODE)
-
- return func(query, *match.groups(), *args, **kwargs)
-
- _router_tree[url_format] = _router_tree.get(url_format, {})
- for method in methods:
- _router_tree[url_format][method] = inner
-
- return inner
-
- return wrapper
-
-
-def run(request) -> Response:
- res = ErrorResponse(NOT_FOUND_CODE)
-
- method, url = request['method'], request['url']
- for url_pattern in _router_tree:
- if re.fullmatch(url_pattern, url) and method in _router_tree[url_pattern]:
- res = _router_tree[url_pattern][method](url, request['query'])
- break
-
- return res
-
-
-_router_tree = {}
diff --git a/day9/task5_vue/backend/routes.py b/day9/task5_vue/backend/routes.py
new file mode 100644
index 0000000..65117c2
--- /dev/null
+++ b/day9/task5_vue/backend/routes.py
@@ -0,0 +1,15 @@
+from backend.core.router import Route, router_table
+from backend.views import *
+
+router_table.extend([
+ Route(index_get, '/'),
+ Route(return_static, r'/static/(.*?)/(.*?)/?'),
+
+ Route(upload_file, '/api/upload/?', ['POST']),
+ Route(update_post, '/api/update/?', ['POST']),
+ Route(delete_post, '/api/delete/?', ['POST']),
+ Route(add_post, '/api/add/?', ['POST']),
+ Route(db_get, '/api/get/?', ['POST']),
+
+ Route(validate, '/validate/?', ['POST']),
+])
diff --git a/day9/task5_vue/backend/schemes.py b/day9/task5_vue/backend/schemes.py
new file mode 100644
index 0000000..c0af1dd
--- /dev/null
+++ b/day9/task5_vue/backend/schemes.py
@@ -0,0 +1,23 @@
+from backend.database.field_types import *
+from backend.database.validators import *
+from backend.database.scheme import DatabaseScheme
+
+
+class Day9Table(DatabaseScheme):
+ meta = {
+ 'name': 'table_task1',
+ 'primary_key': 'service_id'
+ }
+
+ fields = {
+ 'service_id': IntegerField(None, False, True, None, validators=[ValidateNull, ValidateType]),
+ 'servtype': TextField(20, True, False, 'hosting', validators=[ValidateNull, ValidateLength, ValidateType]),
+ 'subtype': TextField(32, True, False, '', validators=[ValidateNull, ValidateLength, ValidateType]),
+ 'user_id': IntegerField(None, False, False, None, validators=[ValidateNull, ValidateType]),
+ 'referrer_user_id': IntegerField(None, False, False, None, validators=[ValidateNull, ValidateType]),
+ 'state': TextField(1, False, False, 'N', validators=[ValidateNull, ValidateLength, ValidateType]),
+ 'creation_date': DateField(False, '0000-01-01', validators=[ValidateType, ValidateNull, ValidateDate]),
+ 'creation_time': TimeField(False, '00:00:00', validators=[ValidateType, ValidateNull, ValidateTime]),
+ 'creation_request_sent_date': DatetimeField(True, None, validators=[ValidateType, ValidateDatetime]),
+ 'notified_about_expiration': IntegerField(None, False, False, 0, validators=[ValidateNull, ValidateType])
+ }
diff --git a/day9/task5_vue/backend/settings.py b/day9/task5_vue/backend/settings.py
new file mode 100644
index 0000000..2271672
--- /dev/null
+++ b/day9/task5_vue/backend/settings.py
@@ -0,0 +1,18 @@
+STATIC_FILES_PATH = 'static'
+
+SERVER_HOST = ''
+SERVER_PORT = 8000
+
+DEBUG_MODE = True
+
+DATABASES = [
+ {
+ 'type': 'mysql',
+
+ 'name': 'mysql',
+ 'host': 'pcserv2',
+ 'username': 'day9',
+ 'password': 'day9',
+ 'db_name': 'day9'
+ }
+]
diff --git a/day9/task5_vue/backend_serve.py b/day9/task5_vue/backend/views.py
index 7aa89ef..d6d9351 100644
--- a/day9/task5_vue/backend_serve.py
+++ b/day9/task5_vue/backend/views.py
@@ -1,15 +1,12 @@
-from backend.router import route
-from backend.utils import render_template, NOT_FOUND_CODE
-from backend.config import SERVER_HOST, SERVER_PORT, STATIC_FILES_PATH
+from backend.core.utils import render_template, NOT_FOUND_CODE
+from backend.settings import STATIC_FILES_PATH
-from backend.core import ErrorResponse, TextFileResponse, ImageResponse, HtmlResponse, JsonResponse
+from backend.core.response_types import ErrorResponse, TextFileResponse, ImageResponse, HtmlResponse, JsonResponse
+from backend.database.database import get_wrapper_for
-import logging
import csv
-from sys import stdout
-@route('/static/(.*?)/(.*?)/?')
def return_static(query, *args):
if args[0] in ['css', 'js']:
try:
@@ -29,16 +26,17 @@ def return_static(query, *args):
return ErrorResponse(404)
-@route('/api/upload/?', ['POST'])
def upload_file(query, *args):
base_html = '<a href="/">Return to main page</a><br>%s'
try:
- data = query['files'][0]['data'].decode()
+ data = query['files'][0]['data'].decode('utf-8')
except (UnicodeDecodeError, KeyError, IndexError):
return ErrorResponse(500, base_html % '<h1>Error while reading file</h1>')
- permitted_headers = db_column_names()
+ wrapper = get_wrapper_for('mysql')
+
+ permitted_headers = wrapper.get_column_names()
try:
data = list(csv.reader(data.strip().splitlines(), delimiter=';', quotechar='"'))
@@ -47,41 +45,30 @@ def upload_file(query, *args):
except IndexError:
return ErrorResponse(500, base_html % '<h1>File format error</h1>')
- cursor = db.cursor()
- cursor.execute("START TRANSACTION; DELETE FROM `table_task1`; COMMIT;")
- cursor.close()
+ wrapper.clear_table('table_task1')
+ scheme = wrapper.schemes['table_task1']
for row in data[1:]:
- values = []
- for i in range(len(row)):
- if row[i] == 'NULL':
- if data[0][i] == 'creation_request_sent_date':
- value = 'NULL'
- else:
- value = '"NULL"'
- else:
- value = f'"{row[i]}"'
-
- values.append(value)
-
- cursor = db.cursor()
- cursor.execute(
- "START TRANSACTION; "
- "INSERT INTO `table_task1` ({}) VALUES ({}); "
- "COMMIT;".format(
- ','.join(data[0]), ','.join(values)
- )
- )
- cursor.close()
+ validation_results = scheme.validate(dict(zip(data[0], row)))
+ if not validation_results['error']:
+ wrapper.insert_one('table_task1', row, data[0])
+ else:
+ return ErrorResponse(500)
return HtmlResponse(base_html % '<h1>File uploaded</h1>')
-@route('/api/update/?', ['POST'])
+def validate(query, *args):
+ wrapper = get_wrapper_for('mysql')
+ scheme = wrapper.schemes['table_task1']
+ validation_results = scheme.validate(query)
+ return JsonResponse(validation_results)
+
+
def update_post(query, *args):
- print(query)
+ wrapper = get_wrapper_for('mysql')
service_id = query['service_id']
- permitted_fields = db_column_names()
+ permitted_fields = wrapper.get_column_names()
permitted_fields.remove('service_id')
query_set = []
@@ -89,27 +76,18 @@ def update_post(query, *args):
if field_name in permitted_fields:
query_set.append(f'{field_name}="{value}"')
- cursor = db.cursor()
- cursor.execute("START TRANSACTION; UPDATE `table_task1` SET {} WHERE {}; COMMIT;".format(
- ','.join(query_set), f'service_id="{service_id}"'
- ))
- cursor.close()
-
+ wrapper.update('table_task1', query_set, {'service_id': service_id})
return HtmlResponse('<a href="/">Return to main page</a><br><h1>Database Updated</h1>')
-@route('/api/delete/?', ['POST'])
def delete_post(query, *args):
- service_id = query['service_id']
- cursor = db.cursor()
- cursor.execute(f'START TRANSACTION; DELETE FROM `table_task1` WHERE service_id="{service_id}"; COMMIT;')
-
+ get_wrapper_for('mysql').delete_from('table_task1', {'service_id': query['service_id']})
return HtmlResponse('<a href="/">Return to main page</a><br><h1>Database Updated</h1>')
-@route('/api/add/?', ['POST'])
def add_post(query, *args):
- header_fields = db_column_names()
+ wrapper = get_wrapper_for('mysql')
+ header_fields = wrapper.get_column_names()
headers = []
values = []
for field_name in query:
@@ -118,23 +96,17 @@ def add_post(query, *args):
values.append(f'"{query[field_name]}"')
if len(headers) == len(header_fields):
- cursor = db.cursor()
- cursor.execute("START TRANSACTION; INSERT INTO `table_task1` ({}) VALUES ({}); COMMIT;".format(
- ','.join(headers), ','.join(values)
- ))
- cursor.close()
+ wrapper.insert_one('table_task1', values, headers)
return HtmlResponse('<a href="/">Return to main page</a><br><h1>Database Updated</h1>')
-@route('/api/get/?', ['POST'])
def db_get(query, *args):
- table_headers = db_column_names()
- cursor = db.cursor()
+ wrapper = get_wrapper_for('mysql')
+ table_headers = wrapper.get_column_names()
if query['type'] == 'full':
- cursor.execute('SELECT * FROM table_task1;')
- content = list(map(list, cursor.fetchall()))
+ content = wrapper.get_data('table_task1')
for row in content:
# creation_time
@@ -146,8 +118,6 @@ def db_get(query, *args):
creation_time = ':'.join(_.rjust(2, '0') for _ in creation_time.split(':'))
row[8] = creation_date + ' ' + creation_time
- cursor.close()
-
json_content = []
for row in content:
new_row = []
@@ -160,8 +130,7 @@ def db_get(query, *args):
return JsonResponse({'headers': table_headers, 'content': json_content})
elif query['type'] == 'single_id':
- cursor.execute(f'SELECT * FROM table_task1 WHERE service_id="{query["service_id"]}";')
- content = list(cursor.fetchall()[0])
+ content = wrapper.get_rows('table_task1', {'service_id': query['service_id']})[0]
# creation_time
content[7] = ':'.join(_.rjust(2, '0') for _ in str(content[7]).split(':'))
@@ -172,8 +141,6 @@ def db_get(query, *args):
creation_time = ':'.join(_.rjust(2, '0') for _ in creation_time.split(':'))
content[8] = creation_date + ' ' + creation_time
- cursor.close()
-
if content is not None:
json_content = []
for col in content:
@@ -188,27 +155,6 @@ def db_get(query, *args):
return ErrorResponse(NOT_FOUND_CODE)
-@route('/')
def index_get(query, *args):
data = render_template('index.html')
return HtmlResponse(data)
-
-
-def prepare_logger():
- logger = logging.getLogger('tableApp')
- logger.setLevel(logging.INFO)
-
- sh = logging.StreamHandler(stdout)
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
- sh.setFormatter(formatter)
- logger.addHandler(sh)
-
-
-if __name__ == '__main__':
- prepare_logger()
-
- from backend.server import start_server
- from backend.database import db, db_column_names
-
- logging.getLogger('tableApp').info(f'Starting server...')
- start_server(SERVER_HOST, SERVER_PORT)
diff --git a/day9/task5_vue/package-lock.json b/day9/task5_vue/package-lock.json
index 64b647f..7718c3d 100644
--- a/day9/task5_vue/package-lock.json
+++ b/day9/task5_vue/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "task5_vue",
- "version": "1.0.0",
+ "version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/day9/task5_vue/package.json b/day9/task5_vue/package.json
index 5ab0280..650d57a 100644
--- a/day9/task5_vue/package.json
+++ b/day9/task5_vue/package.json
@@ -4,7 +4,7 @@
"description": "Example app for day9 of reg.ru",
"main": "src/index.js",
"scripts": {
- "start": "python backend_serve.py",
+ "start": "python runserver.py",
"build": "webpack -p"
},
"author": "Andrew Guschin",
diff --git a/day9/task5_vue/runserver.py b/day9/task5_vue/runserver.py
new file mode 100644
index 0000000..8dd4d25
--- /dev/null
+++ b/day9/task5_vue/runserver.py
@@ -0,0 +1,27 @@
+from backend.database.database import initialize_databases, wrappers
+from backend.schemes import *
+from backend.settings import DEBUG_MODE, DATABASES, SERVER_HOST, SERVER_PORT
+
+import logging
+from sys import stdout
+
+
+def prepare_logger():
+ logger = logging.getLogger('tableApp')
+ logger.setLevel(logging.DEBUG if DEBUG_MODE else logging.INFO)
+
+ sh = logging.StreamHandler(stdout)
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+ sh.setFormatter(formatter)
+ logger.addHandler(sh)
+
+
+if __name__ == '__main__':
+ prepare_logger()
+ initialize_databases(DATABASES, [Day9Table])
+
+ from backend.routes import *
+ from backend.core.server import start_server
+
+ logging.getLogger('tableApp').info(f'Starting server...')
+ start_server(SERVER_HOST, SERVER_PORT)
diff --git a/day9/task5_vue/src/components/EditFormBox.vue b/day9/task5_vue/src/components/EditFormBox.vue
index 72d09fe..46438d4 100644
--- a/day9/task5_vue/src/components/EditFormBox.vue
+++ b/day9/task5_vue/src/components/EditFormBox.vue
@@ -5,46 +5,50 @@
<div class="formRow">
<div class="formRow-label">service_id</div>
- <div class="formRow-input" v-if="formType === 'update'">
- <input type="text" name="service_id" class="formInput" readonly="readonly" v-bind:value="tableRow['service_id']">
- </div>
- <div class="formRow-input" v-else-if="formType === 'create'">
- <input type="text" name="service_id" class="formInput" v-bind:value="tableRow['service_id']">
+ <div class="formRow-input">
+ <input
+ v-if="formType === 'update'"
+ type="text" ref="service_id" class="formInput"
+ readonly="readonly" v-bind:value="tableRow['service_id']">
+ <input
+ v-else-if="formType === 'create'"
+ type="text" ref="service_id" class="formInput"
+ v-bind:value="tableRow['service_id']">
</div>
</div>
<div class="formRow">
<div class="formRow-label"> servtype </div>
<div class="formRow-input">
- <input type="text" name="servtype" class="formInput" v-bind:value="tableRow['servtype']">
+ <input type="text" ref="servtype" class="formInput" v-bind:value="tableRow['servtype']">
</div>
</div>
<div class="formRow">
<div class="formRow-label"> subtype </div>
<div class="formRow-input">
- <input type="text" name="subtype" class="formInput" v-bind:value="tableRow['subtype']">
+ <input type="text" ref="subtype" class="formInput" v-bind:value="tableRow['subtype']">
</div>
</div>
<div class="formRow">
<div class="formRow-label"> user_id </div>
<div class="formRow-input">
- <input type="text" name="user_id" class="formInput" v-bind:value="tableRow['user_id']">
+ <input type="text" ref="user_id" class="formInput" v-bind:value="tableRow['user_id']">
</div>
</div>
<div class="formRow">
<div class="formRow-label"> referrer_user_id </div>
<div class="formRow-input">
- <input type="text" name="referrer_user_id" class="formInput" v-bind:value="tableRow['referrer_user_id']">
+ <input type="text" ref="referrer_user_id" class="formInput" v-bind:value="tableRow['referrer_user_id']">
</div>
</div>
<div class="formRow">
<div class="formRow-label"> state </div>
<div class="formRow-input">
- <select name="state" v-bind:value="tableRow['state']">
+ <select ref="state" v-bind:value="tableRow['state']">
<option>N</option>
<option>A</option>
<option>S</option>
@@ -57,13 +61,13 @@
<div class="formRow">
<div class="formRow-label"> creation_date </div>
<div class="formRow-input">
- <input type="date" name="creation_date" class="formInput" v-bind:value="tableRow['creation_date']">
+ <input type="date" ref="creation_date" class="formInput" v-bind:value="tableRow['creation_date']">
</div>
</div>
<div class="formRow">
<div class="formRow-label"> creation_time </div>
<div class="formRow-input">
- <input type="time" name="creation_time" class="formInput" v-bind:value="tableRow['creation_time']">
+ <input type="time" ref="creation_time" class="formInput" v-bind:value="tableRow['creation_time']">
</div>
</div>
@@ -71,28 +75,31 @@
<div class="formRow-label"> creation_request_sent_date </div>
<div class="formRow-input" style="display: flex; flex-direction: row">
<input
- type="date" name="creation_request_sent_date"
+ type="date" ref="creation_request_sent_date"
class="formInput" style="width: 50%;"
v-bind:value="tableRow['creation_request_sent_date'].split(' ')[0]">
<input
- type="time" name="creation_request_sent_time"
+ type="time" ref="creation_request_sent_time"
class="formInput" style="width: 50%;"
v-bind:value="tableRow['creation_request_sent_date'].split(' ')[1]">
</div>
+ <div class="formRow-error" v-if="validationResponse.creation_request_sent_date !== null">
+ {{ validationResponse.creation_request_sent_date }}
+ </div>
</div>
<div class="formRow">
<div class="formRow-label"> notified_about_expiration </div>
<div class="formRow-input">
- <input type="text" name="notified_about_expiration" class="formInput" v-bind:value="tableRow['notified_about_expiration']">
+ <input type="text" ref="notified_about_expiration" class="formInput" v-bind:value="tableRow['notified_about_expiration']">
</div>
</div>
<div class="formButtons">
<button type="button" @click="cancelCallback">Cancel</button>
- <button type="submit" v-if="formType === 'update'">Update</button>
- <button type="submit" v-else-if="formType === 'create'">Create</button>
+ <button type="button" v-if="formType === 'update'" @click="submitForm">Update</button>
+ <button type="button" v-else-if="formType === 'create'" @click="submitForm">Create</button>
</div>
</form>
</div>
@@ -100,16 +107,74 @@
</template>
<script>
+ import axios from 'axios';
+
export default {
name: "EditFormBox",
props: ['formType', 'cancelCallback', 'tableRow'],
+ data() {
+ return {
+ validationResponse: {
+ error: false,
+ service_id: null, servtype: null,
+ subtype: null, user_id: null,
+ referrer_user_id: null,
+ state: null, creation_date: null,
+ creation_time: null,
+ creation_request_sent_date: null,
+ notified_about_expiration: null
+ }
+ }
+ },
+
mounted() {
let action = '';
if (this.formType === 'create') action = '/api/add';
else if (this.formType === 'update') action = '/api/update';
this.$refs.tableForm.action = action;
+ },
+
+ methods: {
+ submitForm() {
+ let creation_date = this.$refs.creation_request_sent_date.value;
+ let creation_time = this.$refs.creation_request_sent_time.value;
+
+ let formData = {
+ service_id: parseInt(this.$refs.service_id.value),
+ servtype: this.$refs.servtype.value,
+ subtype: this.$refs.subtype.value,
+ user_id: parseInt(this.$refs.user_id.value),
+ referrer_user_id: parseInt(this.$refs.referrer_user_id.value),
+ state: this.$refs.state.value,
+ creation_date: this.$refs.creation_date.value,
+ creation_time: this.$refs.creation_time.value,
+ creation_request_sent_date: (creation_date + ' ' + creation_time),
+ notified_about_expiration: parseInt(this.$refs.notified_about_expiration.value)
+ };
+
+ axios
+ .request({
+ url: '/validate/',
+ method: 'post',
+ headers: {'Content-Type': 'application/json'},
+ data: JSON.stringify(formData)
+ })
+ .then(response => {
+ this.validationResponse = response.data;
+ });
+
+ if (this.validationResponse.error !== null) {
+ axios
+ .request({
+ url: '/api/update/',
+ method: 'post',
+ headers: {'Content-Type': 'application/json'},
+ data: JSON.stringify(formData)
+ })
+ }
+ }
}
}
</script>
@@ -154,6 +219,11 @@
background-color: #fbfbfb;
}
+ .formRow-error {
+ font-size: 15px;
+ color: #ff0000;
+ }
+
.formButtons {
display: flex;
flex-direction: row;