diff --git a/connection.py b/connection.py new file mode 100644 index 00000000..64723248 --- /dev/null +++ b/connection.py @@ -0,0 +1,52 @@ +# Creates a decorator to handle the database connection/cursor opening/closing. +# Creates the cursor with RealDictCursor, thus it returns real dictionaries, where the column names are the keys. +import os +import psycopg2 +import psycopg2.extras + + +def get_connection_string(): + # setup connection string + # to do this, please define these environment variables first + user_name = os.environ.get('PSQL_USER_NAME') + password = os.environ.get('PSQL_PASSWORD') + host = os.environ.get('PSQL_HOST') + database_name = os.environ.get('PSQL_DB_NAME') + + env_variables_defined = user_name and password and host and database_name + + if env_variables_defined: + + # this string describes all info for psycopg2 to connect to the database + return 'postgresql://{user_name}:{password}@{host}/{database_name}'.format( + user_name=user_name, + password=password, + host=host, + database_name=database_name + ) + else: + raise KeyError('Some necessary environment variable(s) are not defined') + + +def open_database(): + try: + connection_string = get_connection_string() + connection = psycopg2.connect(connection_string) + connection.autocommit = True + except psycopg2.DatabaseError as exception: + print('Database connection problem') + raise exception + return connection + + +def connection_handler(function): + def wrapper(*args, **kwargs): + connection = open_database() + # we set the cursor_factory parameter to return with a RealDictCursor cursor (cursor which provide dictionaries) + dict_cur = connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + ret_value = function(dict_cur, *args, **kwargs) + dict_cur.close() + connection.close() + return ret_value + + return wrapper diff --git a/data_handler.py b/data_handler.py index 0d8d9086..9e6b0004 100644 --- a/data_handler.py +++ b/data_handler.py @@ -16,15 +16,40 @@ def get_boards(): Gather all boards :return: """ - return persistence.get_boards(force=True) + return persistence.get_boards() def get_cards_for_board(board_id): - persistence.clear_cache() - all_cards = persistence.get_cards() - matching_cards = [] - for card in all_cards: - if card['board_id'] == str(board_id): - card['status_id'] = get_card_status(card['status_id']) # Set textual status for the card - matching_cards.append(card) - return matching_cards + return persistence.get_cards_for_board(board_id) + + +def get_cards(): + return persistence.get_cards() + + +def add_board(board_title): + return persistence.add_new_board(board_title) + + +def add_card(card_title, board_id, status_id): + return persistence.add_new_card(card_title, board_id, status_id) + + +def delete_board(id): + return persistence.delete_board(id) + + +def delete_card(id): + return persistence.delete_card(id) + + +def change_status( id, status_id): + return persistence.change_status( id, status_id) + +def rename_board(board_id, title): + return persistence.rename_board(board_id, title) + + +def addNewUser(username, password): + return persistence.addNewUser(username, password) + diff --git a/main.py b/main.py index 9e25a1ac..f48dd97b 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ -from flask import Flask, render_template, url_for -from util import json_response +from flask import Flask, render_template, url_for, request, json +from util import json_response, hash_password import data_handler @@ -23,9 +23,9 @@ def get_boards(): return data_handler.get_boards() -@app.route("/get-cards/") +@app.route("/get-cards/") @json_response -def get_cards_for_board(board_id: int): +def get_cards_for_board(board_id): """ All cards that belongs to a board :param board_id: id of the parent board @@ -33,6 +33,73 @@ def get_cards_for_board(board_id: int): return data_handler.get_cards_for_board(board_id) +@app.route("/get-cards") +@json_response +def get_cards(): + """ + All cards that belongs to a board + :param board_id: id of the parent board + """ + return data_handler.get_cards() + + +@app.route("/post-new-board", methods=['POST', 'GET']) +@json_response +def post_new_board(): + response_data = json.loads(request.data) + data_handler.add_board(response_data['title']) + return response_data + + +@app.route('/post-new-card', methods=['POST', 'GET']) +@json_response +def post_new_card(): + response_data = json.loads(request.data) + data_handler.add_card(response_data['title'], response_data['board_id'], response_data['status_id']) + return response_data + + +@app.route('/delete-board', methods=['POST', 'GET']) +@json_response +def delete_board(): + response_data = json.loads(request.data) + data_handler.delete_board(response_data['id']) + return response_data + + +@app.route('/delete-card', methods=['POST', 'GET']) +@json_response +def delete_card(): + response_data = json.loads(request.data) + data_handler.delete_card(response_data['id']) + return response_data + + +@app.route('/change-status', methods=['POST', 'GET']) +@json_response +def change_status(): + response_data = json.loads(request.data) + data_handler.change_status(response_data['id'], response_data['status_id']) + + +@app.route('/rename-board', methods=['POST', 'GET']) +@json_response +def rename_board(): + response_data = json.loads(request.data) + data_handler.rename_board(response_data['id'], response_data['title']) + + return response_data + + +@app.route('/reg', methods=["POST"]) +@json_response +def reg(): + reg_data = json.loads(request.data) + hashed = hash_password(reg_data['password']) + data_handler.addNewUser(reg_data['username'], hashed) + return reg_data + + def main(): app.run(debug=True) diff --git a/persistence.py b/persistence.py index 1e1f1e3d..318d12aa 100644 --- a/persistence.py +++ b/persistence.py @@ -1,51 +1,118 @@ -import csv +import connection -STATUSES_FILE = './data/statuses.csv' -BOARDS_FILE = './data/boards.csv' -CARDS_FILE = './data/cards.csv' -_cache = {} # We store cached data in this dict to avoid multiple file readings +@connection.connection_handler +def get_statuses(cursor): + cursor.execute(""" + SELECT id, title FROM status; + """) + statuses = cursor.fetchall() + return statuses -def _read_csv(file_name): - """ - Reads content of a .csv file - :param file_name: relative path to data file - :return: OrderedDict - """ - with open(file_name) as boards: - rows = csv.DictReader(boards, delimiter=',', quotechar='"') - formatted_data = [] - for row in rows: - formatted_data.append(dict(row)) - return formatted_data +@connection.connection_handler +def get_boards(cursor): + cursor.execute(""" + SELECT id, title FROM board; + """) -def _get_data(data_type, file, force): - """ - Reads defined type of data from file or cache - :param data_type: key where the data is stored in cache - :param file: relative path to data file - :param force: if set to True, cache will be ignored - :return: OrderedDict - """ - if force or data_type not in _cache: - _cache[data_type] = _read_csv(file) - return _cache[data_type] + boards = cursor.fetchall() + return boards -def clear_cache(): - for k in list(_cache.keys()): - _cache.pop(k) +@connection.connection_handler +def get_cards(cursor): + cursor.execute(""" + SELECT id, board_id, title, status_id, card_order FROM card; + """) + cards = cursor.fetchall() + return cards -def get_statuses(force=False): - return _get_data('statuses', STATUSES_FILE, force) +@connection.connection_handler +def get_cards_for_board(cursor, board_id): + cursor.execute("""SELECT id, board_id, title, status_id, card_order FROM card + WHERE board_id = %(board_id)s; + """, + {'board_id': board_id}) -def get_boards(force=False): - return _get_data('boards', BOARDS_FILE, force) + cards = cursor.fetchall() + return cards -def get_cards(force=False): - return _get_data('cards', CARDS_FILE, force) +@connection.connection_handler +def get_cards(cursor): + cursor.execute("""SELECT id, board_id, title, status_id, card_order FROM card; + """) + + cards = cursor.fetchall() + print(cards) + return cards + + +@connection.connection_handler +def add_new_board(cursor, board_title): + cursor.execute("""INSERT INTO board (title) VALUES (%(board_title)s); + """, + {'board_title': board_title}) + + +@connection.connection_handler +def add_new_card(cursor, card_title, board_id, status_id): + cursor.execute(""" + INSERT INTO card (title, board_id, status_id) VALUES (%(card_title)s, %(board_id)s, %(status_id)s); + """, + {'card_title': card_title, + 'board_id': board_id, + 'status_id': status_id}) + + +@connection.connection_handler +def delete_board(cursor, board_id): + cursor.execute(""" + DELETE FROM card + WHERE board_id = %(board_id)s; + + DELETE FROM board + WHERE id = %(board_id)s; + """, + {'board_id': board_id}) + + +@connection.connection_handler +def delete_card(cursor, id): + cursor.execute(""" + DELETE FROM card + WHERE id = %(id)s; + """, + {'id': id}) + + +@connection.connection_handler +def change_status(cursor, id, status_id): + cursor.execute(""" + UPDATE card + SET status_id = %(status_id)s + WHERE id = %(id)s; + """, {'id': id, 'status_id': status_id}) + + +@connection.connection_handler +def rename_board(cursor, board_id, title): + cursor.execute(""" + UPDATE board + SET title = %(title)s + WHERE id = %(board_id)s; + """, + {'board_id': board_id, + 'title': title}) + + +@connection.connection_handler +def addNewUser(cursor, username, password): + cursor.execute(""" + INSERT INTO users (username, password) VALUES(%(username)s, %(password)s); + """, + {'username': username, 'password': password}) diff --git a/sample_data/proman-sample-data.sql b/sample_data/proman-sample-data.sql new file mode 100644 index 00000000..dd561111 --- /dev/null +++ b/sample_data/proman-sample-data.sql @@ -0,0 +1,89 @@ +ALTER TABLE IF EXISTS ONLY public.board DROP CONSTRAINT IF EXISTS pk_board_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.card DROP CONSTRAINT IF EXISTS pk_card_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.status DROP CONSTRAINT IF EXISTS pk_status_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.card DROP CONSTRAINT IF EXISTS fk_board_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.card DROP CONSTRAINT IF EXISTS fk_status_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.users DROP CONSTRAINT IF EXISTS pk_users_idd CASCADE; + +DROP TABLE IF EXISTS public.users; +DROP SEQUENCE IF EXISTS public.users_id_seq; +CREATE TABLE users( + id serial NOT NULL, + username text, + password text +); + + +DROP TABLE IF EXISTS public.board; +DROP SEQUENCE IF EXISTS public.board_id_seq; +CREATE TABLE board( + id serial NOT NULL, + title text +); + + +DROP TABLE IF EXISTS public.card; +DROP SEQUENCE IF EXISTS public.card_id_seq; +CREATE TABLE card( + id serial NOT NULL, + board_id integer NOT NULL, + title text, + status_id integer NOT NULL, + card_order integer +); + + +DROP TABLE IF EXISTS public.status; +DROP SEQUENCE IF EXISTS public.status_id_seq; +CREATE TABLE status( + id serial NOT NULL , + title text +); + +ALTER TABLE ONLY users + ADD CONSTRAINT pk_users_id PRIMARY KEY (id); + +ALTER TABLE ONLY board + ADD CONSTRAINT pk_board_id PRIMARY KEY (id); + +ALTER TABLE ONLY card + ADD CONSTRAINT pk_card_id PRIMARY KEY (id); + +ALTER TABLE ONLY status + ADD CONSTRAINT pk_status_id PRIMARY KEY (id); + +ALTER TABLE ONLY card + ADD CONSTRAINT fk_board_id FOREIGN KEY (board_id) REFERENCES board(id); + +ALTER TABLE ONLY card + ADD CONSTRAINT fk_status_id FOREIGN KEY (status_id) REFERENCES status(id); + + +INSERT INTO board VALUES (1, 'board 1'); +INSERT INTO board VALUES (2, 'board 2'); +SELECT pg_catalog.setval('board_id_seq', 3, true); + +INSERT INTO status VALUES (0, 'new'); +INSERT INTO status VALUES (1, 'in progress'); +INSERT INTO status VALUES (2, 'testing'); +INSERT INTO status VALUES (3, 'done'); +SELECT pg_catalog.setval('status_id_seq', 4, true); + +INSERT INTO card VALUES (1, 1, 'new card1', 0, 0); +INSERT INTO card VALUES (2, 1, 'new card2', 0, 1); +INSERT INTO card VALUES (3, 1, 'in progress card', 1, 0); +INSERT INTO card VALUES (4, 1, 'planning', 2, 0); +INSERT INTO card VALUES (5, 1, 'done card', 3, 0); +INSERT INTO card VALUES (6, 1, 'done card', 3, 1); +INSERT INTO card VALUES (7, 2, 'new card1', 0, 0); +INSERT INTO card VALUES (8, 2, 'new card2', 0, 1); +INSERT INTO card VALUES (9, 2, 'in progress card', 1, 0); +INSERT INTO card VALUES (10, 2, 'planning', 2, 0); +INSERT INTO card VALUES (11, 2, 'done card', 3, 0); +INSERT INTO card VALUES (12, 2, 'done card', 3, 1); +SELECT pg_catalog.setval('card_id_seq', 13, true); + +INSERT INTO users VALUES (0, 'admin', '$2b$12$3.zHaCLsED7YFXBXIOhB2eHLVGBBfXUlQXurKynGhINca6FObgrwi'); +SELECT pg_catalog.setval('users_id_seq', 1, true); + + diff --git a/static/css/design.css b/static/css/design.css new file mode 100644 index 00000000..808f5f06 --- /dev/null +++ b/static/css/design.css @@ -0,0 +1,163 @@ +:root{ + --border-radius: 3px; + --status-1: #590000; + --status-2: #594300; + --status-3: #525900; + --status-4: #085900; +} + +body{ + min-width: 620px; + background: #ddd url(http://cdn.backgroundhost.com/backgrounds/subtlepatterns/diagonal-noise.png); + font-family: sans-serif; +} + +h1, .board-title, .board-column-title{ + font-weight: 100; +} + +h1{ + text-align: center; + font-size: 4em; + letter-spacing: 5px; + transform: scale(1.2, 1); +} + +button{ + background: #222; + color: #fff; + border: none; + font-size: 14px; + font-family: sans-serif; + padding: 4px 10px; +} + +.board-container{ + max-width: 960px; + margin: 0 auto; +} + +section.board{ + margin: 20px; + border: aliceblue; + background: #ffffff90; + border-radius: 3px; +} + +.board-header{ + height: 50px; + background: #fff; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.board-title{ + margin: 13px; + display: inline-block; + font-size: 20px; +} +.board-title, .board-add, .board-toggle, .board-delete{ + display: inline-block; +} + +.board-toggle{ + float: right; + margin: 13px; +} + +.board-delete{ + float:right; + margin: 13px; +} + +.board-columns{ + display: flex; + flex-wrap: nowrap; +} + +.board-column{ + padding: 10px; + flex: 1; +} + +.board-column-content{ + min-height: 49px; +} + +.board-column-content:empty{ + /*This only works if the tag is really empty and there is not even whitespace inside*/ + border: 4px solid #cdcdcd; + margin-top: 4px; + border-radius: 10px; + background: #eee; +} + +.board-column-title{ + text-align: center; +} + +.card{ + position: relative; + background: #222; + color: #fff; + border-radius: var(--border-radius); + margin: 4px 0; + padding: 4px; +} + +.board-column:nth-of-type(1) .card{ + background: var(--status-1); +} + +.board-column:nth-of-type(2) .card{ + background: var(--status-2); +} + +.board-column:nth-of-type(3) .card{ + background: var(--status-3); +} + +.board-column:nth-of-type(4) .card{ + background: var(--status-4); +} + +.card-remove{ + display: block; + position: absolute; + top: 4px; + right: 4px; + font-size: 12px; + cursor: pointer; +} + +.card-title{ + padding-right: 16px; +} + +#add-new-board{ + max-width: 920px; + margin: 0 auto; + +} + +.hidden{ + display: none; +} + +.board-add { + margin: 13px; +} + +.rename-board { + margin: 13px; +} + +.save-button { + float: left; + margin: 13px; + +} + +.input-field { + margin: 13px; + float: left; +} \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css deleted file mode 100644 index d8f3b84f..00000000 --- a/static/css/main.css +++ /dev/null @@ -1,3 +0,0 @@ -#boards { - background-color: green; -} \ No newline at end of file diff --git a/static/js/data_handler.js b/static/js/data_handler.js index 66df0ba8..a2b18f9a 100644 --- a/static/js/data_handler.js +++ b/static/js/data_handler.js @@ -13,12 +13,22 @@ export let dataHandler = { method: 'GET', credentials: 'same-origin' }) - .then(response => response.json()) // parse the response as JSON - .then(json_response => callback(json_response)); // Call the `callback` with the returned object + .then(response => response.json()) // parse the response as JSON + .then(json_response => callback(json_response)); // Call the `callback` with the returned object }, _api_post: function (url, data, callback) { + // it is not called from outside // sends the data to the API, and calls callback function + + return fetch(url, { + method: 'POST', + dataType: 'json', + credentials: 'same-origin', + body: JSON.stringify(data), + }) + .then(response => response.json()) + .then(json_response => callback(json_response)); }, init: function () { }, @@ -43,15 +53,84 @@ export let dataHandler = { }, getCardsByBoardId: function (boardId, callback) { // the cards are retrieved and then the callback function is called with the cards + + this._api_get(`/get-cards/${boardId}`, (response) => { + this._data = response; + callback(response, boardId); + }); }, getCard: function (cardId, callback) { // the card is retrieved and then the callback function is called with the card }, createNewBoard: function (boardTitle, callback) { // creates new board, saves it and calls the callback function with its data + let data = {'title': boardTitle}; + + this._api_post('/post-new-board', data, (response) => { + this._data = response; + callback(response); + }); }, createNewCard: function (cardTitle, boardId, statusId, callback) { // creates new card, saves it and calls the callback function with its data - } + let data = { + 'title': cardTitle, + 'board_id': boardId, + 'status_id': statusId + }; + + this._api_post('/post-new-card', data, (response) => { + this._data = response; + callback(response); + }) + }, // here comes more features + getCards: function (callback) { + this._api_get(`/get-cards`, (response) => { + this._data = response; + callback(response); + }); + }, + deleteBoard: function (boardId, callback) { + let data = {'id': boardId}; + this._api_post('/delete-board', data, (response) => { + this._data = response; + callback(); + }); + }, + deleteCard: function (id, callback) { + let data = {'id': id}; + this._api_post(`/delete-card`, data, (response) => { + this._data = response; + callback(); + }); + }, + + change_status: function (id,status_id) { + let data = { + 'id': id, + 'status_id': status_id + }; + this._api_post(`/change-status`, data, (response) => { + this._data = response; + + }); + }, + + updateBoardName: function (boardId, newTitle, callback) { + let data = {'id': boardId, + 'title': newTitle}; + this._api_post('/rename-board', data, (response) => { + this._data = response; + callback(); + }) + + }, + reg: function (dict, callback) { + this._api_post('/reg', dict, (response) => { + this._data = response; + callback(response); + }) + } + }; diff --git a/static/js/dom.js b/static/js/dom.js index 0092afb7..d1e41742 100644 --- a/static/js/dom.js +++ b/static/js/dom.js @@ -1,5 +1,5 @@ // It uses data_handler.js to visualize elements -import { dataHandler } from "./data_handler.js"; +import {dataHandler} from "./data_handler.js"; export let dom = { _appendToElement: function (elementToExtend, textToAppend, prepend = false) { @@ -21,37 +21,190 @@ export let dom = { // This function should run once, when the page is loaded. }, loadBoards: function () { - // retrieves boards and makes showBoards called - dataHandler.getBoards(function(boards){ + dataHandler.getBoards(boards => { dom.showBoards(boards); }); }, + boardToggleClicked: function () { + const board = this.closest('section.board').querySelector('.board-columns'); + board.classList.toggle('hidden'); + }, showBoards: function (boards) { - // shows boards appending them to #boards div - // it adds necessary event listeners also + const boardContainer = document.querySelector('.board-container'); + boardContainer.textContent = ''; + for (const board of boards) { + const template = document.querySelector('#board-template'); + const clone = document.importNode(template.content, true); + clone.querySelectorAll('.board-column-content').forEach(column => column.dataset.boardId = board.id); + clone.querySelector('.board-title').innerHTML = board.title; + clone.querySelector('.board-title').addEventListener('click', dom.switchToInput); + clone.querySelector('.board-add').setAttribute("id", board.id); + clone.querySelector(".board-toggle").addEventListener('click', dom.boardToggleClicked); + clone.querySelector('.board-delete').setAttribute("id", board.id); + boardContainer.appendChild(clone); + dom.loadCards(parseInt(board.id)); + dom.deleteBoard(parseInt(board.id)); + + } + dom.addDragula(); + }, + loadCards: function (boardId) { + dataHandler.getCardsByBoardId(boardId, dom.showCards); + }, + showCards: function (cards, boardId) { + dom.addNewCard(boardId); + let cardContainer = document.querySelectorAll('.board-column-content'); - let boardList = ''; + const currentCards = cards.filter(card => parseInt(boardId) === parseInt(card.board_id)); + + for (let card of currentCards) { + for (let column of cardContainer) { + if (parseInt(card.status_id) === parseInt(column.id) && parseInt(card.board_id) === parseInt(column.dataset.boardId)) { + const template = document.querySelector('#cards-template'); + const clone = document.importNode(template.content, true); + clone.querySelector('.card-title').textContent = card.title; + clone.querySelector('.card').id = card.id; + column.appendChild(clone); + } + } - for(let board of boards){ - boardList += ` -
  • ${board.title}
  • - `; } + dom.initDeleteCardButtons(cards, boardId); + }, + addNewBoard: function () { + let addBoardButton = document.querySelector("#add-new-board-btn"); + addBoardButton.addEventListener('click', function () { + dataHandler.createNewBoard('board', dom.showNewBoard); + }) + }, + showNewBoard: function (response) { + let boardContainer = document.querySelector('.board-container'); - const outerHtml = ` - - `; + const template = document.querySelector('#board-template'); + const clone = document.importNode(template.content, true); + clone.querySelector('.board-title').innerHTML = response.title; + boardContainer.appendChild(clone); + }, + addNewCard: function (boardId) { + let addNewCardButtons = document.querySelectorAll(".board-add"); + for (let button of addNewCardButtons) { + if (parseInt(button.id) === parseInt(boardId)) { + button.addEventListener('click', function () { + dataHandler.createNewCard("New Card", boardId, 0, dom.showNewCard); + }); + } + } + }, + showNewCard: function (response) { + let columns = document.querySelectorAll('.board-column-content'); - this._appendToElement(document.querySelector('#boards'), outerHtml); + for (let column of columns) { + + if (parseInt(response.board_id) === parseInt(column.dataset.boardId) && + parseInt(response.status_id) === parseInt(column.id)) { + const template = document.querySelector('#cards-template'); + const clone = document.importNode(template.content, true); + clone.querySelector('.card-title').textContent = response.title; + column.appendChild(clone); + } + } }, - loadCards: function (boardId) { - // retrieves cards and makes showCards called + deleteBoard: function (boardId) { + + let deleteBoardButtons = document.querySelectorAll('.board-delete'); + for (let button of deleteBoardButtons) { + if (parseInt(button.id) === parseInt(boardId)) { + button.addEventListener('click', function () { + dataHandler.deleteBoard(boardId, dom.loadBoards) + }) + } + } }, - showCards: function (cards) { - // shows the cards of a board - // it adds necessary event listeners also + initDeleteCardButtons: function (cards, boardId) { + let deleteCardButtons = document.querySelectorAll(`div[data-board-id="${boardId}"] .fa-trash-alt`); + for (let button of deleteCardButtons) { + button.addEventListener('click', function () { + const card_id = this.parentElement.parentElement.id + dataHandler.deleteCard(card_id, dom.loadBoards); + }) + } + }, + addDragula: function () { + var dragulaWatcher = null; + dragulaWatcher = setInterval(function () { + if (typeof dragula != 'function') return; + + clearInterval(dragulaWatcher); + let columnList = document.querySelectorAll('.board-column-content'); + // console.log(columnList); + let columnListArray = Array.from(columnList); + + dragula(columnListArray).on('drop', dom.get_new_container); + }, 500); + + }, + get_new_container: function (el) { + let card = el; + let new_container = el.parentElement; + let status_id = new_container.id; + let card_id = card.id; + dataHandler.change_status(card_id, status_id); }, - // here comes more features -}; + + switchToInput: function () { + let boardHeader = this.parentElement; + let originalTitleSpan = boardHeader.querySelector('.board-title'); + let newInputField = document.createElement('input'); + + newInputField.value = originalTitleSpan.textContent; + newInputField.classList.add('input-field'); + + originalTitleSpan.classList.add('hidden'); + + boardHeader.appendChild(newInputField); + + document.addEventListener('keydown', dom.checkKeyDown); + }, + checkKeyDown: function (event) { + let inputField = document.querySelector('.input-field'); + let boardHeader = inputField.parentElement; + let originalTitleSpan = boardHeader.querySelector('.board-title'); + let originalTitle = originalTitleSpan.textContent; + + let boardId = parseInt(boardHeader.querySelector('.board-add').id); + + let newTitle; + + if (event.key === "Enter") { + newTitle = inputField.value; + originalTitleSpan.textContent = newTitle; + inputField.remove(); + originalTitleSpan.classList.remove('hidden'); + dataHandler.updateBoardName(boardId, newTitle, dom.loadBoards); + + } else if (event.key === "Escape") { + newTitle = originalTitle; + originalTitleSpan.textContent = newTitle; + inputField.remove(); + originalTitleSpan.classList.remove('hidden'); + + } else { + return + } + }, + userReg: function () { + const signUp = document.querySelector('#sign-up'); + // const navLogin = document.querySelector('#nav-login'); + // const navReg = document.querySelector('#nav-reg'); + const regUsername = document.querySelector('#reg-username'); + const regPw = document.querySelector('#reg-user-pass'); + signUp.addEventListener('click', function () { + const data = {'username': regUsername.value, 'password': regPw.value}; + dataHandler.reg(data, dom.newUserResp); + document.querySelector('#sign-up').setAttribute('data-dismiss', 'modal'); + }) + }, + newUserResp: function (resp) { + } + +}; \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index f9298ae2..15dee0f5 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -6,7 +6,7 @@ function init() { dom.init(); // loads the boards to the screen dom.loadBoards(); - + dom.addNewBoard(); + dom.userReg(); } - init(); diff --git a/templates/index.html b/templates/index.html index 6f42f26e..04014b9f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,21 +1,149 @@ - + + + ProMan + + + + + + + - - - - - - - - -

    ProMan

    -
    Boards are loading...
    - - \ No newline at end of file + + + + + + + + + + + + + + + + + +

    ProMan

    + +
    + +
    + +
    +
    + + + + +{% include 'template.html' %} + + + + + + diff --git a/templates/template.html b/templates/template.html new file mode 100644 index 00000000..5c58409c --- /dev/null +++ b/templates/template.html @@ -0,0 +1,40 @@ +{#board template#} + + + +{#cards template#} + \ No newline at end of file diff --git a/util.py b/util.py index 352cbd88..0bc33ce3 100644 --- a/util.py +++ b/util.py @@ -1,5 +1,6 @@ from functools import wraps from flask import jsonify +import bcrypt def json_response(func): @@ -13,3 +14,9 @@ def decorated_function(*args, **kwargs): return jsonify(func(*args, **kwargs)) return decorated_function + + +def hash_password(plain_text_password): + # By using bcrypt, the salt is saved into the hash itself + hashed_bytes = bcrypt.hashpw(plain_text_password.encode('utf-8'), bcrypt.gensalt()) + return hashed_bytes.decode('utf-8')