diff options
| author | Andrew <saintruler@gmail.com> | 2019-07-07 23:40:09 +0400 |
|---|---|---|
| committer | Andrew <saintruler@gmail.com> | 2019-07-07 23:40:09 +0400 |
| commit | 5dc61e9d6a760e3a86b0bb459c0a628941069d95 (patch) | |
| tree | eeec35ae80b8befd2d2920d52cf08aa5bc695736 | |
| parent | 64f9869d53bd4ec8b8867c8142a0deb919e143a4 (diff) | |
WIP: Добавлена загрузка файлов.
| -rw-r--r-- | day9/task5/index.html | 485 | ||||
| -rw-r--r-- | day9/task5/main.py | 86 | ||||
| -rw-r--r-- | day9/task5/server.py | 35 | ||||
| -rw-r--r-- | day9/task5/static/css/ContentBoxStyle.css | 26 | ||||
| -rw-r--r-- | day9/task5/static/css/FormStyle.css | 60 | ||||
| -rw-r--r-- | day9/task5/static/css/NewEntryStyle.css | 32 | ||||
| -rw-r--r-- | day9/task5/static/css/TableStyle.css | 23 | ||||
| -rw-r--r-- | day9/task5/static/images/upload_icon.png | bin | 0 -> 57084 bytes | |||
| -rw-r--r-- | day9/task5/static/js/config.js | 1 | ||||
| -rw-r--r-- | day9/task5/static/js/main.js | 317 | ||||
| -rw-r--r-- | day9/task5/utils.py | 59 |
11 files changed, 631 insertions, 493 deletions
diff --git a/day9/task5/index.html b/day9/task5/index.html index 1e7c9a5..036e6e1 100644 --- a/day9/task5/index.html +++ b/day9/task5/index.html @@ -4,165 +4,10 @@ <meta charset="UTF-8"> <title>"table_task1" table view</title> - <!-- table styling--> - <style type="text/css"> - table { - border-spacing: 0; - padding: 0; - border: 1px solid black; - } - - thead > tr > th { - padding: 6px; - font-size: 16px; - background-color: #6f6f6f; - } - - td { - padding: 3px; - font-size: 13px; - } - </style> - - <!-- form styling--> - <style type="text/css"> - .formRow { - width: 100%; - display: flex; - flex-direction: column; - } - - .formRow-label { - font-size: 13px; - 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; - - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - } - - .formRow-input > input:disabled { - background-color: #bebebe; - color: #4b4b4b; - } - - .formInput[type="date"], .formInput[type="time"] { - padding: 5px; - font-size: 1.1em; - } - - .formRow-input > select:hover, .formRow-input > input:not([disabled]):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; - } - </style> - - <!-- table fields styling--> - <style type="text/css"> - .odd { background-color: #dedede; } - .even { background-color: #c5c5c5; } - - .editableField:hover { - background-color: rgba(0, 0, 0, 0.3); - } - </style> - - <!-- content box styling--> - <style type="text/css"> - .contentBox { - padding: 20px; - width: 500px; - height: 520px; - 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; - } - </style> - - <!-- add new entry styling--> - <style type="text/css"> - .newEntryBtn { - overflow: hidden; - white-space: nowrap; - - position: fixed; - bottom: 50px; - left: 50px; - - padding: 5px; - border-radius: 20px; - width: 40px; - height: 40px; - border: none; - background-color: #2871e2; - color: white; - font-size: 20px; - - transition-property: width, font-size; - transition-duration: 0.6s; - } - - .newEntryBtn:hover { - width: 150px; - font-size: 15px; - background-color: #286bd6; - } - - .newEntryBtn:focus { - width: 200px; - font-size: 15px; - background-color: #2861c3; - } - </style> + <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()"> @@ -177,324 +22,16 @@ onmouseenter="addNewRowOnHover()" onmouseleave="addNewRowOnEndHover()" > + </button> - </div> - - <script> - 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("&") - } - }; - - 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">` - }; - - const baseFormFields = `<form id="tableForm" action="/update"> - <div class="formRow"> - <div class="formRow-label">service_id</div> - <div class="formRow-input"><input type="text" name="service_id" class="formInput" disabled></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 elementFromHTML(html) { - let elem = document.createElement('div'); - elem.innerHTML = html; - return elem.firstChild; - } - - function onPageLoad() { - request.post('http://localhost:8000/get', renderTable, {'type': 'full'}); - } - - function onFieldClick(fieldId) { - let fieldElement = document.getElementById(fieldId); - - if (fieldElement.firstChild.nodeName !== 'INPUT' && fieldElement.firstChild.nodeName !== 'SELECT') { - let [columnName, serviceId] = fieldId.split('-'); - fieldElement.innerHTML = defaultColumnInputs[columnName]; - if (columnName === 'creation_request_sent_date') { - let dateElement = fieldElement.firstChild; - let timeElement = fieldElement.childNodes[1]; - - dateElement.onkeyup = timeElement.onkeyup = (event) => { - if (event.code === 'Enter') { - if (dateElement.value !== '' && timeElement.value !== '') - fieldEditSubmit(fieldId, `${dateElement.value} ${timeElement.value}`); - } - }; - } - else { - let inputElement = fieldElement.firstChild; - inputElement.onkeyup = (event) => { - if (event.code === 'Enter') - fieldEditSubmit(fieldId, inputElement.value); - }; - } - } - } - - function fieldEditSubmit(fieldId, value) { - let [columnName, serviceId] = fieldId.split('-'); - request.post('http://localhost:8000/update', () => {}, { - 'service_id': serviceId, [columnName]: value - }); - document.getElementById(fieldId).innerHTML = value; - } - - function renderTable(text) { - let data = JSON.parse(text); - - let table = document.getElementById('table'); - - 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, rowIndex) => { - row.push(`<button value="${row[0]}" onclick="setupEditFields('${row[0]}')">✎</button>`); - row.push(`<button value="${row[0]}" onclick="removeField('${row[0]}')">✖</button>`); - - let contentRow = document.createElement('tr'); - contentRow.setAttribute('id', `row_${row[0]}`); - - row.forEach((column, colIndex) => { - let color = (colIndex % 2 + rowIndex % 2) % 2 === 0 ? 'odd' : 'even'; - 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.classList.add(color); - columnNode.innerHTML = column; - contentRow.appendChild(columnNode); - }); - - tableContent.appendChild(contentRow); - }); - - table.appendChild(tableHeaders); - table.appendChild(tableContent); - } - - function setupEditFields(service_id) { - showEditForm(); - request.post('http://localhost:8000/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 - }); - } - - function removeField(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 - } - } - } - - 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; - } - - showEditForm = () => { - let contentBox = showForm(); - contentBox.appendChild(elementFromHTML(formEditButtons)); - }; - - showCreateForm = () => { - let contentBox = showForm(); - contentBox.appendChild(elementFromHTML(formCreateButtons)); - }; - - function hideForm() { - let newEntryBtn = document.getElementById('newEntryBtn'); - newEntryBtn.style.display = 'inline'; - - let contentBox = document.getElementById('contentBox'); - while (contentBox.firstChild) - contentBox.removeChild(contentBox.firstChild); +<!-- > <img src="/static/images/upload_icon.png" alt=""> </div>--> - hideContentBox(); - } + <form enctype="multipart/form-data" action="/api/upload" method="post"> + <input type="file" name="filename" accept="text/csv"> + <button type="submit">Submit</button> + </form> - function addNewRowOnHover() { - let btn = document.getElementById('newEntryBtn'); - btn.innerText = 'Add new row'; - } - function addNewRowOnEndHover() { - let btn = document.getElementById('newEntryBtn'); - btn.innerText = '+'; - } - </script> + <script src="/static/js/main.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 index d5bcbb8..c49852a 100644 --- a/day9/task5/main.py +++ b/day9/task5/main.py @@ -1,12 +1,63 @@ from router import route from utils import render_template, NOT_FOUND_CODE -from config import SERVER_HOST, SERVER_PORT +from config import SERVER_HOST, SERVER_PORT, STATIC_FILES_PATH import logging +import csv from sys import stdout -@route('/update', ['POST']) +@route('/static/(.*?)/(.*?)/?') +def return_static(query, *args): + if args[0] in ['css', 'js', 'images']: + try: + with open(STATIC_FILES_PATH + f'/{args[0]}/{args[1]}', encoding='utf-8') as f: + data = f.read() + + return data + except (PermissionError, FileNotFoundError): + return '' + + +@route('/api/upload', ['POST']) +def upload_file(query, *args): + try: + data = query['files'][0]['data'].decode() + except (UnicodeDecodeError, KeyError, IndexError): + return '<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 '<h1>File format error</h1>' + except IndexError: + return '<h1>File format error</h1>' + + for row in data[1:]: + cursor = db.cursor() + 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.execute( "START TRANSACTION; INSERT INTO `table_task1` ({}) VALUES ({}); COMMIT;".format( + ','.join(data[0]), ','.join(values) + )) + cursor.close() + + return '<h1>File uploaded</h1>' + + +@route('/api/update', ['POST']) def update_post(query, *args): service_id = query['service_id'] permitted_fields = db_column_names() @@ -18,22 +69,41 @@ def update_post(query, *args): query_set.append(f'{field_name}="{value}"') cursor = db.cursor() - cursor.execute("UPDATE `table_task1` SET {} WHERE {};".format( + cursor.execute("START TRANSACTION; UPDATE `table_task1` SET {} WHERE {}; COMMIT;".format( ','.join(query_set), f'service_id="{service_id}"' )) cursor.close() - return f'<h1>Database Updated</h1>' + return f'<h1>Database Updated {query}</h1>' -@route('/delete', ['POST']) +@route('/api/delete', ['POST']) def delete_post(query, *args): - return f'<h1>DELETE: {query}</h1>' + service_id = query['service_id'] + cursor = db.cursor() + cursor.execute(f'START TRANSACTION; DELETE FROM `table_task1` WHERE service_id="{service_id}"; COMMIT;') + + return f'<h1>Database Updated {query}</h1>' -@route('/add', ['POST']) +@route('/api/add', ['POST']) def add_post(query, *args): - return f'<h1>ADD: {query}</h1>' + 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 f'<h1>Database Updated {query}</h1>' @route('/get', ['POST']) diff --git a/day9/task5/server.py b/day9/task5/server.py index a6044bd..7757821 100644 --- a/day9/task5/server.py +++ b/day9/task5/server.py @@ -3,7 +3,7 @@ from urllib.parse import parse_qs from json import dumps from router import run -from utils import HTTP_STATUS_CODES +from utils import HTTP_STATUS_CODES, parse_multipart_form import logging @@ -32,24 +32,39 @@ class MyHTTPRequestHandler(BaseHTTPRequestHandler): def do_POST(self): content_length = int(self.headers['Content-Length']) - post_data = parse_qs(self.rfile.read(content_length).decode('utf-8')) + content_type = self.headers['Content-type'] + post_data = self.rfile.read(content_length) - for key in post_data: - post_data[key] = post_data[key][0] + 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} + })) - self.finalize_request(run({ - 'url': self.path, - 'method': 'POST', - 'query': post_data - })) + elif content_type.split(';')[0] == 'text/plain': + 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 + })) def finalize_request(self, response): if isinstance(response, int): self._set_response(response, 'text/html') response = f'<center><h1>ERROR {response} {HTTP_STATUS_CODES[response].upper()}</h1></center>' - elif isinstance(response, (dict, list, tuple)): + elif isinstance(response, (dict, list)): self._set_response(200, 'application/json') response = dumps(response) + elif isinstance(response, tuple): + if response[0] == 'image': + self._set_response(200, f'image/{response[1]}') + response = '' else: self._set_response(200, 'text/html') diff --git a/day9/task5/static/css/ContentBoxStyle.css b/day9/task5/static/css/ContentBoxStyle.css new file mode 100644 index 0000000..78c37d6 --- /dev/null +++ b/day9/task5/static/css/ContentBoxStyle.css @@ -0,0 +1,26 @@ +.contentBox { + padding: 20px; + width: 500px; + height: 520px; + 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 new file mode 100644 index 0000000..2659a68 --- /dev/null +++ b/day9/task5/static/css/FormStyle.css @@ -0,0 +1,60 @@ +.formRow { + width: 100%; + display: flex; + flex-direction: column; +} + +.formRow-label { + font-size: 13px; + 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; + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.formRow-input > input:disabled { + background-color: #bebebe; + color: #4b4b4b; +} + +.formInput[type="date"], .formInput[type="time"] { + padding: 5px; + font-size: 1.1em; +} + +.formRow-input > select:hover, .formRow-input > input:not([disabled]):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 new file mode 100644 index 0000000..e1141b2 --- /dev/null +++ b/day9/task5/static/css/NewEntryStyle.css @@ -0,0 +1,32 @@ +.newEntryBtn { + overflow: hidden; + white-space: nowrap; + + position: fixed; + bottom: 50px; + left: 50px; + + padding: 5px; + border-radius: 20px; + width: 40px; + height: 40px; + border: none; + background-color: #2871e2; + color: white; + font-size: 20px; + + transition-property: width, font-size; + transition-duration: 0.6s; +} + +.newEntryBtn:hover { + width: 150px; + font-size: 15px; + background-color: #286bd6; +} + +.newEntryBtn:focus { + width: 200px; + font-size: 15px; + background-color: #2861c3; +} diff --git a/day9/task5/static/css/TableStyle.css b/day9/task5/static/css/TableStyle.css new file mode 100644 index 0000000..61f2f28 --- /dev/null +++ b/day9/task5/static/css/TableStyle.css @@ -0,0 +1,23 @@ +table { + border-spacing: 0; + padding: 0; + border: 1px solid black; +} + +thead > tr > th { + padding: 6px; + font-size: 16px; + background-color: #6f6f6f; +} + +td { + padding: 3px; + font-size: 13px; +} + +.odd { background-color: #dedede; } +.even { background-color: #c5c5c5; } + +.editableField:hover { + background-color: rgba(0, 0, 0, 0.3); +} diff --git a/day9/task5/static/images/upload_icon.png b/day9/task5/static/images/upload_icon.png Binary files differnew file mode 100644 index 0000000..1e552a3 --- /dev/null +++ b/day9/task5/static/images/upload_icon.png diff --git a/day9/task5/static/js/config.js b/day9/task5/static/js/config.js new file mode 100644 index 0000000..815f363 --- /dev/null +++ b/day9/task5/static/js/config.js @@ -0,0 +1 @@ +HOST = 'http://localhost:8000'; diff --git a/day9/task5/static/js/main.js b/day9/task5/static/js/main.js new file mode 100644 index 0000000..63a025e --- /dev/null +++ b/day9/task5/static/js/main.js @@ -0,0 +1,317 @@ +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("&") + } + }; + + 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">` + }; + + const baseFormFields = `<form id="tableForm" action="/update"> + <div class="formRow"> + <div class="formRow-label">service_id</div> + <div class="formRow-input"><input type="text" name="service_id" class="formInput" disabled></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 elementFromHTML(html) { + let elem = document.createElement('div'); + elem.innerHTML = html; + return elem.firstChild; + } + + function onPageLoad() { + request.post(`${HOST}/get`, renderTable, {'type': 'full'}); + } + + function onFieldClick(fieldId) { + let fieldElement = document.getElementById(fieldId); + + if (fieldElement.firstChild.nodeName !== 'INPUT' && fieldElement.firstChild.nodeName !== 'SELECT') { + let [columnName, serviceId] = fieldId.split('-'); + fieldElement.innerHTML = defaultColumnInputs[columnName]; + if (columnName === 'creation_request_sent_date') { + let dateElement = fieldElement.firstChild; + let timeElement = fieldElement.childNodes[1]; + + dateElement.onkeyup = timeElement.onkeyup = (event) => { + if (event.code === 'Enter') { + if (dateElement.value !== '' && timeElement.value !== '') + fieldEditSubmit(fieldId, `${dateElement.value} ${timeElement.value}`); + } + }; + } + else { + let inputElement = fieldElement.firstChild; + inputElement.onkeyup = (event) => { + if (event.code === 'Enter') + fieldEditSubmit(fieldId, inputElement.value); + }; + } + } + } + + function fieldEditSubmit(fieldId, value) { + let [columnName, serviceId] = fieldId.split('-'); + request.post(`${HOST}/api/update`, () => {}, { + 'service_id': serviceId, [columnName]: value + }); + document.getElementById(fieldId).innerHTML = value; + } + + 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, rowIndex) => { + row.push(`<button value="${row[0]}" onclick="setupEditFields('${row[0]}')">✎</button>`); + row.push(`<button value="${row[0]}" onclick="removeField('${row[0]}')">✖</button>`); + + let contentRow = document.createElement('tr'); + contentRow.setAttribute('id', `row_${row[0]}`); + + row.forEach((column, colIndex) => { + let color = (colIndex % 2 + rowIndex % 2) % 2 === 0 ? 'odd' : 'even'; + 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.classList.add(color); + columnNode.innerHTML = column; + contentRow.appendChild(columnNode); + }); + + tableContent.appendChild(contentRow); + }); + + table.appendChild(tableHeaders); + table.appendChild(tableContent); + } + + function setupEditFields(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 + }); + } + + 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 + } + } + } + + 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; + } + + showEditForm = () => { + let contentBox = showForm(); + contentBox.appendChild(elementFromHTML(formEditButtons)); + }; + + showCreateForm = () => { + let contentBox = showForm(); + contentBox.appendChild(elementFromHTML(formCreateButtons)); + }; + + 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 addNewRowOnHover() { + let btn = document.getElementById('newEntryBtn'); + btn.innerText = 'Add new row'; + } + + function addNewRowOnEndHover() { + let btn = document.getElementById('newEntryBtn'); + btn.innerText = '+'; + }
\ No newline at end of file diff --git a/day9/task5/utils.py b/day9/task5/utils.py index 3e0dc64..8f3e794 100644 --- a/day9/task5/utils.py +++ b/day9/task5/utils.py @@ -1,4 +1,7 @@ -import re +from io import BytesIO + + +CHUNK_SIZE = 49600 HTTP_STATUS_CODES = { 100: "Continue", @@ -74,3 +77,57 @@ def render_template(path, **kwargs): template = template.replace(f'%%{key}%%', kwargs[key]) return template + + +def parse_multipart_form(data: bytes): + b = BytesIO(data) + separator = b.readline().strip() + + files = [] + with open('request', 'wb') as f: + f.write(data) + + while True: + line = b.readline().strip().strip(b'-') + if not line: + break + + headers_raw = [] + while not line.strip() == b'': + headers_raw.append(line.strip().decode()) + line = b.readline() + + headers = {'Content-Type': headers_raw[1].split(': ')[1]} + for pair in headers_raw[0].split(': ')[1].split('; ')[1:]: + key, value = pair.split('=') + headers[key] = value[1:-1] + + data = b'' + prev_chunk = b.read(CHUNK_SIZE) + chunk = b.read(CHUNK_SIZE) + while separator not in (prev_chunk + chunk): + data += prev_chunk + prev_chunk = chunk + chunk = b.read(CHUNK_SIZE) + + if not chunk: + break + + chunk = (prev_chunk + chunk) + if chunk.startswith(separator) or chunk.endswith(separator): + with_sep = chunk.strip(separator) + else: + if separator in chunk: + with_sep, buffer = chunk.split(separator) + b = BytesIO(buffer + b.read()) + b.readline() + + else: + with_sep = chunk + + data += with_sep + headers['data'] = data + + files.append(headers) + + return files |