summaryrefslogtreecommitdiff
path: root/day9
diff options
context:
space:
mode:
Diffstat (limited to 'day9')
-rw-r--r--day9/task5/index.html485
-rw-r--r--day9/task5/main.py86
-rw-r--r--day9/task5/server.py35
-rw-r--r--day9/task5/static/css/ContentBoxStyle.css26
-rw-r--r--day9/task5/static/css/FormStyle.css60
-rw-r--r--day9/task5/static/css/NewEntryStyle.css32
-rw-r--r--day9/task5/static/css/TableStyle.css23
-rw-r--r--day9/task5/static/images/upload_icon.pngbin0 -> 57084 bytes
-rw-r--r--day9/task5/static/js/config.js1
-rw-r--r--day9/task5/static/js/main.js317
-rw-r--r--day9/task5/utils.py59
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]}')">&#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 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
new file mode 100644
index 0000000..1e552a3
--- /dev/null
+++ b/day9/task5/static/images/upload_icon.png
Binary files differ
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]}')">&#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 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