diff --git a/data_handler.py b/data_handler.py index 0d8d9086..7fed4f96 100644 --- a/data_handler.py +++ b/data_handler.py @@ -19,6 +19,10 @@ def get_boards(): return persistence.get_boards(force=True) +def add_new_board(title): + return persistence.add_new_board(title) + + def get_cards_for_board(board_id): persistence.clear_cache() all_cards = persistence.get_cards() diff --git a/database_connection.py b/database_connection.py new file mode 100644 index 00000000..7925d19b --- /dev/null +++ b/database_connection.py @@ -0,0 +1,105 @@ +import os +import psycopg2 +import psycopg2.extras + + +def establish_connection(connection_data=None): + """ + Create a database connection based on the :connection_data: parameter + + :connection_data: Connection string attributes + + :returns: psycopg2.connection + """ + if connection_data is None: + connection_data = get_connection_data() + try: + connect_str = "dbname={} user={} host={} password={}".format(connection_data['dbname'], + connection_data['user'], + connection_data['host'], + connection_data['password']) + conn = psycopg2.connect(connect_str) + conn.autocommit = True + except psycopg2.DatabaseError as e: + print("Cannot connect to database.") + print(e) + else: + return conn + + +def get_connection_data(db_name=None): + """ + Give back a properly formatted dictionary based on the environment variables values which are started + with :MY__PSQL_: prefix + + :db_name: optional parameter. By default it uses the environment variable value. + """ + if db_name is None: + db_name = os.environ.get('MY_PSQL_DBNAME') + + return { + 'dbname': db_name, + 'user': os.environ.get('MY_PSQL_USER'), + 'host': os.environ.get('MY_PSQL_HOST'), + 'password': os.environ.get('MY_PSQL_PASSWORD') + } + + +def execute_script_file(file_path): + """ + Execute script file based on the given file path. + Print the result of the execution to console. + + Example: + > execute_script_file('db_schema/01_create_schema.sql') + + :file_path: Relative path of the file to be executed. + """ + package_directory = os.path.dirname(os.path.abspath(__file__)) + full_path = os.path.join(package_directory, file_path) + with open(full_path) as script_file: + with establish_connection() as conn, \ + conn.cursor() as cursor: + try: + sql_to_run = script_file.read() + cursor.execute(sql_to_run) + print("{} script executed successfully.".format(file_path)) + except Exception as ex: + print("Execution of {} failed".format(file_path)) + print(ex.args) + + +def execute_select(statement, variables=None): + """ + Execute SELECT statement optionally parameterized + + Example: + > execute_select('SELECT %(title)s; FROM shows', variables={'title': 'Codecool'}) + + :statement: SELECT statement + + :variables: optional parameter dict""" + result_set = [] + with establish_connection() as conn: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor: + cursor.execute(statement, variables) + result_set = cursor.fetchall() + return result_set + + +def execute_dml_statement(statement, variables=None): + """ + Execute data manipulation query statement (optionally parameterized) + + :statment: SQL statement + + :variables: optional parameter dict""" + result = None + with establish_connection() as conn: + with conn.cursor() as cursor: + cursor.execute(statement, variables) + try: + result = cursor.fetchone() + except psycopg2.ProgrammingError as pe: + pass + return result diff --git a/design-materials/design.html b/design-materials/design.html index 60ece6a0..3a5e36d2 100644 --- a/design-materials/design.html +++ b/design-materials/design.html @@ -8,7 +8,7 @@ - +

ProMan

@@ -18,7 +18,7 @@

ProMan

Board 1 - +
diff --git a/main.py b/main.py index 9e25a1ac..5a5305d1 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,9 @@ -from flask import Flask, render_template, url_for +from flask import Flask, render_template, url_for, request from util import json_response - +import persistence import data_handler +import json + app = Flask(__name__) @@ -20,7 +22,8 @@ def get_boards(): """ All the boards """ - return data_handler.get_boards() + + return persistence.get_sql_boards() @app.route("/get-cards/") @@ -33,6 +36,14 @@ def get_cards_for_board(board_id: int): return data_handler.get_cards_for_board(board_id) +@app.route('/add-new-board', methods=["GET", "POST"]) +@json_response +def add_new_board(): + title = request.get_json()['title'] + return data_handler.add_new_board(title) + + + def main(): app.run(debug=True) diff --git a/persistence.py b/persistence.py index 1e1f1e3d..9e1c9929 100644 --- a/persistence.py +++ b/persistence.py @@ -1,51 +1,9 @@ -import csv +import database_connection -STATUSES_FILE = './data/statuses.csv' -BOARDS_FILE = './data/boards.csv' -CARDS_FILE = './data/cards.csv' +def get_sql_boards(): + return database_connection.execute_select('SELECT * FROM boards;') -_cache = {} # We store cached data in this dict to avoid multiple file readings - -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 - - -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] - - -def clear_cache(): - for k in list(_cache.keys()): - _cache.pop(k) - - -def get_statuses(force=False): - return _get_data('statuses', STATUSES_FILE, force) - - -def get_boards(force=False): - return _get_data('boards', BOARDS_FILE, force) - - -def get_cards(force=False): - return _get_data('cards', CARDS_FILE, force) +def add_new_board(title): + return database_connection.execute_dml_statement("""INSERT INTO boards (title) + VALUES (%(title)s) RETURNING *""", dict(title=title)) diff --git a/design-materials/design.css b/static/css/design.css similarity index 94% rename from design-materials/design.css rename to static/css/design.css index a8b8f2f2..177c538b 100644 --- a/design-materials/design.css +++ b/static/css/design.css @@ -37,6 +37,15 @@ button{ margin: 0 auto; } +.board-template { + background: #ffffff90; +} + +.add-new-board-button { + text-align: center; + margin-bottom: 5vh; +} + section.board{ margin: 20px; border: aliceblue; diff --git a/static/js/data_handler.js b/static/js/data_handler.js index 66df0ba8..67c17f21 100644 --- a/static/js/data_handler.js +++ b/static/js/data_handler.js @@ -8,7 +8,7 @@ export let dataHandler = { _api_get: function (url, callback) { // it is not called from outside // loads data from API, parses it and calls the callback with it - + const json = JSON.parse('[{"id": "1", "title": "Board 1"}, {"id": "2", "title": "Board 2"}]'); fetch(url, { method: 'GET', credentials: 'same-origin' @@ -19,6 +19,17 @@ export let dataHandler = { _api_post: function (url, data, callback) { // it is not called from outside // sends the data to the API, and calls callback function + fetch(url, { + method: 'POST', + credentials: 'same-origin', + headers: { + "Content-Type": "application/json", + // "Content-Type": "application/x-www-form-urlencoded", + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) // parse the response as JSON + .then(json_response => callback(json_response)); }, init: function () { }, @@ -32,6 +43,17 @@ export let dataHandler = { callback(response); }); }, + + addNewBoard: function (callback) { + // the boards are retrieved and then the callback function is called with the boards + + // Here we use an arrow function to keep the value of 'this' on dataHandler. + // if we would use function(){...} here, the value of 'this' would change. + this._api_post('/add-new-board', {title: 'New Board'}, (response) => { + this._data = response; + callback(response); + }); + }, getBoard: function (boardId, callback) { // the board is retrieved and then the callback function is called with the board }, diff --git a/static/js/dom.js b/static/js/dom.js index 0092afb7..a12e7d76 100644 --- a/static/js/dom.js +++ b/static/js/dom.js @@ -26,26 +26,65 @@ export let dom = { dom.showBoards(boards); }); }, + + + getBoardTitle: function (title) { + const template = document.querySelector('#board'); + const clone = document.importNode(template.content, true); + + if (title[1] == 'New Board') { + clone.querySelector('.board-title').textContent = title[1]; + console.log("yes") + } else { + clone.querySelector('.board-title').textContent = title; + } + + + return clone; + }, + + showBoards: function (boards) { // shows boards appending them to #boards div // it adds necessary event listeners also + //console.log(clone); + let boardList = document.createElement("section"); + boardList.id ="board"; + for (let board of boards) { + boardList.appendChild(this.getBoardTitle(board.title)) + } - let boardList = ''; + let container = document.querySelector('.board-container'); + container.appendChild(boardList); - for(let board of boards){ - boardList += ` -
  • ${board.title}
  • - `; - } + }, - const outerHtml = ` -
      - ${boardList} -
    - `; - this._appendToElement(document.querySelector('#boards'), outerHtml); + addNewBoardEventListener: function () { + let addNewBoardButton = document.getElementsByClassName("add-new-board-button"); + addNewBoardButton[0].addEventListener("click", this.addNewBoardClickHandler) }, + + + addNewBoardClickHandler: function () { + dataHandler.addNewBoard(function (newCardTitle) { + dom.showBoard(newCardTitle); + }) + + }, + + showBoard: function (newCardTitle) { + let boardList = document.createElement("section"); + boardList.id ="board"; + + boardList.appendChild(this.getBoardTitle(newCardTitle)); + + + + let container = document.querySelector('.board-container'); + container.appendChild(boardList); + }, + loadCards: function (boardId) { // retrieves cards and makes showCards called }, diff --git a/static/js/main.js b/static/js/main.js index f9298ae2..4311419e 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -6,6 +6,8 @@ function init() { dom.init(); // loads the boards to the screen dom.loadBoards(); + // add event listener on 'add new board' button + dom.addNewBoardEventListener() } diff --git a/templates/board_template.html b/templates/board_template.html new file mode 100644 index 00000000..909f40c8 --- /dev/null +++ b/templates/board_template.html @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 6f42f26e..3d5bf657 100644 --- a/templates/index.html +++ b/templates/index.html @@ -11,11 +11,35 @@ - + + + + + + + + + + + + + + + +

    ProMan

    -
    Boards are loading...
    + +
    + +
    + +
    + +
    + {% include "board_template.html" %} +
    - \ No newline at end of file +